mysql死锁的常用解决办法
十分想念顺店杂可。。。
MySQL 死锁是并发场景中常见的问题,本质是两个或多个事务相互持有对方需要的锁,且都在等待对方释放锁,形成循环等待。解决死锁需从预防、检测和处理三个层面入手,以下是具体方案:
一、死锁的检测方法
首先需要确认死锁发生的具体场景,常用工具和命令:
查看 InnoDB 死锁日志
执行SHOW ENGINE INNODB STATUS;
,在输出结果的LATEST DETECTED DEADLOCK
部分可查看最近一次死锁的详细信息,包括:- 参与死锁的事务 ID、SQL 语句
- 事务持有和等待的锁类型(行锁、表锁等)
- 锁定的资源(行记录、索引等)
开启死锁日志记录
在my.cnf
中配置,持久化记录死锁日志:innodb_print_all_deadlocks = 1 # 记录所有死锁(默认只记录最近一次) log_error = /var/log/mysql/error.log # 日志存储路径
二、死锁产生的常见原因
事务操作顺序不一致
多个事务操作相同表 / 行时,操作顺序不同会导致循环等待。
例:事务 A 先更新表 1 再更新表 2,事务 B 先更新表 2 再更新表 1,可能形成死锁。锁粒度不合理
- 未使用索引导致行锁升级为表锁(InnoDB 行锁基于索引,无索引时会锁定全表)。
- 范围查询(如
WHERE id > 100
)产生间隙锁(Gap Lock),扩大锁定范围。
事务持有锁时间过长
事务中包含大量操作(如复杂计算、远程调用),导致锁长期不释放,增加冲突概率。隔离级别不当
较高的隔离级别(如REPEATABLE READ
)会产生更多锁(如间隙锁),死锁概率更高。
三、解决死锁的核心方案
1. 统一事务操作顺序(最有效)
确保所有事务操作相同资源(表、行)时,严格遵循相同的顺序,避免循环等待。
例:所有事务必须先操作表 A,再操作表 B,最后操作表 C。
-- 错误示例(顺序相反导致死锁)
-- 事务A
UPDATE table1 SET col=1 WHERE id=1; -- 持有table1的锁
UPDATE table2 SET col=1 WHERE id=1; -- 等待table2的锁(被事务B持有)-- 事务B
UPDATE table2 SET col=1 WHERE id=1; -- 持有table2的锁
UPDATE table1 SET col=1 WHERE id=1; -- 等待table1的锁(被事务A持有)-- 正确示例(统一顺序)
-- 事务A和事务B都先操作table1,再操作table2
UPDATE table1 SET col=1 WHERE id=1;
UPDATE table2 SET col=1 WHERE id=1;
2. 减小事务范围,缩短锁持有时间
- 事务只包含必要的 SQL 操作,避免无关逻辑(如计算、日志打印)。
- 拆分大事务为多个小事务,减少单次锁定的资源量。
-- 优化前(大事务,锁持有久)
BEGIN;
UPDATE order SET status=1 WHERE id=100; -- 锁定订单
SELECT * FROM user WHERE id=1; -- 无关查询,延长锁持有时间
UPDATE log SET content='xxx' WHERE order_id=100; -- 非核心操作
COMMIT;-- 优化后(小事务,仅保留核心操作)
BEGIN;
UPDATE order SET status=1 WHERE id=100; -- 核心操作,快速提交
COMMIT;-- 非核心操作单独处理(无需锁定订单)
BEGIN;
UPDATE log SET content='xxx' WHERE order_id=100;
COMMIT;
3. 优化索引和查询,减少锁冲突
- 确保查询使用索引:避免无索引导致的全表锁(InnoDB 行锁依赖索引)。
- 避免锁定不必要的行:使用精确的
WHERE
条件,减少锁定范围;避免SELECT ... FOR UPDATE
锁定过多行。 - 慎用范围查询:范围查询(如
id > 100
)会产生间隙锁,可改用精确查询或降低隔离级别。
-- 错误示例(无索引导致表锁)
UPDATE user SET name='xxx' WHERE phone='13800138000'; -- phone无索引,锁定全表-- 正确示例(添加索引,仅锁单行)
ALTER TABLE user ADD INDEX idx_phone(phone); -- 为phone添加索引
UPDATE user SET name='xxx' WHERE phone='13800138000'; -- 仅锁定符合条件的行
4. 调整事务隔离级别
InnoDB 默认隔离级别为REPEATABLE READ
,该级别会产生间隙锁(防止幻读),死锁概率较高。若业务允许,可降低至READ COMMITTED
:
- 减少间隙锁的使用(仅在外键约束和唯一性检查时保留)。
- 释放锁更快(非匹配行的锁会提前释放)。
-- 临时修改当前会话隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;-- 永久修改(my.cnf)
transaction-isolation = READ-COMMITTED
5. 应用层重试机制
死锁发生后,MySQL 会自动回滚其中一个事务(“牺牲品”),应用程序可捕获1213
错误码(死锁错误),重试事务:
// Java示例:捕获死锁错误并重试
int maxRetries = 3;
int retryCount = 0;
while (retryCount < maxRetries) {try {// 执行事务逻辑executeTransaction();break;} catch (SQLException e) {// 1213是MySQL死锁错误码if (e.getErrorCode() == 1213) {retryCount++;// 短暂休眠后重试(避免立即重试再次冲突)Thread.sleep(100 * retryCount);} else {// 处理其他错误throw e;}}
}
6. 其他辅助手段
- 使用表锁代替行锁:在高并发且表数据量小的场景,可主动使用
LOCK TABLES
强制表锁(需谨慎,会降低并发)。 - 监控死锁趋势:通过
performance_schema
或第三方工具(如 Prometheus)监控死锁频率,及时发现异常。
总结
解决 MySQL 死锁的核心原则是:减少锁冲突概率 + 快速处理不可避免的冲突。通过统一操作顺序、优化事务和索引、调整隔离级别等预防措施,结合日志监控和重试机制,可有效降低死锁对业务的影响。