
查询和处理 MySQL 死锁是数据库管理中的一项核心技能。下面我将为你详细讲解如何查询死锁、分析死锁日志,并提供一些避免死锁的最佳实践。
死锁(Deadlock)是指两个或多个事务在执行过程中,因争夺锁资源而造成的一种互相等待的僵局。当事务 A 持有锁 1 并等待锁 2,而事务 B 持有锁 2 并等待锁 1 时,就会发生死锁。此时,若无外部干预,两个事务都将永远无法继续执行。
MySQL 的 InnoDB 存储引擎会自动检测死锁,并通过回滚其中一个事务(通常是回滚代价较小的那个)来打破僵局。
查询死锁主要有以下三种方法,从简单到深入。
MySQL 会记录最近一次发生的死锁详细信息。你可以通过执行以下命令来查看:
SHOW ENGINE INNODB STATUS;
执行后,你会看到一个很长的状态报告。找到名为 LATEST DETECTED DEADLOCK 的部分,这里面包含了死锁的所有关键信息。
关键信息解读:
-
*** (1) TRANSACTION / *** (2) TRANSACTION: 参与死锁的两个事务。
-
UPDATE products SET stock = stock - 1 WHERE id = 1;: 每个事务正在执行的 SQL 语句。
-
*** (1) HOLDS THE LOCK(S): 事务 1 已经持有的锁。
-
*** (1) WAITING FOR THIS LOCK TO BE GRANTED: 事务 1 正在等待的锁。
-
*** WE ROLL BACK TRANSACTION 12345: MySQL 最终决定回滚事务 12345 来解决死锁。
通过分析这些信息,你就能清楚地看到两个事务是如何互相等待对方的锁的。
如果你想持续监控死锁的发生,可以开启 MySQL 的死锁日志记录功能。
-
开启死锁日志在 MySQL 的配置文件 (my.cnf 或 my.ini) 的 [mysqld] 部分添加以下配置:
[mysqld]
innodb_print_all_deadlocks = 1
-
重启 MySQL 服务修改配置后,需要重启 MySQL 服务才能生效。
sudo systemctl restart mysql
-
查看日志文件之后,所有发生的死锁都会被记录到 MySQL 的错误日志文件中。你可以通过以下命令找到错误日志的位置:
SHOW VARIABLES LIKE 'log_error';
然后使用 tail, grep 等命令来实时查看或检索死锁日志。
tail -f /var/log/mysql/error.log | grep "DEADLOCK"
如果死锁正在发生,你可以查询哪些事务正在等待锁,这有助于实时定位问题。
SELECT
r.trx_id,
r.trx_mysql_thread_id,
r.trx_query,
b.trx_id AS blocking_trx_id,
b.trx_mysql_thread_id AS blocking_thread_id,
b.trx_query AS blocking_query
FROM
information_schema.innodb_lock_waits w
JOIN
information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
JOIN
information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id;
这个查询会显示:
-
trx_id: 请求锁的事务 ID。
-
trx_query: 请求锁的事务正在执行的 SQL。
-
blocking_trx_id: 持有锁并造成阻塞的事务 ID。
-
blocking_query: 持有锁的事务正在执行的 SQL。
查询到死锁只是第一步,更重要的是解决它。
如果发现某个事务长期阻塞,可以手动终止它。
-
找到阻塞的线程 ID从上面的 SHOW ENGINE INNODB STATUS 或 information_schema 查询中找到造成阻塞的 MySQL thread id。
-
终止线程
注意:这是一个高危操作,会强制回滚该线程正在执行的所有未提交事务,请谨慎使用。
从根本上避免死锁是最佳策略。以下是一些经过实践检验的最佳实践:
-
保持一致的锁顺序:这是最重要的原则。在所有事务中,确保以相同的顺序来获取锁(即更新表和行的顺序)。例如,在处理订单和库存时,所有事务都先更新
orders 表,再更新 products 表。
-
尽量缩小事务范围:让事务尽可能地短,减少持有锁的时间。避免在事务中执行耗时的操作,如等待用户输入、调用外部 API 等。
-
使用更低级别的锁:如果业务允许,使用
SELECT ... FOR UPDATE (行级锁) 代替 LOCK TABLES (表级锁)。行级锁的粒度更细,冲突概率更低。
-
为查询添加合适的索引:没有索引的
UPDATE 或 DELETE 语句可能会导致 MySQL 扫描全表并锁定所有行,大大增加死锁风险。确保 WHERE 子句中的条件列都有索引。
-
设置合理的事务隔离级别:大多数应用使用默认的
REPEATABLE READ 隔离级别。在某些情况下,降低到 READ COMMITTED 可以减少 Gap Lock(间隙锁)的范围,从而降低死锁概率。
-
使用
SELECT ... FOR UPDATE SKIP LOCKED (MySQL 8.0+):在高并发场景下,如果一个事务只是想获取一个可用的资源(如从队列中取任务),可以使用 SKIP LOCKED。它会跳过那些已经被锁定的行,而不是等待,从而从根本上避免了死锁。
-
查:使用
SHOW ENGINE INNODB STATUS; 查看最近一次死锁的详细信息。
-
析:分析死锁日志,找出是哪两个(或多个)事务、哪两条 SQL、争夺了哪些锁。
-
解:
-
紧急处理:使用
KILL 命令终止阻塞的事务。
-
长期优化:遵循保持一致的锁顺序等最佳实践,从根本上消除死锁产生的条件。
通过以上方法,你就可以有效地查询、分析和解决 MySQL 中的死锁问题了。