在 MySQL 中,死锁是指两个或多个事务相互等待对方释放锁,导致所有相关事务无法继续执行的情况。死锁会影响数据库的并发性能,因此需要及时检测并处理。假设有两个事务 T1 和 T2:
事务 T1 首先锁定 表 A 的行 1。然后尝试锁定 表 B 的行 1。
事务 T2 首先锁定 表 B 的行 1。然后尝试锁定 表 A 的行 1。
T1 锁定了表 A 的行 1,并等待获取表 B 的行 1。
T2 锁定了表 B 的行 1,并等待获取表 A 的行 1。
此时,T1 和 T2 互相等待,导致死锁。
解决方式:MySQL 会自动检测死锁,并回滚其中一个事务,释放其占有的锁,以使另一个事务得以继续执行。
MySQL 会在以下几种情况下产生死锁(Deadlock):
1. 多个事务同时锁定相同的资源(循环等待)
- 当两个或多个事务以不同的顺序锁定相同的资源,并相互等待对方释放锁时,就会发生死锁。
- 示例:
由于事务 A 和事务 B 互相等待对方释放锁,造成死锁。-- 事务 A START TRANSACTION; UPDATE table1 SET column1 = 'value1' WHERE id = 1; -- 锁住 id = 1 UPDATE table1 SET column1 = 'value2' WHERE id = 2; -- 等待事务 B 释放 id = 2-- 事务 B START TRANSACTION; UPDATE table1 SET column1 = 'value2' WHERE id = 2; -- 锁住 id = 2 UPDATE table1 SET column1 = 'value1' WHERE id = 1; -- 等待事务 A 释放 id = 1
2. 索引不合理导致的意外行锁升级
- 在 InnoDB 存储引擎下,行锁是基于索引 的,而不是基于物理行。如果 SQL 语句未使用索引,会导致 表锁,进而增加死锁的可能性。
- 示例:
由于-- 假设 `column1` 没有索引 SELECT * FROM table1 WHERE column1 = 'some_value' FOR UPDATE;
column1
没有索引,InnoDB 会对整张表加锁,可能导致多个事务争抢表锁,增加死锁风险。
3. 不同事务的更新顺序不一致
- 当多个事务以不同的顺序修改同一张表的不同行时,容易产生死锁。
- 示例:
-- 事务 A UPDATE orders SET status = 'shipped' WHERE id = 1; UPDATE orders SET status = 'shipped' WHERE id = 2;-- 事务 B UPDATE orders SET status = 'shipped' WHERE id = 2; UPDATE orders SET status = 'shipped' WHERE id = 1;
- 事务 A 先锁住
id=1
,再尝试锁id=2
; - 事务 B 先锁住
id=2
,再尝试锁id=1
; - 结果:两个事务都在等待对方释放锁,造成死锁。
- 事务 A 先锁住
4. 外键约束导致的隐式锁
- 由于 外键约束 可能导致级联更新/删除操作,在并发事务中可能出现死锁。
- 示例:
由于外键约束,删除-- 假设 child_table 具有外键约束 DELETE FROM parent_table WHERE id = 1; DELETE FROM parent_table WHERE id = 2;
parent_table
的数据会影响child_table
,多个事务并发执行时,可能导致死锁。
5. 间隙锁(Next-Key Lock)
- 在 REPEATABLE READ 隔离级别下,InnoDB 可能会对索引范围执行 间隙锁(Gap Lock),导致多个事务在并发插入或更新时出现死锁。
- 示例:
SELECT * FROM users WHERE age BETWEEN 18 AND 25 FOR UPDATE;
- 如果多个事务同时查询
18~25
之间的数据并尝试更新,就可能导致死锁。
- 如果多个事务同时查询
如何解决 MySQL 死锁?
- 使用索引
- 确保
WHERE
条件涉及索引,避免表锁导致的死锁。
- 确保
- 控制事务顺序
- 保证多个事务按相同的顺序访问数据,减少死锁发生的概率。
- 减少事务持有锁的时间
- 使用 短事务,减少锁持有时间:
START TRANSACTION; -- 快速执行 SQL 语句 COMMIT;
- 使用 短事务,减少锁持有时间:
- 降低隔离级别
- 在合适的业务场景下,将事务隔离级别从
REPEATABLE READ
降为READ COMMITTED
,避免间隙锁导致的死锁。
- 在合适的业务场景下,将事务隔离级别从
- 监控死锁并进行重试
- 通过
SHOW ENGINE INNODB STATUS\G
查看死锁信息。 - 代码层面可使用重试机制,如:
for _ in range(3): # 允许最多重试3次try:cursor.execute("UPDATE ...") # 可能导致死锁的操作connection.commit()break # 成功执行,跳出循环except MySQLdb.OperationalError as e:if "Deadlock" in str(e):continue # 遇到死锁,重试else:raise # 其他错误则抛出
- 通过