行锁升级为表锁的典型诱因是where条件使用非索引字段或函数,如concat(name,'')导致全表扫描加锁;应确保查询条件落在有效索引上,避免隐式转换、长in列表,并用explain验证执行计划。

事务中行锁升级为表锁的典型诱因
MySQL 的 InnoDB 默认用行锁,但很多情况下会 silently 升级成表锁——最常见的是在 WHERE 条件中使用了非索引字段或函数。比如执行 UPDATE user SET status=1 WHERE CONCAT(name, '') = 'alice',即使 name 有索引,CONCAT 也会让优化器放弃索引,触发全表扫描+全表加锁。
这类操作在高并发下极易引发锁等待甚至死锁。避免方法很简单:确保所有 WHERE、ORDER BY、GROUP BY 字段都落在有效索引上,且不被函数/表达式包裹。
- 用
EXPLAIN检查执行计划,确认type是ref/range而非ALL或index - 避免在条件中对索引列做隐式类型转换,如
WHERE user_id = '123'(user_id是INT) - 批量更新时慎用
IN列表过长(>1000 项),可能退化为临时表扫描,改用分批或JOIN临时表
SELECT ... FOR UPDATE 的范围控制误区
SELECT ... FOR UPDATE 看似只锁查到的行,实际会锁住满足条件的**索引区间**,尤其在非唯一索引或无索引时,可能锁住整个索引段(gap lock)。例如在 age 字段(非唯一、无索引)上执行 SELECT * FROM user WHERE age > 25 FOR UPDATE,可能锁住所有 age > 25 的记录,甚至包括未来插入的值(next-key lock)。
这不是 bug,而是为了防止幻读。但业务中若只需锁具体几条已知主键的记录,就该直接按 PRIMARY KEY 查:
SELECT * FROM user WHERE id IN (101, 102, 105) FOR UPDATE;
这样只会加 record lock,不涉及 gap,锁粒度最小。
- 尽量用主键或唯一索引做
FOR UPDATE条件 - 若必须用非唯一索引,考虑在事务开始前先
SELECT ... LOCK IN SHARE MODE验证数据存在性,再更新,减少锁持有时间 - 确认是否真的需要可重复读(RR)隔离级别;如业务允许读已提交(RC),则
UPDATE不加 gap lock,仅锁匹配行
长事务导致锁持有时间过长
锁不是在 SQL 执行完就释放,而是在事务 COMMIT 或 ROLLBACK 后才释放。一个事务里混入日志写入、HTTP 调用、循环处理等耗时操作,会让行锁“悬停”数秒甚至更久,阻塞其他事务。
典型表现是 SHOW ENGINE INNODB STATUS 中看到大量 TRX_WAITING,且 trx_wait_started 时间戳远早于当前时间。
- 把事务边界收窄:只包裹真正需要原子性的 DML 操作,其他逻辑移出事务外
- 避免在事务内调用外部服务;如必须,先
SELECT FOR UPDATE获取数据并缓存,COMMIT后再调用 - 监控
innodb_trx.trx_state = 'LOCK WAIT'和trx_started时间差,设置告警阈值(如 > 2s)
死锁检测与自动回滚不可依赖
MySQL 的死锁检测是主动的,一旦发现环路会选一个事务回滚(Deadlock found when trying to get lock),但这只是兜底机制,不是设计目标。频繁死锁说明访问顺序混乱,比如事务 A 先锁 user 再锁 order,事务 B 反过来,就必然冲突。
解决核心是统一资源访问顺序:
- 所有业务模块按固定顺序操作多张表,比如约定总是先
user→ 再order→ 最后payment - 对同一张表的多行更新,按主键升序排列(
ORDER BY id ASC),避免不同事务以不同顺序加锁 - 不要在应用层重试死锁错误时盲目立即重试,应加随机小延迟(如 10–100ms),降低重试碰撞概率
锁竞争的本质不是“怎么加锁”,而是“谁在什么时候、以什么顺序、加了什么范围的锁”。越早看清执行计划和事务边界,越不容易掉进隐式锁升级和长事务的坑里。










