死锁源于事务间循环等待资源,解决需打破循环等待条件。通过缩短事务时间、统一加锁顺序、使用索引减少锁范围、合理使用SELECT FOR UPDATE并配合ORDER BY、避免长事务和全表扫描,结合应用层重试机制,可显著降低死锁发生概率;排查时利用SHOW ENGINE INNODB STATUS分析冲突事务的锁持有与等待关系,定位问题SQL并优化执行计划。

MySQL死锁,说到底就是两笔或多笔交易(事务)在争抢资源时,各自都持有了一部分资源,又都想获取对方持有的另一部分资源,结果谁也无法继续,形成了一个僵局。要避免它,核心思路就是减少事务冲突的可能性,并建立一套可预测的资源获取顺序。这听起来有点抽象,但落实到具体操作,就是精细化事务管理、优化查询和设计合理的数据访问模式。
死锁这东西,我个人觉得它就像是数据库并发世界里的一面镜子,映照出我们对数据访问和事务设计的理解深度。它不像慢查询那样直接指向性能瓶颈,更多时候,它是在提醒我们:嘿,你这里的数据访问逻辑可能有点混乱,或者说,不够“礼貌”。
避免死锁,首先要从根本上理解它的发生机制,然后才能对症下药。我的经验是,没有银弹,但一系列最佳实践的组合拳,能大大降低死锁的发生概率。
A 表的记录,再锁定 B 表的记录。这就像大家排队,都按一个方向走,就不会在路口撞车。SELECT ... FOR UPDATE 的艺术:在需要更新数据之前,如果你知道稍后会更新某些行,提前使用 SELECT ... FOR UPDATE 显式锁定这些行。这能确保在后续更新时,这些行已经为你所独占,避免了其他事务介入并打乱你的锁定顺序。但也要注意,滥用它会增加锁等待。SQLSTATE '40001' 或错误代码 1213),然后重试整个事务。这是一种“防守性编程”,确保系统在面对死锁时能自我恢复。死锁的发生,并非偶然,而是并发控制中特定条件组合下的必然产物。从操作系统的角度看,它符合著名的“Coffman条件”:
举个例子,假设我们有两张表 accounts (账户) 和 transactions (交易),每个表都有主键索引。
事务 A:
START TRANSACTION; UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 锁定了 id=1 的账户 UPDATE transactions SET status = 'processed' WHERE account_id = 2; -- 尝试锁定 account_id=2 的交易 COMMIT;
事务 B:
START TRANSACTION; UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- 锁定了 id=2 的账户 UPDATE transactions SET status = 'pending' WHERE account_id = 1; -- 尝试锁定 account_id=1 的交易 COMMIT;
如果事务 A 先锁定了 accounts.id=1,同时事务 B 锁定了 accounts.id=2。
接着,事务 A 尝试锁定 transactions 表中 account_id=2 的记录,但这条记录可能已经被事务 B 的某个操作(即使是隐式的)锁住,或者事务 B 正在尝试锁定它。
同时,事务 B 尝试锁定 transactions 表中 account_id=1 的记录,而这条记录又可能被事务 A 锁住。
这就形成了一个完美的循环等待:A 等 B 释放,B 等 A 释放。MySQL的InnoDB存储引擎会检测到这种情况,并选择其中一个事务(通常是修改行数最少的那个)回滚,以打破死锁。
理解了这些,我们就能明白,避免死锁的核心就是打破这些条件中的一个或多个,特别是循环等待条件。
在实际的系统设计和编码过程中,我们可以从多个维度入手,将死锁的预防融入到日常开发中。
事务粒度与范围的精确控制:
强制一致的资源访问顺序:
全局约定:在设计数据库访问层时,就应该规定好访问多表或多行时的顺序。例如,涉及 User 和 Order 两张表的操作,总是先处理 User 表,再处理 Order 表。
按主键/唯一键排序:如果需要更新多行数据,且这些行分布在同一个表中,可以先根据它们的主键或唯一键进行排序,然后按序更新。
-- 错误示范,可能导致死锁 UPDATE accounts SET balance = balance - 100 WHERE id = 1; UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- 较好实践,如果需要同时更新,且可以提前确定所有ID -- 假设要更新 id=1 和 id=2 的账户,并且总是按ID升序锁定 SELECT * FROM accounts WHERE id IN (1, 2) FOR UPDATE ORDER BY id; UPDATE accounts SET balance = balance - 100 WHERE id = 1; UPDATE accounts SET balance = balance + 100 WHERE id = 2;
FOR UPDATE 结合 ORDER BY 是一个非常强大的组合,它能确保你以一个固定的、可预测的顺序锁定所有需要的行。
优化SQL查询,提升索引使用效率:
WHERE 条件能有效利用索引,避免全表扫描。合理使用锁定机制:
SELECT ... FOR UPDATE 的精准使用:当需要确保数据在读取后到更新前不会被其他事务修改时,这非常有用。但只锁定真正需要修改的行。LOCK TABLES:除非万不得已(比如进行DDL操作),否则尽量不要使用 LOCK TABLES,因为它会锁定整张表,极大地限制了并发。应用程序层面的重试机制:
Deadlock found when trying to get lock; try restarting transaction 这样的错误。死锁不是“避免”了就万事大吉,它总会以意想不到的方式出现。关键在于,当它发生时,我们有没有能力快速定位问题并进行优化。
SHOW ENGINE INNODB STATUS 是你的朋友:
这是排查InnoDB死锁的黄金命令。它的输出非常详细,其中有一个 LATEST DETECTED DEADLOCK 部分,会清晰地告诉你:
-- 示例输出片段(简化版) ------------------------ LATEST DETECTED DEADLOCK ------------------------ 2023-10-27 10:30:05 0x7f9a8c0d7000 *** (1) TRANSACTION: TRANSACTION 12345, ACTIVE 5 sec starting 'SQL_TEXT_1' LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s) MySQL thread id 10, OS thread handle 0x7f9a8c0d7000, query id 20 localhost root updating UPDATE accounts SET balance = balance - 100 WHERE id = 1 *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 123 page no 456 n bits 72 index `PRIMARY` of table `test`.`accounts` trx id 12345 lock_mode X locks rec but not gap waiting Record lock, table `test`.`accounts`, index `PRIMARY`, id 2 *** (2) TRANSACTION: TRANSACTION 12346, ACTIVE 7 sec starting 'SQL_TEXT_2' LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s) MySQL thread id 11, OS thread handle 0x7f9a8c0d8000, query id 21 localhost root updating UPDATE accounts SET balance = balance + 100 WHERE id = 2 *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 123 page no 456 n bits 72 index `PRIMARY` of table `test`.`accounts` trx id 12346 lock_mode X locks rec but not gap Record lock, table `test`.`accounts`, index `PRIMARY`, id 1 *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 123 page no 456 n bits 72 index `PRIMARY` of table `test`.`accounts` trx id 12346 lock_mode X locks rec but not gap waiting Record lock, table `test`.`accounts`, index `PRIMARY`, id 1 *** WE ROLL BACK TRANSACTION (1)
通过这个输出,你可以清晰地看到事务12345在等待id=2的锁,而事务12346持有id=1的锁,并也在等待id=1的锁(这里示例有点简化,实际是它持有的锁和它等待的锁构成循环)。
利用 information_schema 视图:
information_schema.innodb_trx、information_schema.innodb_locks 和 information_schema.innodb_lock_waits(或 sys.innodb_lock_waits 在MySQL 5.7+)提供了更细粒度的当前事务和锁信息。你可以通过这些视图实时监控锁等待情况,虽然它们不会直接报告死锁,但可以帮助你理解系统当前的并发状态。
分析慢查询日志和错误日志:
error.log)会记录死锁的详细信息,这比 SHOW ENGINE INNODB STATUS 更持久,方便事后分析。确保你的MySQL配置开启了死锁日志记录。审查数据库模式和索引:
WHERE 子句、JOIN 条件和外键都有合适的索引。调整 innodb_lock_wait_timeout 参数:
这个参数定义了一个事务在等待锁时最长等待多少秒。默认值通常是50秒。
死锁的排查和优化,很多时候更像是一场侦探游戏,需要结合代码逻辑、SQL执行计划、MySQL的内部状态报告,才能最终找到那个“元凶”并加以解决。
以上就是mysqlmysql如何避免死锁发生的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号