01 企业级秒杀系统项目介绍:Redis 预扣 + RocketMQ 削峰 + 一致性修复闭环,如何抗住 1 万并发?
01 企业级秒杀系统项目介绍:Redis 预扣 + RocketMQ 削峰 + 一致性修复闭环,如何抗住 1 万并发?
做“秒杀”最怕两句话:
- “压测能过,上线就挂”——因为真实流量并不均匀
- “一致性靠运气”——因为异步链路没有补偿闭环
susan_seckill 秒杀系统是一套能跑起来、能解释清楚、能被复用推广的企业级秒杀工程:入口把无效流量挡住,核心链路把写压削峰,最终靠一致性修复把系统状态拉回正确值。
目录
- 一图看懂:架构与数据闭环
- 亮点 1:入口限压不是一句话,是一组可执行的“门禁”
- 亮点 2:Redis 原子预扣,把 DB 行锁压力转移到缓存原子操作
- 亮点 3:RocketMQ 异步下单削峰,并且内置降级通道
- 亮点 4:数据库为准 + Redis/ES 最终一致性 + 异步补偿
- 亮点 5:库存修复定时任务:一致性不是“相信”,而是“校验 + 修复”
- 亮点 6:分布式锁 + Lua 原子解锁,保障修复与扣减互斥
- 亮点 7:排队页与渐进反馈,让用户在前端等,把写流量拉平
- 亮点 8:用户级幂等防重与重复秒杀拦截
- 亮点 9:结果查询读优化-Redis命中-DB兜底
- 亮点 10:秒杀处理状态机-进度与文案可观测
- 亮点 11:读链路走ES减压-订单商品信息不打库
- 亮点 12:订单超时延迟消息-自动取消并退还库存
- 亮点 13:补偿线程池参数化-CallerRuns保底不丢任务
- 亮点 14:库存修复监控指标-以数据驱动治理
- 亮点 15:锁模板化执行-一行代码包住临界区
- 真实用户评价
- 加入星球
- 脱敏用户反馈:为什么这套方案“容易被认同”
- SEO FAQ:搜索引擎友好问答(含 Schema)
一图看懂:架构与数据闭环
秒杀系统的系统架构图:

一句话讲清楚:入口轻量化过滤 + Redis 原子预扣 + MQ 异步削峰 + DB 为准的一致性补偿 + 定时修复自愈。
技术架构图
秒杀功能核心流程图:

秒杀系统的技术架构图:

亮点 1:入口限压不是一句话,是一组可执行的“门禁”
秒杀的第一原则:让非法/无效/重复请求尽可能早地失败,而且失败要便宜。
入口核心片段(UserSeckillProductServiceImpl#doSeckillProduct):
// 从本地缓存中校验库存
Long seckillProductId = userSeckillProductEntity.getSeckillProductId();
if (BooleanUtils.isTrue(LOCAL_LOW_IN_STOCK_MAP.get(seckillProductId))) {
throw new BusinessException("该商品已售罄");
}
// 校验验证码与活动时间窗口
userService.checkCode(userSeckillProductEntity.getUuid(), userSeckillProductEntity.getCode());
checkTime(userSeckillProductEntity);这里的“专业”不在于代码多,而在于策略清晰:
- 本地售罄标记:把“售罄后的重试洪峰”挡在 JVM 内存层
- 验证码/时间窗:把机器流量与过期请求挡在业务写路径之外
亮点 2:Redis 原子预扣,把 DB 行锁压力转移到缓存原子操作
秒杀能抗压的核心之一:不要把“瞬时高并发写”打到数据库。
项目在入口处用 Redis 原子自减进行预扣(UserSeckillProductServiceImpl#doSeckillProduct):
Long stock = redisUtil.decrement(productStockKey);
if (stock < 0) {
LOCAL_LOW_IN_STOCK_MAP.put(seckillProductId, true);
throw new BusinessException("该商品已售罄");
}而 redisUtil.decrement 的底层是 Redis 原子操作(RedisUtil#decrement):
public Long decrement(String key) {
return stringRedisTemplate.opsForValue().increment(key, -1);
}这一步的工程价值:
- 原子性由 Redis 单线程模型保障
- 极大降低 DB 写压力与行锁冲突概率
亮点 3:RocketMQ 异步下单削峰,并且内置降级通道
削峰填谷不是“接上 MQ 就算完工”,关键在于:投递失败时如何兜底。
项目在发送端使用异步发送,并在异常时降级同步发送(UserSeckillProductServiceImpl#send):
rocketMQTemplate.asyncSend(topic, jsonMessage, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
log.info("消息发送成功, topic:{}, messageId:{}, queueId:{}, brokerName:{}",
topic, sendResult.getMsgId(), sendResult.getMessageQueue().getQueueId(),
sendResult.getMessageQueue().getBrokerName());
}
@Override
public void onException(Throwable throwable) {
log.error("消息发送失败, topic:{}, message:{}, error:{}",
topic, jsonMessage, throwable.getMessage(), throwable);
if (throwable.getMessage().contains("timeout") ||
throwable.getMessage().contains("RemotingTooMuchRequestException")) {
rocketMQTemplate.syncSend(topic, jsonMessage);
}
}
}, 30000);能让人“想点赞”的点:失败路径不是口头承诺,而是写进了代码。
亮点 4:数据库为准 + Redis/ES 最终一致性 + 异步补偿
异步化之后的一致性,不是靠“相信 MQ”,而是靠“数据库为准 + 补偿”。
库存扣减逻辑(StockConsistencyServiceImpl#reduceStock):
// 1. 扣减数据库库存
for (int i = 0; i < quantity; i++) {
int result = seckillProductMapper.reduceWithHoldStock(seckillProductId);
if (result <= 0) {
log.error("扣减数据库库存失败,商品ID:{},第{}次扣减", seckillProductId, i + 1);
break;
}
}
// 2. 扣减Redis库存
try {
String productStockKey = getProductStockKey(seckillProductId);
redisUtil.increment(productStockKey, -quantity);
redisSuccess = true;
} catch (Exception e) {
log.error("扣减Redis库存失败,商品ID:{},数量:{},异常:{}",
seckillProductId, quantity, e.getMessage(), e);
}
// 5. 如果Redis或ES失败,启动异步补偿
if (!redisSuccess || !esSuccess) {
asyncCompensateStock(seckillProductId, -quantity, !redisSuccess, !esSuccess);
}这段代码释放了两个强信号:
- 数据库是事实来源:以 DB 操作为准,缓存/搜索是加速器
- 失败可恢复:有补偿路径,系统能靠机制拉回正确状态
亮点 5:库存修复定时任务:一致性不是“相信”,而是“校验 + 修复”
很多系统的最终一致性停留在“补偿线程池”,但线上真正难的是:异常总会发生,如何自愈。
项目在 seckill-job 中提供库存修复调度器(StockRepairScheduler),把一致性检查与修复变成持续运行的治理能力:
@Scheduled(fixedRate = 5 * 60 * 1000)
public void repairStockTask() {
executeScheduledTask("常规库存修复任务", () -> {
stockRepairMetrics.recordActiveRepairTask();
String result = stockRepairService.repairAllProductsStock();
log.info("常规库存修复任务完成:{}", result);
});
}
@Scheduled(fixedRate = 10 * 60 * 1000)
public void checkStockConsistencyTask() {
executeScheduledTask("库存一致性检查任务", () -> {
stockRepairMetrics.recordConsistencyCheckTask();
String result = performConsistencyCheck();
log.info("库存一致性检查任务完成:{}", result);
});
}这就是企业级系统的“底色”:把不可控的异常变成可治理的机制。
亮点 6:分布式锁 + Lua 原子解锁,保障修复与扣减互斥
库存修复与库存扣减在并发下必须互斥,否则修复会“把正确值修错”。
项目用 Redis 分布式锁实现互斥,并用 Lua 脚本保证解锁原子性(DistributedLockUtil):
private static final String UNLOCK_SCRIPT =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";配合释放锁:
Long result = stringRedisTemplate.execute(script,
Collections.singletonList(fullLockKey), lockValue);
boolean success = result != null && result == 1L;这段非常“面试官友好”:你不仅说明了锁,还解释了为什么需要 Lua(避免误删别人的锁)。
亮点 7:排队页与渐进反馈,让用户在前端等,把写流量拉平
高并发系统的体验优化,本质是流量治理。
项目提供排队页 queue.html:展示排队进度,并以固定频率轮询结果,减少用户反复刷新对后端造成的抖动。
关键片段(queue.html):
setTimeout(() => {
this.startResultCheck();
}, 8000);
this.checkInterval = setInterval(() => {
this.checkSeckillResult();
}, 2000);
const response = await axios.get('/getResult', {
params: {
seckillProductId: this.seckillProductId,
uuid: this.uuid,
code: this.code
},
headers: this.getHeaders()
});
亮点 8:用户级幂等防重与重复秒杀拦截
秒杀系统在入口层的第一条强约束是:同一用户同一商品只允许成功一次,重复请求必须“低成本失败”,否则数据库会被重复下单、重复查单拖垮。
关键代码片段(UserSeckillProductServiceImpl#doSeckillProduct):
String userSeckillRecord = redisUtil.get(getUserSeckillProductKey(seckillProductId, currentUser.getUsername()));
if (StringUtils.hasLength(userSeckillRecord)) {
throw new BusinessException("您已经秒杀过该商品了,请勿重复秒杀");
}亮点 9:结果查询读优化-Redis命中-DB兜底
高并发场景下,“下单接口”不是唯一热点,“结果查询接口”同样会被排队页轮询放大。这里的做法是:用 Redis 先返回订单 ID,再按需回源 DB 拿完整订单数据,避免每次轮询都打库。
关键代码片段(UserSeckillProductServiceImpl#getResult):
String userSeckillProductKey = getUserSeckillProductKey(userSeckillProductQueryEntity.getSeckillProductId(), currentUser.getUsername());
String orderId = redisUtil.get(userSeckillProductKey);
if (!StringUtils.hasLength(orderId)) {
return null;
}
SeckillOrderTradeEntity seckillOrderTradeEntity = seckillOrderTradeMapper.findById(Long.valueOf(orderId));
return seckillOrderTradeEntity;亮点 10:秒杀处理状态机-进度与文案可观测
很多系统在异步化之后,用户体验会变成“盲等”。项目把“处理状态”做成一个轻量状态机:用 Redis 记录 status:progress:message,配 5 分钟 TTL,前端可轮询展示进度与文案。
关键代码片段(UserSeckillProductServiceImpl#getResultWithProgress):
String statusKey = getSeckillProcessStatusKey(seckillProductId, userName);
String statusValue = redisUtil.get(statusKey);
if (statusValue == null) {
return SeckillResultResponse.failure("未找到秒杀记录");
}
String[] statusParts = statusValue.split(":");
int status = Integer.parseInt(statusParts[0]);
int progress = Integer.parseInt(statusParts[1]);
String message = statusParts[2];以及状态写入(5 分钟过期):
String statusKey = getSeckillProcessStatusKey(seckillProductId, userName);
String statusValue = status + ":" + progress + ":" + message;
redisUtil.set(statusKey, statusValue, 300);亮点 11:读链路走ES减压-订单商品信息不打库
秒杀的写路径已经很重,读路径必须尽量轻。
项目在创建订单时从 ES 获取商品信息,减少 DB 热点读压力,同时保持展示数据的实时性(最终一致语义)。
关键代码片段
(UserSeckillProductServiceImpl#createOrder / createUserSeckillProductOrderEntity):
ESSeckillProductEntity latestEsProduct = productService.getProductFromES(seckillProductId);
if (Objects.isNull(latestEsProduct)) {
setSeckillProcessStatus(seckillProductId, userName, 2, 100, "商品不存在");
return;
}ESSeckillProductEntity esProduct = productService.getProductFromES(seckillProductId);
AssertUtil.notNull(esProduct, String.format("秒杀商品[%s]不存在", seckillProductId));
userSeckillProductOrderEntity.setProductId(esProduct.getProductId());
userSeckillProductOrderEntity.setProductName(esProduct.getName());
userSeckillProductOrderEntity.setPrice(esProduct.getPrice());亮点 12:订单超时延迟消息-自动取消并退还库存
秒杀系统的“支付链路”必须与“下单链路”解耦,否则支付慢会拖垮核心路径。
项目使用 RocketMQ 延迟消息实现订单超时处理:到期未支付则取消订单,并通过一致性服务退还库存(DB/Redis/ES)。
关键代码片段(OrderTimeoutServiceImpl#sendOrderTimeoutMessage):
int delayLevel = calculateDelayLevel(timeoutMinutes);
rocketMQTemplate.asyncSend(
orderTimeoutTopic,
MessageBuilder.withPayload(JSONUtil.toJsonStr(timeoutMessage)).build(),
new SendCallback() { /* onSuccess/onException */ },
3000L,
delayLevel
);超时取消与恢复库存(OrderTimeoutServiceImpl#cancelOrderAndRestoreStock):
boolean restoreSuccess = stockConsistencyService.restoreStock(
order.getSeckillProductId(), order.getQuantity());亮点 13:补偿线程池参数化-CallerRuns保底不丢任务
“最终一致性”落地靠补偿任务,但补偿任务在高峰期也会爆。
项目把补偿执行器显式配置为线程池,并通过 CallerRuns 策略保证在队列打满时也不会静默丢任务。
关键代码片段(StockConsistencyConfig#stockCompensationExecutor):
executor.setCorePoolSize(2);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("stock-compensation-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setAllowCoreThreadTimeOut(true);
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);亮点 14:库存修复监控指标-以数据驱动治理
“能跑”只是第一步,“能被运营”才是企业级。
项目提供库存修复指标服务,用原子变量统计修复次数、失败次数、耗时、错误分布等,为后续接入告警与看板提供数据底座。
关键代码片段(StockRepairMetricsServiceImpl):
private final AtomicLong totalRepairAttempts = new AtomicLong(0);
private final AtomicLong totalRepairSuccess = new AtomicLong(0);
private final AtomicLong totalRepairFailures = new AtomicLong(0);
private final AtomicLong totalInconsistenciesFound = new AtomicLong(0);
private final AtomicLong totalRepairTimeMs = new AtomicLong(0);
private final AtomicInteger maxRepairTimeMs = new AtomicInteger(0);以及指标摘要输出:
log.info("总修复尝试次数: {}", totalRepairAttempts.get());
log.info("总修复成功次数: {}", totalRepairSuccess.get());
log.info("发现不一致次数: {}", totalInconsistenciesFound.get());亮点 15:锁模板化执行-一行代码包住临界区
在库存修复、库存操作这种天然“临界区”场景里,锁的获取/释放最怕写散、写漏。
项目提供 executeWithLock 模板方法:成功获取锁才执行,最后统一释放锁,极大降低误用概率。
关键代码片段(DistributedLockUtil#executeWithLock):
String lockValue = acquireLock(lockKey, expireTime);
if (lockValue == null) {
log.warn("获取锁失败,无法执行业务逻辑,lockKey:{}", lockKey);
return null;
}
try {
return business.execute();
} finally {
releaseLock(lockKey, lockValue);
}真实用户评价
如果你关心的不只是“这套秒杀系统能不能跑”,而是“学完之后能不能真正补齐高并发项目经验、面试表达和简历说服力”,下面这组内容就是站内已有的真实用户反馈截图。为了保护隐私,这里继续沿用匿名化截图展示。






加入星球
如果你想要的不只是“看完文章觉得秒杀系统很强”,而是直接拿到完整源码、配套教程、答疑支持和简历包装思路,那么加入 Java突击队 星球会更直接。
秒杀系统只是其中一个代表性高并发项目,真正的价值是把多个实战项目、技术专题和求职支持打包到同一个学习闭环里。