跳转到主内容
新派编程网 - 新潮编程理念,全新技术课堂!

MySQL如何应对秒杀场景下的数据库压力_Redis预扣减与异步写入队列

秒杀时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 里只是归档快照。中间那层异步管道,得经得起丢、经得起重、经得起查漏——否则一个补偿脚本写错,第二天运营就会拿着后台库存对不上来找你。

相关文章