# 对一个电商平台来说突然的秒杀场景,如何进行库存扣减。 ## 一、秒杀库存扣减的核心挑战 秒杀的特点是: 1. **高并发** 同一时间有几十万、上百万请求同时抢一个库存。 2. **库存极少** 比如 1000 件。 3. **不能超卖** 超卖导致重大售后问题。 4. **业务链路必须极短** 扣库存必须快(毫秒级)。 5. **数据库承压不能太大** 秒杀流量不能把主站压垮。 所以目标是: * 扣减要 **原子性** * 避免数据库被打爆 * 不允许超卖 * 能回溯成功/失败 * 撤回和放库存也要安全 --- ## 二、常见的扣库存方式(由弱到强) ### 方式 1:直接 MySQL 扣减(悲观锁 / 乐观锁) ``` update product set stock = stock - 1 where id = 1 and stock > 0; ``` 优点: * 简单 * 强一致性 缺点: * QPS 极低(几百) * 秒杀瞬间直接把库打挂 * 不适合真实秒杀场景 → **不能用于真正秒杀,只能用于普通下单。** --- ## 三、真正用于秒杀的库存扣减方案(大厂通用) 秒杀场景一般采用 **“削峰 + 前置判断 + 内存扣减 + 异步确认”** 的架构。 下面给最佳方案。 --- ## 四、最佳方案:Redis + Lua 原子扣减(强烈建议) Redis 的特性: * 单线程 → 天然原子性 * 内存 → 高吞吐 * 支持 Lua → 业务逻辑原子执行 #### 扣库存 Lua 脚本示例(面试官很爱看) ```lua local stock = tonumber(redis.call("GET", KEYS[1])) if stock <= 0 then return -1 -- 库存不足 end redis.call("DECR", KEYS[1]) return 1 -- 扣减成功 ``` Go/Java 直接执行 Lua,每秒可承受 10~20 万 QPS。 #### 优点 * 扣减是原子的,不会超卖 * Redis 是内存级别,性能爆炸强 * 所有请求不打数据库 #### 缺点 * Redis 异常会影响库存一致性,需要持久化机制(后面讲) --- ## 五、核心思想:库存“前置”到 Redis,数据库做最终确认 流程图: ``` 用户请求 → 接入层限流 → Redis 预扣库存(Lua 原子扣减) → 写入秒杀订单消息队列 → 消费 MQ,在数据库最终落地订单、扣库存 ``` --- ## 六、为什么要加消息队列(MQ)? 如果 Redis 扣库存成功,用户不会立即下订单,而是将消息写入 Kafka / RocketMQ: * 订单写数据库是慢操作 * 秒杀并发巨大 * 直接写数据库必挂 * MQ 能承受百万级写入做削峰 最终由订单服务异步消费消息: * 检查用户资格 * 建订单 * 持久化最终库存(这里使用 MySQL 扣减) #### 这样设计有三个目的: 1. **削峰** 2. **数据库只处理异步订单,压力小** 3. **确保订单按序落地,可回查可补偿** --- ## 七、解决 Redis 和 MySQL 的一致性问题 当 Redis 扣库存成功,但数据库订单没写成功怎么办? 解决方案: #### 方式 1:Redis 中设置“占位的库存锁” 扣库存时记录: ``` stock_reserved = +1 ``` 订单失败时,补偿库存: ``` stock += 1 ``` #### 方式 2:订单写失败发送补偿消息 MQ 有死信队列,订单失败会触发“归还库存消息”。 #### 方式 3:定时校准库存 每天使用定时任务扫描数据库与 Redis 差异,修复异常。 --- ## 八、为了防止 Redis 仍然成为热点,需要进一步优化 ### 技术 1:接入层限流(Nginx、Envoy、Gateway) 根据商品库存量,比如 1000 件,整个系统最多放行 10 倍请求(1 万),剩下都在网关拒绝。 ### 技术 2:分布式令牌桶 + 秒杀资格检查 不让所有请求打到库存服务。 ### 技术 3:本地内存标记已售空 服务收到库存为 0,即刻标记 sold_out = true,直接拒绝后续请求 --- ## 九、总结 秒杀场景下库存扣减不能直接打数据库,需要使用 **“Redis + Lua 扣库存 + MQ 下单 + MySQL 最终落库”** 的异步架构。 > 具体流程: > 1)流量通过网关限流、削峰 > 2)Redis Lua 原子扣减库存,保证不超卖 > 3)扣减成功的请求写入消息队列 > 4)订单服务异步消费 MQ,写 MySQL,完成真正的扣库存 > 5)通过补偿消息、定时任务确保 Redis 和数据库一致性 > > 这样可以轻松支撑几十万到上百万 QPS,同时保证库存严格正确。