
秒杀全链路流程整体架构用户点击限时抢购│▼┌──────────────────────────────────────────────┐│ 同步部分 (主线程, 5ms 返回) ││ ││ ① 前端校验 → ② 后端兜底查DB加载库存 ││ → ③ 生成全局唯一订单ID ││ → ④ Lua脚本原子执行(库存扣减去重写Stream) ││ → ⑤ 返回订单号给用户 │└──────────────────────────────────────────────┘│ XADD stream.orders▼┌──────────────────────────────────────────────┐│ 异步部分 (单线程消费, 后台慢慢写DB) ││ ││ ⑥ XREADGROUP 读取Stream消息 ││ → ⑦ Redisson分布式锁 ││ → ⑧ Transactional 写MySQL ││ ├─ DB幂等校验 ││ ├─ 乐观锁扣库存 ││ └─ INSERT 订单 ││ → ⑨ XACK确认消费 ││ ││ 失败处理: 不ACK → Pending重试 → 死循环兜底 │└──────────────────────────────────────────────┘阶段一: 同步响应主线程3~5ms第 0 步: 前端调用用户在商铺详情页点限时抢购按钮shop-detail.html 发请求axios.post(/voucher-order/seckill/ voucherId)请求经过两层拦截器RefreshTokenInterceptor 从 authorization header 读取 token 加载用户到 ThreadLocal → LoginInterceptor 校验通过。第 1 步: 兜底加载 Redis 库存// VoucherOrderServiceImpl.seckillVoucher()Long userId UserHolder.getUser().getId(); // 从ThreadLocal拿当前用户String stockKey seckill:stock: voucherId;Boolean hasStock stringRedisTemplate.hasKey(stockKey);if (Boolean.FALSE.equals(hasStock)) {// Redis里没有库存key → 从DB加载到RedisSeckillVoucher voucher seckillVoucherService.getById(voucherId);stringRedisTemplate.opsForValue().set(stockKey, voucher.getStock().toString());}seckill:stock:10 是一个 Redis String存库存数量。如果这个 key 不存在Redis 重启后从 DB 的 tb_seckill_voucher.stock 字段恢复。第 2 步: 生成全局唯一订单 IDlong orderId redisIdWorker.nextId(order);RedisIdWorker 生成一个 Snowflake 风格的 64 位 ID高 32 位 当前秒 - 基准时间戳(2022-01-01 00:00:00)低 32 位 Redis INCR icr:order:2026:07:04 的返回值比如 609848294759202817 这个数字拆开就是 时间戳部分 自增序列号保证全局唯一 趋势递增。第 3 步: Lua 脚本原子执行核心-- KEYS来自ARGV参数local voucherId ARGV[1] -- 10local userId ARGV[2] -- 1010local orderId ARGV[3] -- 609848294759202817local stockKey seckill:stock: .. voucherId -- seckill:stock:10local orderKey seckill:order: .. voucherId -- seckill:order:10-- ① 判库存local stock redis.call(get, stockKey)if (not stock or tonumber(stock) 0) thenreturn 1 -- 库存不足end-- ② 判重复if (redis.call(sismember, orderKey, userId) 1) thenreturn 2 -- 不能重复下单end-- ③ 扣库存 记录用户 写消息队列三步原子redis.call(incrby, stockKey, -1) -- stock stock - 1redis.call(sadd, orderKey, userId) -- Set里加userId防重redis.call(xadd, stream.orders, *, -- Stream里写订单userId, userId,voucherId, voucherId,id, orderId)return 0 -- 成功这段 15 行 Lua 是整个秒杀方案的灵魂。关键点操作 Redis 命令 O(1) 作用查库存 GET ✅ 判断是否还有券查重复 SISMEMBER ✅ Set 集合判断用户是否买过扣库存 INCRBY ✅ 原子减 1去重 SADD ✅ 记录已购用户发消息 XADD ✅ 写 Stream 消息队列五个操作在同一个 Lua 脚本里执行Redis 单线程保证不会被其他命令插入——这就是零锁开销的原子性。第 4 步: 返回结果Long result stringRedisTemplate.execute(getSeckillScript(),Collections.emptyList(),voucherId.toString(), userId.toString(), String.valueOf(orderId));int r result.intValue();if (r 1) return Result.fail(库存不足);if (r 2) return Result.fail(不能重复下单);return Result.ok(orderId); // ← 用户拿到订单号前端弹抢购成功从用户点击到拿到订单号主线程只做了 3 件事① 兜底加载库存极少数情况触发② 生成 ID1 次 Redis INCR③ 执行 Lua1 次 EVAL。总共 2-3 次网络往返5ms 返回。完全没碰 MySQL这就是快的秘诀。阶段二: 异步消费后台线程Lua 脚本的 XADD stream.orders * userId voucherId orderId 把订单信息写入了 Redis Stream。下面是怎么消费的第 5 步: 消费者线程启动应用启动时 PostConstruct 自动拉起一个单线程 ConsumerPostConstructpublic void init() {SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());}这个线程是一个 while (running) 死循环一直在读 Stream。第 6 步: 从 Stream 读消息// Consumer Group: g1, Consumer: c1ListMapRecordString, Object, Object list stringRedisTemplate.opsForStream().read(Consumer.from(g1, c1),StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),StreamOffset.create(stream.orders, ReadOffset.lastConsumed()));XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS stream.orders → 从上次消费位置拉一条新消息。没有消息就阻塞等 2 秒有消息立刻返回。第 7 步: 解析消息 → 加锁 → 写 DBVoucherOrder voucherOrder parseVoucherOrder(values); // Map → VoucherOrder对象if (handleVoucherOrder(voucherOrder)) {// 成功 → ACK确认stringRedisTemplate.opsForStream().acknowledge(queueName, g1, record.getId());}// 失败 → 不ACK → 消息留在Pending List → 等重试handleVoucherOrder 内部RLock lock redissonClient.getLock(lock:order: userId);boolean isLock lock.tryLock();if (!isLock) return false; // 拿不到锁 → 不ACK等重试try {IVoucherOrderService proxy applicationContext.getBean(IVoucherOrderService.class);proxy.createVoucherOrder(voucherOrder); // Transactional 实际写DBreturn true;} finally {lock.unlock();}第 8 步: createVoucherOrder — 写 DB 的三层防护Transactional(rollbackFor Exception.class)public void createVoucherOrder(VoucherOrder voucherOrder) {// ① 幂等校验DB里是否已有记录int count query().eq(user_id, userId).eq(voucher_id, voucherId).count();if (count 0) return; // 已存在 → 跳过非异常 — 重试安全// ② 乐观锁扣库存boolean success seckillVoucherService.update().setSql(stock stock - 1).eq(voucher_id, voucherId).gt(stock, 0) // ← WHERE stock 0.update();if (!success) throw new RuntimeException(DB库存扣减失败);// ③ 保存订单save(voucherOrder);}三层幂等校验防重复消息 乐观锁防超卖 事务回滚失败全部撤销。第 9 步: ACK 确认DB 写入成功 → XACK stream.orders g1 消息ID → Stream 标记该消息已处理。阶段三: 失败恢复Pending List 重试消息处理失败不 ACK → Redis 标记为 Pending → handlePandingList() 定时扫描// XREADGROUP GROUP g1 c1 COUNT 1 STREAMS stream.orders 0// ← 0 表示从 Pending List 开头读ListMapRecordString, Object, Object list stringRedisTemplate.opsForStream().read(Consumer.from(g1, c1),StreamReadOptions.empty().count(1),StreamOffset.create(queueName, ReadOffset.from(0)));同一批消息循环重试直到全部 ACK 完毕才 break。NOGROUP 兜底Stream 消费者组不存在时ensureConsumerGroup() 自动创建——先 XADD 一条 dummy 消息创建 Stream → 创建 Group → 删除 dummy。 五层防护总结层 位置 方式 防什么1 Lua if stock 0 → return 1 库存判断原子化2 Lua SISMEMBER orderKey userId Redis 层去重3 Java 异步 Redisson lock.tryLock() 分布式并发安全4 DB UPDATE ... WHERE stock 0 乐观锁二次兜底5 DB SELECT count(*) 幂等校验 Pending 重试安全15 行 Lua 200 行 Java构建了一套前端瞬间返回后台异步处理失败自动重试的完整秒杀链路。