根本原因是x锁串行排队,非索引或配置问题;高并发update同一行必然排队,需分段更新、redis预扣减和合并请求来缓解。

UPDATE同一行卡住,本质是X锁排队,不是索引或配置问题
高并发下UPDATE同一行(比如WHERE id = 123)卡死,根本原因不是没走索引、也不是innodb_buffer_pool_size太小,而是InnoDB必须让所有事务串行获取这把X锁。哪怕你加了主键索引、开了READ COMMITTED,只要请求全打在同一行上,就必然排队——聚簇索引让这行物理位置固定,没法靠数据分布分散压力。
常见错误现象: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查该UPDATE语句,type必须是const或eq_ref,rows≤ 1;否则实际锁了多行甚至全表
分段更新(shard by slot)必须满足三个硬性前提
把单行库存拆成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预扣减过滤无效请求,别让MySQL当第一道闸口
真实压测中,80%的请求是库存已空还狂点,这些根本没必要进MySQL。用Redis原子操作提前拦截,MySQL只处理真正需要落库的请求。
- Redis 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之间因网络分区导致超卖
合并请求时警惕延迟与强一致性边界
前端或网关层聚合“用户A点赞文章B”这类请求,能大幅降低数据库写入频次,但不是所有场景都适用。
- 适用场景:点赞数、浏览量、非核心计数器——允许
- 禁用场景:支付扣款、余额变更、库存扣减——强一致性要求下,合并会引入超卖或资损风险
- 合并策略选型:
Redis INCRBY适合累加类;LPUSH + BRPOPLPUSH适合带去重的ID列表;避免在合并逻辑里做复杂校验(如“余额是否足够”),这类检查必须回到数据库,但频率已大幅下降 - 最容易被忽略的坑:合并后生成的批量SQL(如
INSERT ... ON DUPLICATE KEY UPDATE)必须确保ON DUPLICATE KEY字段有唯一索引,否则查找阶段会锁大量无关行
实际落地时,分段更新和Redis预扣减往往组合使用,但关键不在技术堆叠,而在明确每层职责:Redis挡掉无效流量,分段更新摊薄剩余压力,MySQL只承担最终校验与持久化。任何一层越界(比如在Redis里做余额透支判断,或在MySQL事务里调支付网关),都会让优化失效。











