mysql中使用锁时避免死锁的策略与技巧

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

死锁不是锁用错了,而是事务访问资源的顺序不一致

MySQL 中的死锁几乎都源于多个事务以不同顺序加锁访问同一组行。InnoDB 会自动检测并回滚其中一个事务(报错

Deadlock found when trying to get lock
),但频繁死锁说明业务逻辑或 SQL 组织存在隐患。

关键不是“怎么加锁”,而是“怎么让所有事务按相同顺序访问行”。比如两个事务都先更新

user_id=100
再更新
user_id=200
,就不会死锁;但如果一个按 ID 升序、另一个按降序,就极易触发。

始终按主键或唯一索引的**确定性顺序**组织 DML:例如用
ORDER BY id ASC
显式排序后再
UPDATE
SELECT ... FOR UPDATE
避免在应用层分批处理时「凭感觉」取数据:比如分页查出一批 ID 后再循环更新,若两次查询结果顺序不一致(如没加
ORDER BY
),就可能打乱加锁顺序
批量操作尽量用单条语句完成,而不是拆成多条独立的
UPDATE
:一条
UPDATE ... WHERE id IN (1,5,3)
的加锁顺序由 InnoDB 内部按主键排序决定;而三条单独的
UPDATE
则完全取决于执行顺序

SELECT ... FOR UPDATE 和 UPDATE 的加锁行为差异必须清楚

很多人以为

SELECT ... FOR UPDATE
UPDATE
加的是同一种锁,其实不然——前者是否加间隙锁(gap lock)、是否升级为临键锁(next-key lock),高度依赖 WHERE 条件是否命中索引、是否是唯一查找。

比如表

t
有主键
id
,执行:

SELECT * FROM t WHERE id = 10 FOR UPDATE;

只锁住

id=10
这一行(记录锁);但执行:

SELECT * FROM t WHERE id > 5 AND id < 15 FOR UPDATE;

会锁住

(5,15)
区间(间隙锁 + 可能的临键锁),这时如果另一个事务想插入
id=12
,就会被阻塞——而你可能根本没意识到这个隐含的范围锁。

尽量用等值条件 + 唯一索引做
SELECT ... FOR UPDATE
,避免范围扫描
确认是否真的需要
SELECT ... FOR UPDATE
:如果是为防并发修改,有时直接用带条件的
UPDATE
更安全(例如
UPDATE t SET status='processing' WHERE id=10 AND status='ready'
),失败即说明已被抢
开启
innodb_locks_unsafe_for_binlog=OFF
(默认)时,普通
UPDATE
也会加间隙锁;但如果你用的是
READ-COMMITTED
隔离级别,间隙锁会被禁用——这点常被忽略

事务粒度越小越好,别把锁“捂”在手里太久

死锁概率随事务持有锁的时间呈非线性上升。哪怕加锁顺序完全一致,只要一个事务在

SELECT ... FOR UPDATE
后做了耗时操作(比如调外部 HTTP、写日志、复杂计算),另一个事务就可能在等待中被卷入死锁链。

把锁相关操作尽量靠近事务末尾:先做查询判断、本地计算,最后一步才
SELECT ... FOR UPDATE
UPDATE
避免在事务中调用不可控的外部服务;如有必要,拆成「预占位 → 外部执行 → 确认提交」三阶段,用状态字段控制 监控
innodb_row_lock_time_avg
innodb_deadlocks
状态变量,及时发现长事务或高频死锁点

用 SHOW ENGINE INNODB STATUS 定位真实死锁现场

报错信息里只有一句

Deadlock found when trying to get lock
,真正有用的是 MySQL 自动记录的最近一次死锁详情,必须手动查:

SHOW ENGINE INNODB STATUS\G

输出中

LATEST DETECTED DEADLOCK
段落会明确列出: - 涉及的两个事务各自的 SQL - 各自已持有的锁(
HELD LOCKS
) - 正在等待的锁(
WAITING FOR THIS LOCK TO BE GRANTED
) - 甚至哪一行被哪个事务锁着、锁类型(
record lock
,
gap before rec

不要只看报错事务的 SQL,重点对比「对方事务持有什么锁」和「本事务在等什么锁」——这才是顺序冲突的证据 注意
Trx has been waiting 2 sec
这类提示:说明等待已久,大概率是上游事务卡住了,不是锁本身设计问题
生产环境建议开启
innodb_print_all_deadlocks=ON
,把每次死锁都记到 error log,便于事后归因

死锁不是靠“加锁技巧”消除的,而是靠对 InnoDB 行锁机制的理解 + 对业务访问模式的收敛。最容易被忽略的一点是:你以为自己只锁了一行,其实 InnoDB 因为索引结构或隔离级别,悄悄锁了一片区间——这往往才是死锁的起点。

相关推荐