事务中行锁升级为表锁的典型诱因
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),降低重试碰撞概率
锁竞争的本质不是“怎么加锁”,而是“谁在什么时候、以什么顺序、加了什么范围的锁”。越早看清执行计划和事务边界,越不容易掉进隐式锁升级和长事务的坑里。
