秒杀系统避坑指南:我是如何用Redis+Lua+Redisson搞定黑马点评优惠券模块的
高并发秒杀系统实战RedisLuaRedisson的技术组合拳去年参与一个电商促销系统开发时我遇到了职业生涯中最具挑战性的任务——设计一个能支撑百万级并发的秒杀模块。当第一个压力测试结果出来时系统在3000QPS下直接崩溃超卖问题让库存变成了负数这让我意识到传统架构在高并发场景下的无力。1. 秒杀系统的核心挑战秒杀场景本质上是一场资源争夺战当10万用户同时抢购100件商品时系统需要像交通警察一样精确地指挥流量。我们遇到的典型问题包括库存超卖多个请求同时判断库存充足导致实际售出数量超过库存数据库压力每秒数万次查询直接击穿缓存MySQL连接池耗尽重复下单用户快速点击导致生成多个订单系统雪崩某个服务崩溃引发连锁反应关键指标对比问题类型传统方案缺陷理想解决方案特性库存一致数据库行锁导致性能瓶颈内存级操作原子性保证并发控制应用层锁无法跨节点分布式锁自动续期流量整形直接打到数据库多级缓冲异步化处理2. 技术选型与架构设计经过多次压力测试和方案对比我们最终确定了核心架构// 伪代码展示整体流程 public Result handleSeckill(Long voucherId) { // 1. 执行Lua脚本进行库存扣减和资格校验 Long result redisTemplate.execute(script, keys, voucherId, userId); // 2. 获取资格后进入异步处理流程 if (result 0) { VoucherOrder order buildOrder(voucherId); mqProducer.send(order); // 进入消息队列 return Result.ok(排队中); } else { return Result.fail(result 1 ? 库存不足 : 请勿重复下单); } }2.1 Redis Lua脚本的原子性保障Lua脚本在Redis中具有原子执行特性我们用它实现了库存扣减和重复校验的复合操作-- seckill.lua local stockKey seckill:stock:..ARGV[1] local orderKey seckill:order:..ARGV[1] -- 检查库存 if tonumber(redis.call(GET, stockKey)) 0 then return 1 end -- 检查是否已下单 if redis.call(SISMEMBER, orderKey, ARGV[2]) 1 then return 2 end -- 执行扣减 redis.call(DECR, stockKey) redis.call(SADD, orderKey, ARGV[2]) return 0这个脚本解决了两个关键问题将多次网络往返变为单次原子操作避免客户端解析中间状态导致的逻辑漏洞2.2 Redisson分布式锁实践对于一人一单的业务限制我们采用Redisson的可重入锁RLock lock redissonClient.getLock(order:lock: userId); try { // 尝试获取锁等待时间5秒自动释放时间30秒 if (lock.tryLock(5, 30, TimeUnit.SECONDS)) { // 执行订单创建 return createVoucherOrder(voucherId); } } finally { // 只在持有锁的线程中释放 if (lock.isHeldByCurrentThread()) { lock.unlock(); } }锁特性对比锁类型实现原理优点缺点数据库行锁SELECT FOR UPDATE实现简单性能差死锁风险Redis SETNX字符串操作性能较好需自行处理续期、释放RedissonWatchdog机制自动续期可重入依赖Redis3. 性能优化关键点3.1 库存预热与分段缓存我们将库存数据拆分为多个段来缓解热点key问题// 初始化时将库存分散到多个slot for (int i 0; i slotCount; i) { redisTemplate.opsForValue().set( stock:slot: voucherId : i, initialStock / slotCount ); }3.2 异步订单处理采用多级缓冲架构减轻数据库压力Lua脚本快速过滤无效请求Redis Set记录已处理用户消息队列削峰填谷数据库最终持久化# 伪代码订单消费者 def consume_order(): while True: order queue.take() try: # 数据库操作 save_to_db(order) # 更新Redis状态 redis.delete(fprocessing:{order.id}) except Exception as e: # 失败重试逻辑 redis.incr(fretry:{order.id}) if get_retry_count(order.id) 3: queue.put(order)4. 监控与降级策略任何高并发系统都需要完善的应急方案实时监控看板Redis内存/CPU使用率订单队列积压量数据库QPS/TPS熔断降级策略当库存低于5%时关闭详情页查询队列积压超过阈值时返回活动太火爆提示数据库压力过大时启用本地缓存模式压测数据对比优化阶段QPS平均响应时间错误率初始版本1,200450ms12%引入Lua脚本8,000120ms0.5%全链路优化后35,00065ms0.01%5. 经验总结与踩坑记录在实际落地过程中有几个容易忽视的细节Redis连接池配置高并发下需要调整maxTotal和maxIdle参数我们曾因连接不足导致请求堆积Lua脚本复杂度避免在脚本中执行耗时操作我们的第一个版本因为包含日志记录导致性能下降40%锁粒度控制过细的锁粒度会增加系统复杂性过粗则影响并发度库存回补机制支付超时的订单需要及时释放库存我们设计了一个定时任务扫描超时订单// 库存回补示例 Scheduled(fixedRate 60000) public void restoreStock() { ListTimeoutOrder orders queryTimeoutOrders(); orders.forEach(order - { redisTemplate.opsForValue().increment( seckill:stock: order.getVoucherId() ); removeFromOrderSet(order); }); }这个项目让我深刻体会到高并发系统设计没有银弹需要根据具体业务特点不断调整优化。现在回看那些压测不通过的夜晚正是这些挑战让解决方案变得更加健壮可靠。