获课地址:xingkeit.top/15604/
MySQL 死锁成因分析、排查与规避方案:从个人视角谈并发之痛
在关系型数据库的世界里,死锁大概是让每个后端开发者都头疼过的问题。它不像语法错误那样有明显的提示,也不像性能瓶颈那样有迹可循。很多时候,它悄无声息地出现,让某个接口突然卡住几秒,然后抛出一个“Deadlock found”的异常就退出了。我最早遇到死锁时的感受就是两个字:懵了。今天想从个人经验出发,聊聊我对死锁的理解,以及这些年积累的排查和规避思路。
死锁的本质:四个条件缺一不可
死锁听起来玄乎,其实说白了就是两个或多个事务在互相等待对方释放锁,结果谁也等不到谁,僵持住了。
从原理上讲,死锁的发生需要同时满足四个条件:互斥、持有并等待、不可剥夺、循环等待。这四个条件缺一不可。理解这一点有个实际意义:我们规避死锁的思路,本质上就是打破这四个条件中的任意一个。
互斥是 InnoDB 行锁本来就具备的特性,基本没法改变;持有并等待是事务默认的行为,也很难绕过;不可剥夺意味着锁只能由持有者主动释放。所以大多数人规避死锁的着力点都放在了“循环等待”上——通过约定加锁顺序,避免形成环路。
有个点我想特别强调:死锁不等于有 bug。在高并发系统中,死锁几乎是必然会发生的小概率事件。关键在于能否快速恢复,而不是追求永远不死锁。MySQL InnoDB 引擎本身就有死锁检测机制,一旦检测到死锁,会立刻回滚其中一个事务(通常是更新行数较少的一方),让另一个继续执行。所以只要业务代码里有重试逻辑,偶尔的死锁并不是灾难。
常见的死锁场景:那些年我踩过的坑
第一个让我印象深刻的死锁场景,是多字段条件更新导致的“间隙锁纠缠”。表里有一个联合索引,事务 A 锁住了一个范围的间隙,事务 B 也想锁同一个范围,但间隙锁本身是不互斥的,真正的问题往往出在插入操作上。当两个事务都想在同一个间隙里插入数据时,死锁就容易出现。
第二个常见场景是“先查后改”的经典模式。事务 A 先 SELECT 某行,发现不存在,于是准备 INSERT;事务 B 也做同样的操作。两个事务都先获得了共享锁(读锁),然后试图升级为排他锁(写锁)去插入。但共享锁和排他锁是不兼容的,A 等 B 释放共享锁,B 等 A 释放共享锁,死锁就这么形成了。这个问题的本质是:读操作和写操作的锁升级路径不同步。
还有一个让我意外的是“主键与二级索引的顺序问题”。InnoDB 的行锁是加在索引上的。如果一个事务先锁了二级索引,再锁主键索引;另一个事务恰好反过来操作,就有可能在两个索引之间形成循环等待。这种情况在通过不同条件更新同一条记录时特别容易出现。
排查方法:从现象到线索
遇到死锁时,第一反应不应该是慌乱,而是去看 MySQL 的错误日志和引擎状态。有一个命令我印象特别深刻:SHOW ENGINE INNODB STATUS。执行它会输出一大段信息,其中“LATEST DETECTED DEADLOCK”部分就是最近一次死锁的详细记录。
这一段信息里会告诉你:哪些事务参与了死锁,每个事务在等待什么锁、持有什么锁,最终哪个事务被回滚了。读这些信息需要一点耐心,因为输出格式不太友好,但信息本身是非常有价值的。我以前习惯直接把这个输出复制到文本编辑器里,慢慢梳理两个事务的加锁顺序,往往能找到循环等待的关键点。
除了即时查看死锁日志,我还会开启 innodb_print_all_deadlocks 参数,把所有的死锁信息都记录到错误日志里。这样即使死锁发生在凌晨三点,第二天早上也能复盘。另外,业务应用日志里的死锁异常时间戳,也能帮你和数据库的死锁记录对应上。
规避策略:不是消除,而是化解
规避死锁的策略,我认为最重要的是统一加锁顺序。无论是在代码里访问多个表,还是在同一个表里更新多行,都应该按照固定的顺序去操作。比如按主键升序更新,这样两个事务同时操作时,都会先锁小主键再锁大主键,不会形成环路。
其次是尽量简化事务。事务越长,持有的锁就越多,死锁的概率也越大。有些业务逻辑其实不需要在一个事务里完成,可以把只读的查询移出事务,只把必要的写操作包在事务里。另外,合理设置事务隔离级别也能帮忙。在可重复读隔离级别下,InnoDB 会使用间隙锁,这增加了死锁的可能性。如果业务场景允许读已提交(RC)级别,死锁的概率会明显降低。
对于“先查后改”的模式,一个有效的技巧是使用 INSERT ON DUPLICATE KEY UPDATE 或者 REPLACE,把两条语句合并成一条,避免锁升级的过程。还有就是在必要的时候使用 SELECT ... FOR UPDATE 主动加锁。与其让锁隐式地升级,不如一开始就明确要加什么锁,这样加锁路径清晰,反而更可控。
最后,应用层的重试机制是最后一道防线。无论你怎么优化,死锁在极高并发下依然可能出现。捕获“Deadlock found when trying to get lock”异常后等待几十毫秒再重试,往往就能成功。重试三次仍失败才算是真正的异常,这个策略在我经历的项目里屡试不爽。
死锁不是一个需要“根治”的问题,而是一个需要“管理”的风险。理解它的成因,掌握排查方法,并在代码层面做好应对,你会发现它其实没那么可怕。每次成功解决一个死锁问题,都是对数据库并发机制更深一层理解的机会。
本站不存储任何实质资源,该帖为网盘用户发布的网盘链接介绍帖,本文内所有链接指向的云盘网盘资源,其版权归版权方所有!其实际管理权为帖子发布者所有,本站无法操作相关资源。如您认为本站任何介绍帖侵犯了您的合法版权,请发送邮件
[email protected] 进行投诉,我们将在确认本文链接指向的资源存在侵权后,立即删除相关介绍帖子!
暂无评论