高频交易系统中UPDATE行锁竞争是必须优化的硬瓶颈,需设隔离级别为READ COMMITTED、禁用间隙锁、确保binlog_format=ROW、避免索引列计算、批量ID升序排列、热点行分段(shard_id+联合主键)、Redis预扣减拦截无效请求。

高频交易系统里,UPDATE语句的行锁竞争不是“能不能优化”的问题,而是“不优化就崩”的问题。核心矛盾在于:哪怕WHERE条件命中主键,只要所有请求打在同一行,InnoDB的X锁天然串行,QPS上限由单核CPU调度能力决定——这不是慢,是硬瓶颈。
UPDATE WHERE id = ? 必须走主键索引,但光走索引还不够
很多人用EXPLAIN确认type=const、rows=1就以为万事大吉,其实漏掉了关键一环:执行计划只反映“怎么找行”,不反映“锁多久”和“锁哪些间隙”。在REPEATABLE READ隔离级别下,即使更新单行,InnoDB仍可能加临键锁(Next-Key Lock),把该行前后的间隙也锁住,导致INSERT被阻塞。
- 必须把隔离级别设为
READ COMMITTED:它禁用间隙锁,UPDATE执行完立即释放不匹配的锁,大幅降低范围操作引发的隐式阻塞 - 检查
binlog_format是否为ROW:READ COMMITTED+STATEMENTbinlog会导致主从不一致,线上严禁混用 - 避免在WHERE中对索引列做任何计算或函数调用:比如
WHERE id + 0 = 123或WHERE CAST(id AS CHAR) = '123'都会让索引失效,退化为全表扫描加锁
批量UPDATE IN (id1, id2, ...) 必须强制升序排列ID列表
死锁高频发生的原因不是并发高,而是两个事务以不同顺序加锁。InnoDB按SQL中IN子句出现的顺序逐个加锁,不重排、不优化。事务A执行UPDATE t SET v=1 WHERE id IN (5,1,3),事务B执行UPDATE t SET v=1 WHERE id IN (3,5,1),极易形成ABBA死锁链。
- 所有批量更新前,应用层必须对ID数组做升序排序,再拼成
WHERE id IN (1,3,5) - ORM如MyBatis动态生成SQL时,
<if>标签可能导致条件顺序不可控,关键交易路径建议改用预编译+排序后参数绑定 - 别依赖
ORDER BY救场:UPDATE ... WHERE id IN (...) ORDER BY id只影响修改顺序,不影响加锁顺序
热点行UPDATE必须分段(shard by slot),且满足三个硬前提
把UPDATE stock SET qty = qty - 1 WHERE product_id = 123改成单点瓶颈,本质是物理存储无法分散压力。分段不是加缓存,是把一行数据逻辑拆成多行,让锁竞争从“1 vs N”变成“N vs N”。
- 建表加
shard_id字段,联合主键设为(product_id, shard_id),并确保WHERE条件完整包含两者,否则EXPLAIN中key不会显示PRIMARY - 事务内只能有这一条UPDATE +
ROW_COUNT()判断,禁止任何日志、RPC、SLEEP;否则锁持有时间从毫秒级拉长到秒级,分段效果归零 - 全局配置
tx_isolation = 'READ-COMMITTED',否则间隙锁可能跨shard_id锁住相邻槽位,反而扩大锁范围
别让MySQL承担第一道流量过滤职责
真实压测中,80%的UPDATE请求根本不需要进MySQL——比如库存已空还持续点击。这些请求在到达数据库前就应该被拦截,否则只是把锁等待队列拉得更长。
- 用Redis分片Key做预扣减:
DECRBY goods:123:stock:0 1,返回值≥0才放行,否则直接拒绝 - Redis Key按
product_id % N分片,避免单key热点;N一般取8–16,太大增加聚合查询开销 - MySQL最终校验仍需保留:
UPDATE stock SET qty = qty - 1 WHERE product_id = 123 AND shard_id = 7 AND qty >= 1,防止网络分区导致超卖
最易被忽略的是:分段更新后,SELECT SUM(qty)查总数会变慢,且结果有短暂延迟。这不是缺陷,是设计取舍——你要的是高并发写入吞吐,不是强一致性读。如果业务真需要实时总数,说明分段策略本身就不适配当前场景。


















