云计算百科
云计算领域专业知识百科平台

秒杀优化-异步秒杀思路

一、前言:为什么同步秒杀扛不住高并发?

在传统秒杀实现中,用户点击“立即抢购”后,系统会同步完成以下操作:

  • 校验资格(登录、限购)
  • 检查库存
  • 扣减库存
  • 生成订单
  • 写入数据库
  • 返回结果
  • 看似合理,但在万人并发场景下:

    • 数据库连接池耗尽
    • Redis CPU 打满
    • 接口响应时间从 50ms 暴涨到 5s+
    • 大量请求超时或失败

    根本问题:所有操作都在 HTTP 请求线程中同步执行,系统成为“瓶颈漏斗”。

    本文将介绍异步秒杀架构,通过消息队列削峰填谷,轻松应对 10 倍流量冲击。


    二、异步秒杀核心思想:快进快出 + 异步处理

    ✅ 核心原则:
    前端请求只做“资格校验 + 入队”,后续操作全部异步化!

    架构对比:

    方式同步秒杀异步秒杀
    请求耗时 200~1000ms 20~50ms
    DB 压力 高(实时写) 低(异步消费)
    系统吞吐 低(受 DB 限制) 高(仅受 MQ 限制)
    用户体验 卡顿、超时 秒级响应

    三、异步秒杀整体架构图

    用户请求

    [网关层] → 限流、鉴权

    [秒杀服务]
    ├── 1. 校验用户资格(登录、一人一单)
    ├── 2. Redis 扣减预库存(Lua 原子)
    └── 3. 发送消息到 MQ(如 RabbitMQ/Kafka)

    [订单消费者]
    ├── 1. 再次校验(兜底)
    ├── 2. 创建订单 & 优惠券记录
    ├── 3. 扣减 DB 库存(最终一致)
    └── 4. 发送通知(短信/站内信)

    🔑 关键点:HTTP 请求在第 3 步就返回成功!


    四、详细实现步骤(Spring Boot + RabbitMQ)

    步骤 1:定义秒杀消息体

    public class SeckillMessage {
    private Long userId;
    private Long couponId;
    private String requestId; // 幂等 ID
    // getters/setters
    }


    步骤 2:秒杀入口(快进快出)

    @RestController
    public class SeckillController {

    @Autowired
    private SeckillService seckillService;

    @PostMapping("/seckill/{couponId}")
    public Result<String> seckill(@PathVariable Long couponId,
    @RequestHeader("userId") Long userId) {
    // 1. 快速校验(Redis 判断是否已领取)
    if (seckillService.hasReceived(userId, couponId)) {
    return Result.fail("您已领取过该优惠券");
    }

    // 2. Redis Lua 原子扣减预库存
    if (!seckillService.tryDecreaseStock(couponId)) {
    return Result.fail("手慢了,已抢光!");
    }

    // 3. 发送异步消息(核心!)
    String requestId = UUID.randomUUID().toString();
    SeckillMessage message = new SeckillMessage(userId, couponId, requestId);
    rabbitTemplate.convertAndSend("seckill.exchange", "seckill.route", message);

    // 4. 立即返回!不等订单创建
    return Result.success("提交成功,请稍后查看领取记录");
    }
    }

    ✅ 优势:整个 HTTP 请求 < 50ms,用户体验极佳!


    步骤 3:异步消费者(可靠处理)

    @Component
    @RabbitListener(queues = "seckill.queue")
    public class SeckillConsumer {

    @Autowired
    private CouponRecordService recordService;

    @RabbitHandler
    public void handleSeckillMessage(SeckillMessage message) {
    try {
    // 1. 幂等校验(防止重复消费)
    if (recordService.existsByRequestId(message.getRequestId())) {
    return;
    }

    // 2. 兜底校验(Redis 状态可能变化)
    if (recordService.hasReceived(message.getUserId(), message.getCouponId())) {
    return; // 已领取,跳过
    }

    // 3. 创建领取记录 & 扣减 DB 库存
    recordService.createCouponRecord(
    message.getRequestId(),
    message.getUserId(),
    message.getCouponId()
    );

    // 4. 发送通知(可选)
    notificationService.sendSuccessMsg(message.getUserId());

    } catch (Exception e) {
    // 记录日志,可配置死信队列重试
    log.error("秒杀异步处理失败", e);
    }
    }
    }


    五、关键技术点解析

    1️⃣ 预库存 vs 真实库存

    • Redis 预库存:用于快速拦截超卖(高性能)
    • DB 真实库存:用于持久化和对账(强一致)
    • 两者允许短暂不一致,通过对账 Job修复

    2️⃣ 幂等性保障

    • 消息携带 requestId(全局唯一)
    • 消费前检查是否已处理,避免重复发券

    3️⃣ 失败补偿机制

    • 消费失败 → 进入死信队列(DLQ)
    • 定时任务扫描 DLQ,人工或自动重试

    4️⃣ 用户体验优化

    • 前端提示:“提交成功,正在处理…”
    • 跳转到“我的优惠券”页,轮询状态(或 WebSocket 推送)

    六、异步秒杀的优势总结

    维度效果
    性能 QPS 提升 5~10 倍(从 1000 → 10000+)
    稳定性 DB 不再是瓶颈,系统更健壮
    扩展性 可水平扩展消费者实例
    容错性 消费失败可重试,不丢失请求
    资源利用率 高峰流量被“平滑”处理

    七、适用场景与注意事项

    ✅ 适合场景:

    • 优惠券秒杀
    • 限量商品抢购
    • 抽奖活动
    • 报名/预约系统

    ⚠️ 注意事项:

    • 不适用于强一致性场景(如支付)
    • 需要设计状态查询接口(用户需知道是否成功)
    • MQ 必须高可用(集群部署 + 持久化)
    • 监控消息积压(如 Prometheus + Grafana)

    八、结语

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

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 秒杀优化-异步秒杀思路
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!