基于Redis的Stream结构作为消息队列,实现异步秒杀下单

一、前言:为什么用 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);
    }
}

💡 数据库表建议

sql 复制代码
CREATE 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 运维成本

七、结语

感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!

相关推荐
TDengine (老段)1 小时前
TDengine IDMP 数据可视化——状态时间线
大数据·数据库·ai·信息可视化·时序数据库·tdengine·涛思数据
DolphinDB智臾科技1 小时前
V3.00.5 & 2.00.18 更新!TPC-H 性能跃升,MPP 引擎来了…
大数据·数据库·时序数据库·dolphindb
xing-xing1 小时前
Spring Data项目
数据库·spring
七夜zippoe1 小时前
微服务架构下Spring Session与Redis分布式会话实战全解析
java·redis·maven·spring session·分布式会话
i220818 Faiz Ul11 小时前
计算机毕业设计|基于springboot + vue鲜花商城系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
期待のcode12 小时前
SpringBoot连接Redis
spring boot·redis·后端
Apple_羊先森12 小时前
ORACLE数据库巡检SQL脚本--22、检查碎片程度最高的业务表
数据库·sql·oracle