网站Logo GESONG

数据库死锁问题

gesong
2
2025-09-26

数据库死锁是数据库系统中一个常见的问题,它发生在两个或多个事务彼此等待对方释放锁,从而导致所有事务都无法继续执行的情况。

数据库通常会自动检测死锁并强制回滚其中一个事务,以让其他事务继续进行,但频繁发生死锁会严重影响系统性能和用户体验。因此排查死锁问题至关重要。

排查死锁四部曲

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. 实施解决方案

根据死锁原因,选择合适的解决方案。

  1. 保证一致的访问顺序

    • 这是最重要且最有效的方案。如果所有业务逻辑都按照相同的顺序去访问多个资源(数据行、表),就可以从根本上避免循环等待。

    • 例子:如果多个地方都需要更新 accounts 表中的多行,可以制定一个规则:永远按照 id 升序的顺序进行更新。即使事务需要更新id=2和id=1,也先更新id=1,再更新id=2。

  2. 减少事务粒度、缩短事务长度

    • 尽快提交:事务内不要做过多的计算、不要执行RPC调用等耗时操作,尽快提交事务,释放锁。

    • 避免不必要的行锁:精确使用WHERE条件,避免带锁扫描过多数据。

  3. 使用较低的隔离级别

    • 如果业务允许,将隔离级别从 可重复读(RR) 降为 读已提交(RC)。RC隔离级别可以避免很多间隙锁(Gap Lock),从而减少死锁概率。

    • SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

  4. 为SELECT操作添加合理的锁

    • 如果你在事务中需要先查询数据,然后根据查询结果决定更新,并且不容许其他事务修改该数据,那么你的SELECT语句应该直接加锁,而不是简单的 SELECT ...

    • SELECT ... FOR UPDATE:加排他锁。(用于要更新数据的情况)

    • SELECT ... LOCK IN SHARE MODE:加共享锁。(用于确保数据在事务期间不改变,但自身不更新它的情况)

    • 这样可以在事务一开始就持有必要的锁,避免后续的竞争条件。

  5. 重试机制

    • 在应用程序代码中,对于因死锁而失败的事务(MySQL错误码 1213,实现一个重试机制。

    • 捕获死锁异常,等待一个随机短时间(如100ms),然后自动重试整个事务(通常3次左右)。

    • 这是一种防御性编程,不能避免死锁,但能提高系统的容错性和用户体验。

动物装饰