MySQL作为常用的关系型数据库,InnoDB存储引擎下事务并发执行时可能会出现死锁情况,即两个或多个事务互相持有对方需要的锁,导致所有事务都无法继续执行。了解死锁的分析方法,是处理这类问题的核心能力。

MySQL死锁的基础认知
死锁的发生需要满足四个条件:互斥、持有并等待、不可剥夺、循环等待。在InnoDB中,死锁通常会被引擎自动检测到,然后回滚其中一个事务来打破死锁状态,此时客户端会收到死锁相关的错误提示。
我们可以通过SHOW_ENGINE_INNODB_STATUS命令查看InnoDB引擎的实时状态,其中就包含最近的死锁信息,这部分内容是死锁分析的核心依据。
开启MySQL死锁日志
默认情况下,MySQL的死锁信息只会临时存储在内存中,重启后会丢失,我们可以通过配置参数将死锁日志持久化到错误日志中,方便后续排查。
配置参数说明
- innodb_print_all_deadlocks:设置为ON时,所有死锁信息都会记录到MySQL的错误日志中,默认是OFF,只记录最近一次死锁信息。
- log_error:指定错误日志的存储路径,死锁日志会写入到这个文件中。
修改配置示例
可以通过命令行动态修改参数,也可以写入配置文件永久生效:
-- 动态开启所有死锁日志记录 SET GLOBAL innodb_print_all_deadlocks = ON; -- 查看当前参数状态 SHOW VARIABLES LIKE 'innodb_print_all_deadlocks'; SHOW VARIABLES LIKE 'log_error';
查看和解析死锁日志
我们可以通过两种方式获取死锁日志,一种是直接查看InnoDB状态,另一种是从错误日志中读取持久化的日志内容。
获取死锁日志的方法
执行以下命令可以获取包含死锁信息的InnoDB状态:
SHOW ENGINE INNODB STATUSG
在输出的内容中找到LATEST DETECTED DEADLOCK部分,就是最近的死锁详情。如果是开启了持久化配置,也可以直接查看错误日志文件中的对应内容。
死锁日志核心字段解析
一段典型的死锁日志包含以下关键信息:
| 字段名 | 含义说明 |
|---|---|
| 2024-05-20 10:30:00 0x7f3a2b1c4700 | 死锁发生的时间以及对应的线程ID |
| *** (1) TRANSACTION | 第一个死锁事务的编号,后面跟着事务ID、活跃时间、使用的锁数量等信息 |
| *** (1) HOLDS THE LOCK | 该事务当前持有的锁信息,包括锁类型、锁对应的表、索引、记录范围 |
| *** (1) WAITING FOR THE LOCK | 该事务正在等待的锁信息 |
| *** (2) TRANSACTION | 第二个死锁事务的详细信息,结构和第一个事务一致 |
| *** WE ROLL BACK TRANSACTION (2) | 引擎选择回滚的事务编号,通常是回滚成本较小的事务 |
死锁分析实战示例
我们模拟一个常见的死锁场景,两个事务按照不同顺序更新同两条记录,来演示完整的分析过程。
模拟死锁场景
首先创建测试表并插入数据:
CREATE TABLE test_deadlock (
id INT PRIMARY KEY,
num INT
) ENGINE=InnoDB;
INSERT INTO test_deadlock VALUES (1, 10), (2, 20);
然后开启两个事务,按照以下顺序执行SQL:
事务1执行:
BEGIN; UPDATE test_deadlock SET num = num + 1 WHERE id = 1;
事务2执行:
BEGIN; UPDATE test_deadlock SET num = num + 1 WHERE id = 2;
事务1继续执行:
UPDATE test_deadlock SET num = num + 1 WHERE id = 2;
事务2继续执行:
UPDATE test_deadlock SET num = num + 1 WHERE id = 1;
此时事务2会立即提示死锁错误,事务1的第二个更新会等待直到事务2被回滚后执行成功。
解析对应的死锁日志
通过SHOW ENGINE INNODB STATUSG获取到的死锁日志中,可以看到事务1持有id=1的排他锁,等待id=2的排他锁;事务2持有id=2的排他锁,等待id=1的排他锁,形成了循环等待,符合死锁的条件,引擎最终回滚了事务2。
死锁的预防和解决建议
根据死锁的分析结果,我们可以采取以下措施减少死锁的发生:
- 尽量让所有事务按照相同的顺序访问表和记录,避免循环等待锁的情况。
- 缩小事务的范围,减少事务持有锁的时间,降低死锁发生的概率。
- 为查询语句添加合适的索引,避免无索引导致的表锁或者大范围间隙锁。
- 如果业务允许,可以降低事务的隔离级别,比如从可重复读调整为读已提交,减少间隙锁的使用。
- 在代码中捕获死锁错误,进行重试操作,提升业务的容错能力。
以下是一个简单的Java重试逻辑示例,处理MySQL死锁错误:
public void updateWithRetry() {
int retryCount = 0;
while (retryCount < 3) {
try {
// 执行数据库更新操作
executeUpdate();
break;
} catch (SQLException e) {
// MySQL死锁错误码是1213
if (e.getErrorCode() == 1213 && retryCount < 2) {
retryCount++;
try {
Thread.sleep(100);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
} else {
throw e;
}
}
}
}