select for update在秒杀中拦不住排队,根本原因是所有请求争抢同一行x锁,即使命中主键索引也需串行获取;必须结合redis预扣减、分片更新与read committed隔离级别协同优化。

为什么SELECT ... FOR UPDATE在秒杀里根本拦不住排队
秒杀卡住不是因为没走索引,而是所有请求都在抢同一行的 X 锁。哪怕你加了主键索引、WHERE id = 123能精准命中,InnoDB 也必须让事务串行获取这把锁——聚簇索引让物理位置固定,没法靠数据分布分散压力。
常见错误现象包括:SHOW ENGINE INNODB STATUS里反复出现 waiting for trx id;information_schema.INNODB_TRX中大量事务状态为 RUNNING 但 trx_state = 'LOCK WAIT';CPU 跑满,磁盘 IO 却很低。
- 别急着调
innodb_lock_wait_timeout:50 秒超时只是掩盖问题,业务通常等不了 3 秒 - 别迷信“加索引就能解决”:如果 WHERE 条件本身只命中一行,索引再好也拦不住争抢
- 先用
EXPLAIN确认:该语句的type必须是const或eq_ref,且rows ≤ 1;否则实际锁了多行甚至全表
UPDATE ... WHERE 拆成多行分片的前提条件
把单行库存拆成 10 行(如 shard_id 0~9),能降 80%+ 锁冲突,但漏掉任一前提,效果归零甚至更糟。
-
WHERE条件必须命中联合主键:例如UPDATE goods_stock SET stock = stock - 1 WHERE product_id = 123 AND shard_id = 7,EXPLAIN中key字段得显示PRIMARY,rows ≤ 1 - 事务只包这一条
UPDATE+ROW_COUNT()判断:里面不能有日志、HTTP 调用、SLEEP(),否则锁滞留时间指数增长 - 隔离级别必须设为
READ COMMITTED:它禁用间隙锁,避免shard_id查询时意外锁住相邻槽位;全局执行SET GLOBAL tx_isolation = 'READ-COMMITTED',并确认binlog_format = 'ROW'
SKIP LOCKED 和 NOWAIT 不是配置项,是写法问题
SKIP LOCKED 和 NOWAIT 是 MySQL 8.0.1+ 原生支持的语法选项,不是配置项,无需改参数。真正要做的,是写对 SQL、用对场景、避开常见误用。
-
SKIP LOCKED只在锁定读(FOR UPDATE或FOR SHARE)中生效,且必须作用于可命中索引的WHERE条件;否则退化为全表扫描加锁 - 必须有明确主键或唯一索引条件,例如
WHERE sku_id = ?,不能是WHERE status = 'pending'(无索引时会锁整张表的扫描路径) -
NOWAIT报错后必须捕获ERROR 3572 (HY000): Do not wait for lock.,不能当成普通超时错误(1205)或死锁(1213)处理 - ORM 框架(如 MyBatis、Hibernate)可能包装原生错误码,需确认是否透传 3572
Redis 预扣减不是可选,是第一道闸口
真实压测中,80% 的请求是库存已空还狂点,这些根本没必要进 MySQL。用 Redis 原子操作提前拦截,MySQL 只处理真正需要落库的请求。
- Key 设计按
goods_id % 10分片,如goods:123:stock:0到goods:123:stock:9,分散单 key 压力 - 扣减逻辑用
DECRBY goods:123:stock:0 1,返回值 ≥ 0 才放行;否则直接拒绝,不走 DB - 异步落库节奏要可控:用 Redis Streams 或带速率限制的消费者(如每秒最多 500 条),避免 DB 写入突增
- MySQL 最终校验仍需保留:
UPDATE goods SET stock = stock - ? WHERE id = 123 AND stock >= ?,防止 Redis 和 DB 之间因网络分区导致超卖
分段更新和 Redis 预扣减都依赖一个容易被忽略的前提:所有分片操作必须原子、幂等、且不可被事务包裹干扰。一旦在分片逻辑里混入非数据库操作或跨服务调用,锁竞争会从 MySQL 转移到应用层或中间件,问题只会更隐蔽。











