秒杀卡在select ... for update是因为8000 qps集中争抢同一行库存,导致innodb锁队列内核态线程频繁唤醒挂起;需前端控速、网关限流、服务端队列、redis预扣减及库存分片等全链路流量收敛。

为什么秒杀一开就卡在 SELECT ... FOR UPDATE 上?
不是锁机制错了,是所有请求全挤在同一行、同一事务里排队。InnoDB 行锁再细,也扛不住 8000 QPS 同时抢 id = 1 这一行库存。你看到的 waiting for trx id、CPU 95%、超时率飙升,本质是锁队列在内核态疯狂唤醒/挂起线程,而不是 SQL 慢或索引没建好。
应用层削峰:别让请求直冲数据库
核心思路是把“瞬时洪峰”变成“可控流速”,关键不在加机器,而在控制进入数据库的节奏:
- 前端按钮置灰 + 人机校验(滑块/验证码)—— 把用户点击分散到 1~3 秒窗口,天然降低峰值 QPS 30%~60%
- 接入层(Nginx)限流:按 IP 或用户 ID 限速,例如
limit_req zone=seckill burst=20 nodelay,拦掉脚本刷量 - 服务端入口加队列:用 Redis List 或 Streams 接收请求,后台消费者以固定速率(如 500 QPS)拉取并批量处理
- 拒绝前置化:Redis 原子操作
DECRBY stock:1 1先扣减,返回负数直接拒单,不进 DB 事务
热点拆分:从“一行锁”变成“多行锁”
单纯加缓存或调参解决不了“物理位置固定”这个根因。必须打破单点更新:
- 初始化时把总库存 1000 拆成 10 份,写入 10 行:
stock_shard_1~stock_shard_10,每份 100 - 扣减时用哈希或随机选一个
shard_id,执行UPDATE stock_shard SET count = count - 1 WHERE id = ? AND count > 0 - 查总库存走
SUM(count),不依赖单行读取 - 注意:拆分后事务粒度变小,
SELECT ... FOR UPDATE锁的是不同行,冲突下降 90%+
容易被忽略的坑:连接池和事务生命周期
很多人加了队列但效果不明显,问题常出在应用层自身打抖:
-
HikariCP的maximumPoolSize别设成 1000 —— 实际 MySQL 能稳定并发的事务数可能只有 200,设太高反而引发Threads_running飙升、上下文切换爆炸 - 每个扣减事务必须极短:禁止在事务里调远程接口、发消息、做复杂计算;
SELECT ... FOR UPDATE后立刻UPDATE+COMMIT,锁持有时间控制在 5ms 内 - 避免长事务:
SHOW ENGINE INNODB STATUS里看到TRX HAS BEEN WAITING超过 1s,说明事务已拖慢整体吞吐,得查代码里有没有隐式开启事务却忘了提交
真正难的不是实现队列或拆分,而是让所有环节都服从“流量收敛”逻辑:前端控速、网关限流、服务排队、DB 只收稳流。任何一个环节放任自流,都会让优化前功尽弃。











