Redis+Lua实现高并发秒杀系统架构设计 1. 秒杀系统架构设计核心挑战电商秒杀场景通常具备三高特征瞬时高并发10万级QPS、高资源竞争1000件商品被数万人抢购、高一致性要求不能超卖也不能少卖。我在参与某头部电商平台618大促秒杀系统改造时实测发现传统架构在5000QPS时就会出现服务雪崩。下面这个典型超卖案例揭示了问题本质// 存在严重并发问题的库存扣减实现 public String deductStock(String itemId) { Integer stock (Integer) redisTemplate.opsForValue().get(stock: itemId); if (stock 0) { redisTemplate.opsForValue().set(stock: itemId, stock - 1); return 抢购成功; } return 库存不足; }当两个线程同时执行这段代码时可能出现以下时序问题线程A和线程B同时查询库存都得到stock1线程A执行set(stock, 0)线程B继续执行set(stock, 0)实际卖出2件商品但库存只减少1造成超卖2. RedisLua原子性解决方案2.1 Lua脚本的原子性本质Redis采用单线程事件循环模型每个Lua脚本会作为一个整体执行。我在压测时验证过当脚本执行期间即使有10万个并发请求到达也必须等待当前脚本执行完成。这种机制比传统数据库事务更轻量但同样保证原子性。2.2 库存扣减Lua脚本精解以下是经过生产验证的增强版扣减脚本-- KEYS[1]: 库存key -- ARGV[1]: 扣减量 -- ARGV[2]: 当前时间戳用于限流 local stockKey KEYS[1] local decrement tonumber(ARGV[1]) local timestamp tonumber(ARGV[2]) -- 库存存在性检查防缓存击穿 if redis.call(exists, stockKey) 0 then return {-1, 商品不存在} -- 编码-1 end -- 获取当前库存 local stock tonumber(redis.call(get, stockKey)) local afterStock stock - decrement -- 库存检查 if stock 0 then return {-2, 已售罄} -- 编码-2 end if afterStock 0 then return {-3, 库存不足} -- 编码-3 end -- 执行扣减 redis.call(decrby, stockKey, decrement) -- 记录抢购流水防重复扣减 local orderKey order:..stockKey..:..timestamp if redis.call(setnx, orderKey, 1) 1 then redis.call(expire, orderKey, 86400) -- 24小时过期 return {afterStock, 抢购成功} else -- 回滚库存 redis.call(incrby, stockKey, decrement) return {-4, 请勿重复提交} -- 编码-4 end脚本关键增强点增加缓存存在性检查避免缓存击穿引入订单流水记录防止客户端重试导致重复扣减严格类型转换tonumber避免类型错误多状态码返回便于客户端处理不同场景2.3 Spring Boot集成实践在Spring中推荐这样管理Lua脚本Configuration public class LuaConfiguration { Bean public DefaultRedisScriptList stockDeductScript() { DefaultRedisScriptList script new DefaultRedisScript(); script.setLocation(new ClassPathResource(lua/stock_deduct.lua)); script.setResultType(List.class); // 注意返回类型匹配 return script; } } Service Slf4j public class SecKillService { Autowired private RedisTemplateString, String redisTemplate; Autowired private DefaultRedisScriptList stockDeductScript; public SecKillResult handleSecKill(String userId, String itemId) { ListString keys Collections.singletonList(stock: itemId); String timestamp String.valueOf(System.currentTimeMillis()); // 执行脚本 List result redisTemplate.execute( stockDeductScript, keys, 1, // 每次扣减1件 timestamp ); // 处理结果 Long code (Long) result.get(0); String msg (String) result.get(1); if (code 0) { // 异步创建订单 orderService.asyncCreateOrder(userId, itemId); return SecKillResult.success(code); } return SecKillResult.fail(code, msg); } }生产环境建议使用连接池配置Lettuce优于Jedis脚本预加载到RedisSCRIPT LOAD异常时降级到数据库乐观锁3. Redis Stream消息队列应用3.1 为什么选择Stream对比传统消息队列方案RabbitMQ性能瓶颈约1万QPSKafka延迟较高至少100msRedis List没有消费组概念Stream的独特优势百万级QPS处理能力消费组自动负载均衡消息回溯能力精确的Pending消息管理3.2 秒杀订单处理架构// 初始化Stream PostConstruct public void initStream() { if (!redisTemplate.hasKey(order_stream)) { redisTemplate.opsForStream().createGroup( order_stream, order_group ); } } // 生产者抢购成功后写消息 public void produceOrderEvent(Order order) { MapString, String fields new HashMap(); fields.put(userId, order.getUserId()); fields.put(itemId, order.getItemId()); fields.put(orderTime, order.getCreateTime().toString()); redisTemplate.opsForStream().add( order_stream, StreamRecords.string(fields).withId(RecordId.autoGenerate()) ); } // 消费者集群部署 Scheduled(fixedRate 100) public void consumeOrderEvent() { try { ListMapRecordString, String, String records redisTemplate.opsForStream().read( Consumer.from(order_group, consumer_1), StreamReadOptions.empty().count(10), StreamOffset.create(order_stream, ReadOffset.lastConsumed()) ); if (records ! null !records.isEmpty()) { for (MapRecordString, String, String record : records) { processOrder(record); redisTemplate.opsForStream().acknowledge( order_group, record ); } } } catch (Exception e) { log.error(订单处理异常, e); // 错误处理策略 // 1. 重试3次 // 2. 转入死信队列 // 3. 人工补偿 } }3.3 Stream管理技巧监控Pending消息XINFO GROUPS order_stream XPENDING order_stream order_group消息回溯处理// 重新处理未ACK的消息 ListMapRecordString, String, String failedRecords redisTemplate.opsForStream().range( order_stream, 1526567895632-0, 1526567895632-1 );性能调优参数maxlen设置Stream最大长度避免内存溢出MINID定期清理已处理消息4. 高并发优化组合拳4.1 多级缓存架构graph TD A[客户端] --|本地缓存| B(浏览器缓存) A --|CDN缓存| C[边缘节点] C --|Nginx缓存| D[应用服务器] D --|Redis集群| E[分布式缓存] E --|数据库缓存| F[MySQL BufferPool]实战技巧商品详情页静态化库存数据采用Redis本地缓存二级缓存缓存key设计业务前缀:版本号:ID如 stock:v1:10014.2 库存分段方案当库存量较大时如10万件可以采用分段库存提升并发// 初始化分段库存 public void initSegmentStock(String itemId, int total, int segments) { int base total / segments; for (int i 0; i segments; i) { int segmentStock (i segments - 1) ? base total % segments : base; redisTemplate.opsForValue().set( stock: itemId : i, segmentStock ); } } // 随机选择分段扣减 public boolean deductSegmentStock(String itemId, int segments) { Random random new Random(); for (int i 0; i 3; i) { // 重试3次 int segment random.nextInt(segments); Long result redisTemplate.execute( stockDeductScript, Collections.singletonList(stock: itemId : segment), 1 ); if (result 0) return true; } return false; }注意事项分段数建议为并发线程数的2-3倍定期合并库存碎片凌晨低峰期执行4.3 分布式限流方案-- 滑动窗口限流脚本 -- KEYS[1]: 限流key -- ARGV[1]: 窗口大小(秒) -- ARGV[2]: 最大请求数 local key KEYS[1] local window tonumber(ARGV[1]) local max tonumber(ARGV[2]) local now tonumber(redis.call(TIME)[1]) local clearBefore now - window -- 清除过期记录 redis.call(ZREMRANGEBYSCORE, key, 0, clearBefore) -- 获取当前计数 local current redis.call(ZCARD, key) if current max then return 0 else redis.call(ZADD, key, now, now..math.random()) redis.call(EXPIRE, key, window) return 1 end应用场景用户维度限流防止脚本抢购IP维度限流防止DDoS攻击商品维度限流热点商品保护5. 生产环境注意事项5.1 Redis高可用配置推荐集群方案哨兵模式适用于中小规模主从切换自动完成Cluster模式大规模场景数据分片存储双写方案主集群灾备集群通过DTS同步关键参数# 连接池配置 spring.redis.lettuce.pool.max-active200 spring.redis.lettuce.pool.max-wait1000ms spring.redis.lettuce.pool.max-idle50 # 超时设置 spring.redis.timeout3000ms spring.redis.lettuce.command-timeout2000ms5.2 压测指标参考我们在4核8G服务器上的测试数据方案QPS平均延迟99分位延迟超卖率数据库乐观锁1,200450ms800ms0%Redis单命令8,50025ms50ms0.3%RedisLua12,00015ms30ms0%分段库存Lua18,0008ms15ms0%5.3 常见故障处理案例1Stream消费积压现象XPENDING数量持续增长 解决方案增加消费者实例调整batchSize参数监控消费速度与生产速度比例案例2Redis内存溢出现象OOM错误日志 处理步骤设置maxmemory-policyallkeys-lru对大数据key进行分片增加内存或节点案例3网络闪断导致锁失效解决方案实现锁续约机制增加锁令牌校验设置合理的锁超时时间建议5-30秒