一、前言:为什么用 Redis Stream 做秒杀异步下单?
在高并发秒杀场景中,同步创建订单会导致:
- 数据库连接池耗尽
- 接口响应时间飙升(500ms → 5s+)
- 系统雪崩风险极高
而传统的 List 队列虽能异步,但缺乏 ACK 机制,一旦消费失败,订单就永久丢失!
Redis Stream + 消费者组 完美解决了这一痛点:
✅ 消息持久化
✅ 消费确认(ACK)
✅ 失败自动重试(Pending 机制)
✅ 支持多实例水平扩展
本文将手把手教你用 Redis Stream 实现一个生产级异步秒杀系统。
二、整体架构设计
用户请求
↓
[秒杀入口]
├── 1. 校验资格(登录、一人一单)
├── 2. Redis Lua 原子扣减预库存
└── 3. 发送秒杀任务到 Stream
↓
[订单消费者组](order_group)
├── worker-1 → 创建订单 & 扣 DB 库存
├── worker-2 → 创建订单 & 扣 DB 库存
└── ...(可水平扩展)
↓
[结果通知] → WebSocket / 轮询查询状态
🔑 核心思想 :HTTP 请求只做"快校验 + 入队",不等落库!
三、关键技术点
| 模块 | 技术方案 |
|---|---|
| 预库存控制 | Redis + Lua 脚本(防超卖) |
| 异步解耦 | Redis Stream(XADD) |
| 可靠消费 | 消费者组 + ACK + Pending 重试 |
| 幂等性 | 全局 requestId + DB 唯一索引 |
| 状态查询 | 订单状态表 + 轮询/推送 |
四、Spring Boot 完整实现
步骤 1:定义秒杀任务消息体
java
public class SeckillOrderTask {
private String requestId; // 幂等 ID
private Long userId;
private Long voucherId;
private Long orderId;
private long createTime = System.currentTimeMillis();
// getters/setters
}
步骤 2:秒杀入口 ------ 快进快出
java
@RestController
public class SeckillController {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private SeckillService seckillService;
private static final String STREAM_KEY = "seckill_order_stream";
@PostMapping("/seckill/{voucherId}")
public Result<String> seckill(@PathVariable Long voucherId,
@RequestHeader("userId") Long userId) {
// 1. 校验是否已领取(Redis Set)
if (seckillService.hasUserReceived(userId, voucherId)) {
return Result.fail("您已领取过该优惠券");
}
// 2. Lua 原子扣减预库存
if (!seckillService.tryDecreaseStock(voucherId)) {
return Result.fail("手慢了,已抢光!");
}
// 3. 生成唯一请求 ID(幂等)
String requestId = UUID.randomUUID().toString();
// 4. 构造任务并发送到 Stream
Map<String, String> message = Map.of(
"requestId", requestId,
"userId", userId.toString(),
"voucherId", voucherId.toString(),
"orderId", OrderIdGenerator.nextId().toString() // 雪花 ID
);
redisTemplate.opsForStream().add(
StreamRecords.newRecord()
.ofObject(message)
.withStreamKey(STREAM_KEY)
);
// 5. 立即返回!不等订单创建
return Result.success("提交成功,请稍后查看领取记录");
}
}
✅ 整个 HTTP 请求耗时 < 50ms!
步骤 3:创建消费者组(启动时初始化)
java
@Component
public class StreamInitializer {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String STREAM_KEY = "seckill_order_stream";
private static final String GROUP_NAME = "order_group";
@PostConstruct
public void createGroup() {
try {
redisTemplate.execute((RedisCallback<Void>) connection -> {
connection.streamCommands().xGroupCreate(
STREAM_KEY.getBytes(),
GROUP_NAME.getBytes(),
ReadOffset.from("0-0"),
true // MKSTREAM
);
return null;
});
log.info("消费者组 [{}] 创建成功", GROUP_NAME);
} catch (Exception e) {
if (!e.getMessage().contains("BUSYGROUP")) {
log.error("创建消费者组失败", e);
}
}
}
}
步骤 4:订单消费者 ------ 可靠处理 + ACK
java
@Component
public class SeckillOrderConsumer {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private OrderService orderService;
private static final String STREAM_KEY = "seckill_order_stream";
private static final String GROUP_NAME = "order_group";
private static final String CONSUMER_NAME = "order-worker-" +
InetAddress.getLocalHost().getHostAddress().replace(".", "_");
@PostConstruct
public void startConsumer() {
Thread consumer = new Thread(this::consumeLoop);
consumer.setDaemon(true);
consumer.setName("seckill-order-consumer");
consumer.start();
}
private void consumeLoop() {
while (!Thread.currentThread().isInterrupted()) {
try {
List<MapRecord<String, String, String>> records =
redisTemplate.opsForStream().read(
Consumer.from(GROUP_NAME, CONSUMER_NAME),
StreamReadOptions.empty().count(10).block(Duration.ofSeconds(2)),
StreamOffset.create(STREAM_KEY, ReadOffset.lastConsumed())
);
for (MapRecord<String, String, String> record : records) {
String messageId = record.getId().getValue();
Map<String, String> data = record.getValue();
try {
// 幂等校验:防止重复消费
if (orderService.existsByRequestId(data.get("requestId"))) {
ackMessage(messageId); // 已处理过,直接 ACK
continue;
}
// 创建订单(含 DB 扣库存)
orderService.createSeckillOrder(
Long.parseLong(data.get("orderId")),
Long.parseLong(data.get("userId")),
Long.parseLong(data.get("voucherId")),
data.get("requestId")
);
// 成功 → ACK
ackMessage(messageId);
} catch (Exception e) {
log.error("处理秒杀订单失败, messageId={}", messageId, e);
// 不 ACK,消息留在 Pending,后续可重试
}
}
if (records.isEmpty()) {
Thread.sleep(500);
}
} catch (Exception e) {
log.error("消费者异常", e);
try { Thread.sleep(1000); } catch (InterruptedException ie) { break; }
}
}
}
private void ackMessage(String... messageIds) {
redisTemplate.opsForStream().acknowledge(GROUP_NAME, STREAM_KEY, messageIds);
}
}
步骤 5:订单服务(含幂等与 DB 操作)
java
@Service
@Transactional
public class OrderService {
@Autowired
private OrderMapper orderMapper;
public boolean existsByRequestId(String requestId) {
return orderMapper.countByRequestId(requestId) > 0;
}
public void createSeckillOrder(Long orderId, Long userId, Long voucherId, String requestId) {
// 1. 再次校验库存(兜底)
if (!inventoryService.hasEnoughStock(voucherId)) {
throw new RuntimeException("DB 库存不足");
}
// 2. 创建订单(含唯一索引:request_id)
Order order = new Order();
order.setId(orderId);
order.setUserId(userId);
order.setVoucherId(voucherId);
order.setRequestId(requestId);
order.setStatus("SUCCESS");
orderMapper.insert(order);
// 3. 扣减 DB 库存
inventoryService.decreaseStockInDB(voucherId);
}
}
💡 数据库表建议:
sqlCREATE TABLE seckill_order ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, voucher_id BIGINT NOT NULL, request_id VARCHAR(64) UNIQUE NOT NULL, -- 幂等关键 status VARCHAR(20) DEFAULT 'SUCCESS' );
五、失败重试与监控(生产必备)
1️⃣ Pending 消息监控
java
@Scheduled(fixedRate = 30000)
public void checkPending() {
PendingMessagesSummary pending = redisTemplate.opsForStream()
.pending("seckill_order_stream", Consumer.from("order_group", "*"));
if (pending.getTotalPendingMessages() > 50) {
alertService.send("秒杀订单积压告警: " + pending.getTotalPendingMessages());
}
}
2️⃣ 手动重试超时消息
java
// 将 5 分钟未 ACK 的消息重新分配
List<ClaimRecords> claims = redisTemplate.opsForStream().claim(
"seckill_order_stream",
Consumer.from("order_group", "retry-worker"),
Duration.ofMinutes(5),
pendingMessageIds...
);
六、优势总结
| 维度 | 效果 |
|---|---|
| 性能 | QPS 提升 5~10 倍(HTTP 层无 DB 压力) |
| 可靠性 | 消费失败自动重试,订单不丢失 |
| 扩展性 | 消费者组支持多实例水平扩展 |
| 一致性 | Lua + 幂等 + DB 事务,防超卖 |
| 运维 | 仅依赖 Redis,无 Kafka/RabbitMQ 运维成本 |
七、结语
感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!