基于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 运维成本

七、结语

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

相关推荐
2401_871492851 分钟前
Vue.js监听器watch利用回调函数处理级联下拉框数据联动
jvm·数据库·python
志栋智能35 分钟前
超自动化安全:构建智能安全运营的核心引擎
大数据·运维·服务器·数据库·安全·自动化·产品运营
daixin88481 小时前
cursor无法正常使用gpt5.5等模型解决方案
java·redis·cursor
zhoutongsheng1 小时前
C#怎么实现Swagger文档 C#如何在ASP.NET Core中集成Swagger自动生成API文档【框架】
jvm·数据库·python
WinterKay2 小时前
【开源】我写了一个轻量级本地数据库浏览工具,支持 MySQL/Redis 只读查询
数据库·mysql·开源
小猿姐2 小时前
Redis Kubernetes Operator 实测:三个方案的真实差距
redis·容器·kubernetes
zxrhhm3 小时前
Oracle 索引完整指南
数据库·oracle
程序猿乐锅3 小时前
【Tilas|第三篇】多表SQL语句
数据库·经验分享·笔记·学习·mysql
Navicat中国4 小时前
使用 Navicat 导入向导导入 Excel 数据时,系统提示导入成功,表中也能看到数据,但行数统计显示为 0,这是什么原因?
数据库·excel·导入
gmaajt5 小时前
Golang怎么做国际化多语言_Golang i18n教程【核心】
jvm·数据库·python