根本解法是分段更新、Redis预扣减与隔离级别下调三者组合:分段需联合主键精准命中、事务仅含UPDATE和ROW_COUNT判断、设为READ COMMITTED禁用间隙锁,Redis用DECRBY原子拦截无效请求。

没有“终极”方案,只有组合解法:分段更新 + Redis预扣减 + 隔离级别下调,三者缺一不可。 单靠调参数、加索引或换引擎,解决不了X锁排队这个根本问题。
为什么UPDATE WHERE id = ?一定会卡住?
不是没走索引,也不是配置太低——而是InnoDB对同一行的X锁必须串行获取。哪怕你用EXPLAIN确认type是const、rows=1,只要所有请求都打在product_id = 123这一行上,就必然排队。聚簇索引让这行物理位置固定,数据分布无法摊薄压力。
-
SHOW ENGINE INNODB STATUS里反复出现waiting for trx id,就是典型信号 -
information_schema.INNODB_TRX中大量事务状态为RUNNING但trx_state = 'LOCK WAIT' - CPU跑满,磁盘IO却很低:说明不是I/O瓶颈,是锁在空转等待
分段更新必须满足的三个硬性前提
把单行库存拆成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;若shard_id没进WHERE,或联合索引顺序错(比如建了(shard_id, product_id)),就会全表扫 - 事务只包这一条
UPDATE+ROW_COUNT()判断:里面不能有日志、HTTP调用、SLEEP(),否则锁滞留时间指数增长 - 隔离级别必须设为
READ COMMITTED:它禁用间隙锁,避免shard_id范围查询时意外锁住相邻槽位;默认的REPEATABLE READ会让锁范围扩大
Redis预扣减不是可选项,而是第一道闸口
真实压测中,80%的请求是库存已空还狂点——这些根本没必要进MySQL。用Redis原子操作提前拦截:
- 用
DECRBY尝试扣减,返回负数就直接拒绝,不走DB - 真正要落库的请求,先塞进
Redis Streams或带限速的消费者队列,控制DB写入节奏(比如稳定500 QPS) - 别在事务里调HTTP接口或写日志文件——锁会一直挂着,哪怕你只是
sleep(100ms)
别碰innodb_deadlock_detect=OFF这类危险开关
禁用死锁检测看似能省开销,但代价是把问题从“快速失败”变成“长时间挂起”。高并发下大量事务等同一把锁,innodb_lock_wait_timeout超时(默认50秒)只是掩盖问题,业务通常等不了3秒。更糟的是,SQL_NO_CACHE在MySQL 8.0+已移除,用了等于白写,还可能干扰执行计划。
真正容易被忽略的点是:分段更新后查总数用SUM(stock),接受短暂不一致——这不是妥协,而是把一致性成本从数据库层转移到应用层,换来吞吐量翻倍。


















