孤枝玉瘦 發表於 2025-11-28 09:56:59

Redis Stream秒杀系统实现

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">📚 案例背景</a></li><li><a href="#_label1">🏗️ 系统架构图</a></li><li><a href="#_label2">📝 详细步骤说明</a></li><ul class="second_class_ul"><li><a href="#_lab2_2_0">步骤1:用户点击秒杀按钮</a></li><li><a href="#_lab2_2_1">📜 Lua脚本详解 (SECKILL_SCRIPT)</a></li></ul><li><a href="#_label3">🎯 实际场景演示</a></li><ul class="second_class_ul"><li><a href="#_lab2_3_2">场景1:用户A成功秒杀</a></li><li><a href="#_lab2_3_3">场景2:用户B重复秒杀</a></li><li><a href="#_lab2_3_4">场景3:第1001个用户秒杀</a></li></ul><li><a href="#_label4">🔄 异步订单处理流程</a></li><ul class="second_class_ul"><li><a href="#_lab2_4_5">正常处理流程</a></li><li><a href="#_lab2_4_6">异常处理流程</a></li></ul><li><a href="#_label5">🎪 实战场景模拟</a></li><ul class="second_class_ul"><li><a href="#_lab2_5_7">模拟10万并发秒杀</a></li></ul><li><a href="#_label6">🔧 Redis数据状态变化</a></li><ul class="second_class_ul"><li><a href="#_lab2_6_8">秒杀开始前</a></li><li><a href="#_lab2_6_9">秒杀过程中</a></li><li><a href="#_lab2_6_10">秒杀结束后</a></li></ul><li><a href="#_label7">💡 核心优势总结</a></li><ul class="second_class_ul"></ul><li><a href="#_label8">🚀 扩展思考</a></li><ul class="second_class_ul"></ul></ul></div><p class="maodian"><a name="_label0"></a></p><h2>📚 案例背景</h2>
<p><strong>双11秒杀活动</strong>:某电商平台推出1000台特价iPhone,10万用户同时抢购。</p>
<p class="maodian"><a name="_label1"></a></p><h2>🏗️ 系统架构图</h2>
<p>用户请求 &rarr; Lua脚本校验 &rarr; Redis Stream队列 &rarr; 异步处理 &rarr; 数据库</p>
<p class="maodian"><a name="_label2"></a></p><h2>📝 详细步骤说明</h2>
<p class="maodian"><a name="_lab2_2_0"></a></p><h3>步骤1:用户点击秒杀按钮</h3>
<div class="jb51code"><pre class="brush:java;">// 前端调用
public Result seckillVoucher(Long voucherId) {
    // 生成唯一订单ID: 2024110500012345
    long orderId = redisIdWorker.nextId("order");
    // 执行Lua脚本进行原子操作
    Long result = stringRedisTemplate.execute(
            SECKILL_SCRIPT,
            Collections.emptyList(),
            "1001",// 优惠券ID (iPhone特价券)
            "12345", // 用户ID
            "2024110500012345" // 订单ID
    );
    // 立即返回结果给用户
    if(result != 0){
      return Result.fail(result==1 ? "库存不足":"不能重复下单");
    }
    return Result.ok(2024110500012345L);
}</pre></div>
<p class="maodian"><a name="_lab2_2_1"></a></p><h3>📜 Lua脚本详解 (SECKILL_SCRIPT)</h3>
<div class="jb51code"><pre class="brush:java;">-- 参数:优惠券ID、用户ID、订单ID
local voucherId = ARGV
local userId = ARGV
local orderId = ARGV
-- 构建Redis Key
local stockKey = 'seckill:stock:' .. voucherId
local orderKey = 'seckill:order:' .. voucherId
-- 1. 判断库存是否充足
local stock = redis.call('get', stockKey)
if tonumber(stock) &lt;= 0 then
    return 1-- 库存不足
end
-- 2. 判断用户是否已经下单 (set集合)
if redis.call('sismember', orderKey, userId) == 1 then
    return 2-- 不能重复下单
end
-- 3. 扣减库存
redis.call('decr', stockKey)
-- 4. 记录用户购买记录
redis.call('sadd', orderKey, userId)
-- 5. 发送消息到Stream队列
redis.call('xadd', 'stream.order', '*',
    'voucherId', voucherId,
    'userId', userId,
    'orderId', orderId
)
return 0-- 成功</pre></div>
<p class="maodian"><a name="_label3"></a></p><h2>🎯 实际场景演示</h2>
<p class="maodian"><a name="_lab2_3_2"></a></p><h3>场景1:用户A成功秒杀</h3>
<div class="jb51code"><pre class="brush:plain;">时间线:
10:00:00.000 - 用户A点击秒杀按钮
10:00:00.050 - Lua脚本执行:
                ✓ 库存检查: 库存1000 &gt; 0
                ✓ 重复检查: 用户A未购买
                ✓ 库存-1 → 999
                ✓ 记录用户A到已购集合
                ✓ 发送消息到stream.order
10:00:00.100 - 返回订单ID: 2024110500012345
10:00:00.150 - 异步线程处理订单入库
10:00:01.000 - 订单创建完成</pre></div>
<p class="maodian"><a name="_lab2_3_3"></a></p><h3>场景2:用户B重复秒杀</h3>
<div class="jb51code"><pre class="brush:plain;">时间线:
10:00:00.200 - 用户B点击秒杀按钮
10:00:00.250 - Lua脚本执行:
                ✓ 库存检查: 库存999 &gt; 0
                ✗ 重复检查: 用户B已在已购集合中
                → 返回2 (不能重复下单)
10:00:00.300 - 前端显示:"不能重复下单"</pre></div>
<p class="maodian"><a name="_lab2_3_4"></a></p><h3>场景3:第1001个用户秒杀</h3>
<div class="jb51code"><pre class="brush:plain;">时间线:
10:00:05.000 - 用户Z点击秒杀按钮
10:00:05.050 - Lua脚本执行:
                ✗ 库存检查: 库存0 &lt;= 0
                → 返回1 (库存不足)
10:00:05.100 - 前端显示:"库存不足"</pre></div>
<p class="maodian"><a name="_label4"></a></p><h2>🔄 异步订单处理流程</h2>
<p class="maodian"><a name="_lab2_4_5"></a></p><h3>正常处理流程</h3>
<div class="jb51code"><pre class="brush:java;">// VoucherOrderHandler - 订单处理线程
while(true){
    // 从消息队列读取订单
    List&lt;MapRecord&lt;String, Object, Object&gt;&gt; list = stringRedisTemplate
      .opsForStream()
      .read(
            Consumer.from("g1", "c1"),// 消费者组g1,消费者c1
            StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
            StreamOffset.create("stream.order", ReadOffset.lastConsumed()) // 读取新消息
      );
    if(!list.isEmpty()){
      MapRecord&lt;String, Object, Object&gt; record = list.get(0);
      Map&lt;Object, Object&gt; values = record.getValue();
      // 构建订单对象
      VoucherOrder order = new VoucherOrder();
      order.setId(Long.parseLong((String)values.get("orderId")));
      order.setUserId(Long.parseLong((String)values.get("userId")));
      order.setVoucherId(Long.parseLong((String)values.get("voucherId")));
      // 保存到数据库
      voucherOrderService.save(order);
      // 确认消息已处理
      stringRedisTemplate.opsForStream()
            .acknowledge("stream.order", "g1", record.getId());
      log.info("订单处理成功: {}", order.getId());
    }
}</pre></div>
<p class="maodian"><a name="_lab2_4_6"></a></p><h3>异常处理流程</h3>
<div class="jb51code"><pre class="brush:java;">private void handlePendingList() {
    while(true){
      try {
            // 读取未确认的消息 (处理异常情况)
            List&lt;MapRecord&lt;String, Object, Object&gt;&gt; list = stringRedisTemplate
                .opsForStream()
                .read(
                  Consumer.from("g1", "c1"),
                  StreamReadOptions.empty().count(1),
                  StreamOffset.create("stream.order", ReadOffset.from("0")) // 从pending-list读取
                );
            if(list.isEmpty()) break; // 没有异常消息
            MapRecord&lt;String, Object, Object&gt; record = list.get(0);
            // 重新处理订单...
            createVoucherOrder(voucherOrder);
            // 确认消息
            stringRedisTemplate.opsForStream()
                .acknowledge("stream.order", "g1", record.getId());
      } catch (Exception e) {
            // 处理失败,等待后重试
            Thread.sleep(20);
      }
    }
}</pre></div>
<p class="maodian"><a name="_label5"></a></p><h2>🎪 实战场景模拟</h2>
<p class="maodian"><a name="_lab2_5_7"></a></p><h3>模拟10万并发秒杀</h3>
<div class="jb51code"><pre class="brush:java;">// 模拟10万用户同时秒杀
for(int i = 1; i &lt;= 100000; i++){
    new Thread(() -&gt; {
      Result result = seckillVoucher(1001L); // iPhone秒杀
      if(result.getCode() == 200){
            System.out.println("用户" + Thread.currentThread().getId() + "秒杀成功");
      } else {
            System.out.println("用户" + Thread.currentThread().getId() + "秒杀失败");
      }
    }).start();
}</pre></div>
<p><strong>执行结果:</strong></p>
<blockquote><p>用户12345: 秒杀成功<br />用户12346: 秒杀成功<br />...<br />用户11344: 秒杀成功 &nbsp;<br />用户11345: 库存不足<br />用户11346: 库存不足<br />...<br />用户100000: 库存不足</p></blockquote>
<p class="maodian"><a name="_label6"></a></p><h2>🔧 Redis数据状态变化</h2>
<p class="maodian"><a name="_lab2_6_8"></a></p><h3>秒杀开始前</h3>
<div class="jb51code"><pre class="brush:java;">seckill:stock:1001: "1000"   # 库存1000
seckill:order:1001: []         # 空集合
stream.order: []               # 空消息队列</pre></div>
<p class="maodian"><a name="_lab2_6_9"></a></p><h3>秒杀过程中</h3>
<div class="jb51code"><pre class="brush:java;">seckill:stock:1001: "500"      # 库存剩余500
seckill:order:1001: ["12345", "12346", ...]# 500个用户ID
stream.order: [消息1, 消息2, ...]            # 500条待处理消息</pre></div>
<p class="maodian"><a name="_lab2_6_10"></a></p><h3>秒杀结束后</h3>
<div class="jb51code"><pre class="brush:java;">seckill:stock:1001: "0"      # 库存为0
seckill:order:1001:           # 1000个购买用户
stream.order: []               # 消息全部处理完成</pre></div>
<p class="maodian"><a name="_label7"></a></p><h2>💡 核心优势总结</h2>
<ul><li><strong>高性能</strong>: Lua脚本原子操作,毫秒级响应</li><li><strong>高并发</strong>: 异步处理,支持10万+ QPS</li><li><strong>数据一致性</strong>: 库存不会超卖</li><li><strong>可靠性</strong>: 消息队列确保订单不丢失</li><li><strong>用户体验</strong>: 立即返回结果,无需等待</li></ul>
<p class="maodian"><a name="_label8"></a></p><h2>🚀 扩展思考</h2>
<p><strong>问题</strong>: 如果异步处理订单时数据库挂了怎么办?<br /><strong>答案</strong>: 消息会留在pending-list中,等数据库恢复后自动重试。</p>
<p><strong>问题</strong>: 如何防止恶意用户刷 单?<br /><strong>答案</strong>: 在Lua脚本中加入频率限制,如:<code>redis.call(&#39;incr&#39;, &#39;user:limit:&#39;..userId)</code></p>
頁: [1]
查看完整版本: Redis Stream秒杀系统实现