数据库死锁是数据库系统中一个常见的问题,它发生在两个或多个事务彼此等待对方释放锁,从而导致所有事务都无法继续执行的情况。
数据库通常会自动检测死锁并强制回滚其中一个事务,以让其他事务继续进行,但频繁发生死锁会严重影响系统性能和用户体验。因此排查死锁问题至关重要。
排查死锁四部曲
1. 确认死锁存在并捕获死锁信息
开启死锁日志,如mysql,在my.ini中设置
innodb_print_all_deadlocks=ON查看最近一次死锁的详细信息
show engine innodb status;2. 分析死锁日志
解读 MySQL 死锁日志示例:
假设 SHOW ENGINE INNODB STATUS 输出如下(简化版):
LATEST DETECTED DEADLOCK
*** (1) TRANSACTION:
TRANSACTION 3020, ACTIVE 5 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 20, OS thread handle 123145556811776, query id 125 localhost root updating
UPDATE accounts SET balance = balance - 100 WHERE id = 1
*** (1) HOLDS THE LOCK(S):
-- 事务1当前持有什么锁(这里可能省略,因为正在等待)
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
-- 事务1在等待什么锁
RECORD LOCKS space id 25 page no 3 n bits 72 index PRIMARY of table `test`.`accounts` trx id 3020 lock_mode X locks rec but not gap waiting
*** (2) TRANSACTION:
TRANSACTION 3021, ACTIVE 3 sec starting index read
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 21, OS thread handle 123145557065728, query id 126 localhost root updating
UPDATE accounts SET balance = balance + 100 WHERE id = 2
*** (2) HOLDS THE LOCK(S):
-- 事务2当前持有什么锁
RECORD LOCKS space id 25 page no 3 n bits 72 index PRIMARY of table `test`.`accounts` trx id 3021 lock_mode X locks rec but not gap
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
-- 事务2在等待什么锁
RECORD LOCKS space id 25 page no 3 n bits 72 index PRIMARY of table `test`.`accounts` trx id 3021 lock_mode X locks rec but not gap waiting
*** WE ROLL BACK TRANSACTION (2)3. 定位代码和业务逻辑
根据死锁信息中打印的 SQL 语句,去你的应用程序代码中找到它们所在的位置。
检查代码逻辑:
这两个事务是否在同一个业务流中被先后调用?
事务的边界是否正确?(是否该用事务的地方没用,不该用的地方用了?)
多个事务操作资源的顺序是否不一致?(例如,业务A先更新表1再更新表2,而业务B先更新表2再更新表1,极易引发死锁)
4. 实施解决方案
根据死锁原因,选择合适的解决方案。
保证一致的访问顺序
这是最重要且最有效的方案。如果所有业务逻辑都按照相同的顺序去访问多个资源(数据行、表),就可以从根本上避免循环等待。
例子:如果多个地方都需要更新
accounts表中的多行,可以制定一个规则:永远按照id升序的顺序进行更新。即使事务需要更新id=2和id=1,也先更新id=1,再更新id=2。
减少事务粒度、缩短事务长度
尽快提交:事务内不要做过多的计算、不要执行RPC调用等耗时操作,尽快提交事务,释放锁。
避免不必要的行锁:精确使用WHERE条件,避免带锁扫描过多数据。
使用较低的隔离级别
如果业务允许,将隔离级别从 可重复读(RR) 降为 读已提交(RC)。RC隔离级别可以避免很多间隙锁(Gap Lock),从而减少死锁概率。
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
为SELECT操作添加合理的锁
如果你在事务中需要先查询数据,然后根据查询结果决定更新,并且不容许其他事务修改该数据,那么你的SELECT语句应该直接加锁,而不是简单的
SELECT ...。SELECT ... FOR UPDATE:加排他锁。(用于要更新数据的情况)SELECT ... LOCK IN SHARE MODE:加共享锁。(用于确保数据在事务期间不改变,但自身不更新它的情况)这样可以在事务一开始就持有必要的锁,避免后续的竞争条件。
重试机制
在应用程序代码中,对于因死锁而失败的事务(MySQL错误码
1213),实现一个重试机制。捕获死锁异常,等待一个随机短时间(如100ms),然后自动重试整个事务(通常3次左右)。
这是一种防御性编程,不能避免死锁,但能提高系统的容错性和用户体验。