07 如何设计一个支持10万QPS的库存扣减系统
假设您要设计一个库存扣减系统,要求支持秒杀场景下的高并发扣减(10万QPS),且保证不超卖。请对比Redis Lua脚本、分布式锁+数据库扣减、以及Redis+MQ异步扣减三种方案的优劣。您会如何选择?请结合您简历中的高并发经验说明
方案
优势
劣势
适用场景
Redis Lua脚本
原子性保证,无竞态条件;毫秒级响应(内存操作)
集群热key压力大,数据持久化风险(宕机丢数据),无异步流程扩展性
秒杀初期(库存量小,如1000件以内)
分布式锁+数据库扣减
强一致性(ACID),无需额外组件
锁竞争严重(如CAS失败重试),数据库写入瓶颈(QPS难以突破2万),死锁风险高
低频高价值商品(如奢侈品,QPS<1万)
Redis+MQ异步扣减
削峰填谷(支撑10万QPS+),业务解耦,可扩展性强
最终一致性(短暂超卖风险),架构复杂度高,需处理消息堆积
大规模秒杀(如双11,库存百万级)
推荐redis+MQ异步方案 架构图:
1 2 3 [用户请求] → [API网关] → [Redis库存预扣减] → [MQ] → [DB最终扣减+订单创建] ↑ ↓ (返回预扣结果) ↓ (异步保证最终一致性) └─[响应前端] [库存回滚机制] [监控报警]
实现步骤:
1.库存预热 1 2 3 4 for (int i = 0 ; i < 100 ; i++) { redisTemplate.opsForValue().set("stock:sku_123:bucket_" + i, 100 ); }
优势:分散热key压力(参考您简历中Redis集群经验)。 路由算法:bucketId = userId.hashCode() % 100(相同用户始终访问同一桶,避免重复扣减)
2.Lua脚本原子扣减 1 2 3 4 5 6 7 8 -- KEYS=bucket_key, ARGV=扣减数量 local stock = tonumber(redis.call('get', KEYS)) if stock >= tonumber(ARGV) then redis.call('decrby', KEYS, ARGV) return 1 -- 成功 else return 0 -- 库存不足 end
性能优化:每个分桶支撑约1000 QPS,100个分桶总QPS可达10万
3.MQ异步写库 1 2 3 4 5 6 7 8 { "messageId" : "uuid" , "skuId" : "123" , "userId" : "456" , "bucketId" : 78 , "preReduceAmount" : 1 }
1 2 3 UPDATE stock SET available = available - 1 WHERE sku_id = 123 AND available >= 1 ;
异常处理: DB扣减失败:通过messageId幂等重试,3次失败后触发Redis库存回补(INCRBY)。 消息堆积:动态扩容Consumer实例(参考您简历中的水平分库经验)。
4.一致性机制保障 定时对账:
1 2 3 SELECT SUM (pre_reduced) FROM mq_message WHERE status = 'SUCCESS' ; SELECT (total_stock - available) FROM stock WHERE sku_id = 123 ;
异常补偿: 若Redis预扣总量 > DB实际扣减量:自动回补差额(如网络分区导致MQ消息丢失)。 若Redis预扣总量 < DB实际扣减量:触发人工审核(消息重复消费,可能发生超卖)。
接着探讨下超卖的根本原因
1.MQ消息重复消费 消费者处理消息后redis打标失败,导致消息被重复消费 解决办法,数据库一致性校验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Transactional public void handleMessage (Message msg) { try { jdbcTemplate.update( "INSERT INTO mq_processed (id, status) VALUES (?, 'PROCESSING')" , msg.getId() ); } catch (DuplicateKeyException e) { return ; } reduceDBStock(msg.getSkuId(), msg.getAmount()); jdbcTemplate.update( "UPDATE mq_processed SET status='SUCCESS' WHERE id=?" , msg.getId() ); }
2.异步消息丢失 Redis扣减成功后,MQ消息未成功发送(如网络闪断)。此时Redis库存已减,但DB未扣减,导致后续请求继续扣减Redis(实际DB库存已不足) 解决办法,本地事务表+定时补偿
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @Transactional(rollbackFor = Exception.class) public void preReduceStock (String skuId, int amount) { boolean success = redisLuaReduce(skuId, amount); if (!success) throw new RuntimeException ("库存不足" ); jdbcTemplate.update( "INSERT INTO local_message (id, sku_id, amount, status) VALUES (?, ?, ?, 'UNSENT')" , UUID.randomUUID().toString(), skuId, amount ); } @Scheduled(fixedDelay = 5000) public void compensateMessages () { List<Message> messages = jdbcTemplate.query( "SELECT * FROM local_message WHERE status='UNSENT'" , new MessageRowMapper () ); messages.forEach(msg -> { if (sendToMQ(msg)) { jdbcTemplate.update( "UPDATE local_message SET status='SENT' WHERE id=?" , msg.getId() ); } }); }
3.对账延迟及补偿失效 定时对账任务每小时运行一次,但在此期间若Redis预扣量 < DB实际量(如消息重复消费导致DB多扣),系统未能及时回补Redis库存,导致后续请求继续扣减已超卖的库存 解决方法,实时对账+动态限流
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public void preReduceStock (String skuId, int amount) { boolean success = redisLuaReduce(skuId, amount); if (!success) throw new RuntimeException ("库存不足" ); int dbStock = getDBStock(skuId); if (dbStock < 0 ) { redis.incrBy(skuId, amount); throw new RuntimeException ("库存超卖,已回补" ); } }
方案加固 1.链路追踪 在消息体中增加全链路追踪ID,记录Redis扣减、MQ发送、DB处理的各阶段状态。
1 2 3 4 5 6 7 8 9 10 { "messageId" : "msg_123" , "traceId" : "trace_456" , "skuId" : "789" , "amount" : 1 , "steps" : [ { "stage" : "redis_pre_reduce" , "time" : "2025-02-19T20:00:00Z" } , { "stage" : "mq_sent" , "time" : "2025-02-19T20:00:01Z" } ] }
2.多级库存水位
展示层库存:前端展示的库存 = Redis预扣库存 - 已发送MQ但未完成DB扣减的库存。
DB真实库存:实际可售库存1 2 3 4 5 public int getDisplayStock (String skuId) { int redisStock = redis.get(skuId); int mqPending = mqService.getPendingCount(skuId); return Math.max(redisStock - mqPending, 0 ); }
3.快速熔断 当DB实际库存 < 0时,自动熔断,拒绝所有扣减请求。 当MQ堆积超过阈值(如10万条),触发降级,直接写数据库(借助Sentinel限流至1万QPS)
4.监控告警 Drift = |redis与扣减库存量 -(初始库存量-DB最终库存量)| 阈值:Drift > 0时触发告警,人工介入修复
总结:即使采用Redis+MQ方案,超卖风险仍存在于消息可靠性、幂等性、对账延迟等环节。通过本地事务表、实时对账、 多级库存水位等组合策略,可将超卖概率降至0.001%以下,满足绝大多数电商场景需求