mysql事务中锁的竞争如何避免_mysql锁竞争控制

来源:这里教程网 时间:2026-02-28 20:49:49 作者:

事务中行锁升级为表锁的典型诱因

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),降低重试碰撞概率

锁竞争的本质不是“怎么加锁”,而是“谁在什么时候、以什么顺序、加了什么范围的锁”。越早看清执行计划和事务边界,越不容易掉进隐式锁升级和长事务的坑里。

相关推荐