秒杀时MySQL崩在INSERT/UPDATE的根本原因是高并发争抢同一行锁导致卡死,应禁用带WHERE条件的库存SQL操作,改用Redis DECR预扣减+异步单线程写MySQL。

秒杀时 MySQL 直接崩在 INSERT 或 UPDATE 上怎么办
根本原因不是并发高,而是大量请求同时争抢同一行记录的行锁(比如商品库存字段),导致锁等待堆积、事务超时、连接池耗尽。MySQL 在这种场景下不是“慢”,是“卡死”。
实操建议:
- 立刻禁用所有对
stock字段的直接UPDATE ... WHERE id = ? AND stock > 0操作——它在高并发下会触发间隙锁+行锁组合,锁范围远超预期 - 把库存校验和扣减彻底移出 MySQL:前端不查库、下单不查库、扣减不靠 SQL 原子判断
- 如果必须保留 MySQL 作为最终库存源,只允许异步任务单线程写入,且写入语句必须是无条件
UPDATE(如UPDATE item SET stock = ? WHERE id = ?),避免 WHERE 条件引发锁竞争
Redis 预扣减为什么用 DECR 而不用 GETSET 或 Lua 脚本
DECR 是原子操作,失败即返回负数,能天然表达“扣完即止”。而 GETSET 需要先读再设,中间可能被其他客户端插入;Lua 虽然能封装逻辑,但一旦脚本里有 redis.call('GET') + 条件判断,就失去原子性保障——Redis 只保证脚本执行过程原子,不保证脚本内业务逻辑的并发安全。
实操建议:
- 初始化库存用
SET item:1001 100,不是INCRBY——避免冷启动时因 key 不存在导致初始值为 0 - 扣减统一走
DECR item:1001,拿到返回值后立刻判断是否 ≥ 0;若为 -1,说明已售罄,直接拒绝,不进队列 - 不要在 Lua 里做“如果大于 0 再 decr”,那是伪原子——因为
GET和DECR之间存在时间窗口
异步写入队列选 Kafka 还是 Redis List?
取决于你能否容忍“已预扣减但最终 MySQL 库存未更新”的数据不一致窗口。Kafka 提供持久化、重试、顺序保障,适合金融级一致性要求;Redis List 简单快,但进程崩溃或 Redis 故障时消息丢失,库存就永久少扣。
实操建议:
- 中小业务量(QPS LPUSH order_queue + 单消费者轮询
BRPOP,够用且零运维 - 一旦出现
BRPOP timeout或消费者 crash,必须有补偿机制:定时扫描 Redis 中已扣减但未写入 MySQL 的订单 ID(可通过单独 key 记录 pending 列表) - 别用
RPOPLPUSH做“可靠队列”——它不解决网络分区下的重复消费问题,反而让逻辑更难追踪
为什么不能跳过 Redis,直接用 MySQL 的 SELECT ... FOR UPDATE
因为 SELECT ... FOR UPDATE 是行级锁,但在秒杀场景中,所有请求都试图锁同一行(比如商品 ID=1001),结果就是所有事务排队等锁,TPS 从 5000 掉到 200,平均响应从 50ms 涨到 2s+,连接池迅速打满,错误率飙升。
实操建议:
-
SELECT ... FOR UPDATE只适用于低频、点对点、明确知道锁哪几行的场景(如转账),绝不用于热点资源争抢 - 哪怕加了索引,只要 WHERE 条件命中同一行,锁冲突就不可避免;索引优化在这里完全无效
- 如果 DBA 强推“我们调优过了”,请直接要压测报告:QPS 3000 下,
SELECT ... FOR UPDATE的平均锁等待时间和事务失败率
真正难的不是搭 Redis 或选队列,是把“库存”这个概念从数据库里摘出来,重新定义它的生命周期:Redis 里是实时可用数,MySQL 里只是归档快照。中间那层异步管道,得经得起丢、经得起重、经得起查漏——否则一个补偿脚本写错,第二天运营就会拿着后台库存对不上来找你。










