MySQL死锁排查和解决方法,快速定位死锁
前言
做后端开发的同学肯定都遇到过死锁:两个事务互相等对方释放锁,结果都卡住了,数据库报错"Deadlock found when trying to get lock"。这时候整个业务都卡住了,用户都在骂娘,你急得满头大汗不知道怎么办。
很多新手同学遇到死锁就慌了,不知道怎么排查,也不知道怎么解决。其实死锁不可怕,只要掌握了正确的排查方法,很快就能定位到问题。这篇文章就把MySQL死锁的知识点从头到尾讲清楚:什么是死锁?怎么查看死锁?怎么排查?怎么避免?看完之后你就彻底搞懂了。
一、什么是死锁
死锁就是两个或者多个事务,互相持有对方需要的锁,又互相等对方释放锁,结果谁也动不了,就卡住了。
举个最简单的例子:
- 事务A锁了id=1的行,想要锁id=2的行
- 事务B锁了id=2的行,想要锁id=1的行
- 这时候A等B释放id=2的锁,B等A释放id=1的锁
- 两个事务互相等,就死锁了
MySQL检测到死锁之后,会自动回滚其中一个事务,让另一个事务继续执行。所以死锁不会一直卡住,但是被回滚的那个事务就失败了,业务上会报错。
二、死锁产生的原因
死锁一般都是因为这几种情况:
1. 多个事务以不同的顺序访问同一批资源
这是最常见的死锁原因。比如两个事务,一个先操作表A再操作表B,另一个先操作表B再操作表A,就容易死锁。
2. 事务太长,持锁时间久
如果一个事务里做了很多事情,持锁时间很长,其他事务都在等锁,时间一长就容易死锁。
3. 没有用到索引,锁了太多的行
如果WHERE条件没有用到索引,InnoDB就会扫描全表,把所有行都锁了,这样锁的范围很大,就容易和其他事务的锁冲突,产生死锁。
4. 间隙锁导致的死锁
间隙锁虽然是为了解决幻读,但是也会增加死锁的概率。两个事务都往同一个范围里插数据,间隙锁互相冲突,就容易死锁。
三、怎么查看死锁
MySQL默认会自动检测死锁,并且把死锁的信息记录下来。我们可以用下面的命令查看最近一次死锁的信息:
SHOW ENGINE INNODB STATUS;
执行完之后,会输出一大段信息,找到里面的 LATEST DETECTED DEADLOCK 部分,就是最近一次死锁的详细信息。
这个部分会告诉你:
- 哪个事务持有了什么锁
- 哪个事务在等什么锁
- 最后MySQL回滚了哪个事务
开启死锁日志
如果想要记录所有的死锁信息,不仅是最近一次,可以开启死锁日志:
# my.cnf里配置
innodb_print_all_deadlocks = 1
开启之后,所有的死锁信息都会写到MySQL的错误日志里。
四、怎么排查死锁
拿到死锁日志之后,怎么分析呢?一般按这个步骤来:
步骤1:看两个事务分别执行了什么SQL
死锁日志里会显示两个事务的SQL语句。你要看看这两个SQL分别操作了哪些表、哪些行。
步骤2:看锁的信息
日志里会显示每个事务持有了什么锁,在等什么锁。你要搞清楚:
- 事务A持有了哪些行的锁
- 事务A想要拿哪些行的锁
- 事务B持有了哪些行的锁
- 事务B想要拿哪些行的锁
这样就能搞清楚为什么会死锁了。
步骤3:分析业务逻辑
看完SQL和锁的信息之后,再结合业务逻辑,看看这两个事务为什么会以不同的顺序操作同一批数据。
比如:
- 事务A是扣库存的逻辑
- 事务B是修改订单的逻辑
- 它们都操作了商品表和订单表,但是顺序不一样,就死锁了
五、怎么避免死锁
排查完死锁之后,最重要的是怎么避免下次再出现。常见的避免死锁的方法:
1. 按相同的顺序访问资源
这是最有效的方法。所有的事务都按相同的顺序访问表和行,比如所有事务都先操作商品表,再操作订单表,就不会出现两个事务互相等的情况了。
2. 大事务拆成小事务
事务越小,持锁时间越短,死锁的概率就越低。把大事务拆成多个小事务,每个事务只做一点点事情。
比如不要在一个事务里调用RPC接口、查Redis、做复杂计算,这些耗时操作都放到事务外面。
3. 尽量用索引访问数据
如果WHERE条件没有用到索引,就会扫描全表,把所有行都锁了,这样锁的范围很大,死锁概率就高。一定要确保查询都用到了索引。
4. 降低隔离级别
如果对幻读不是很敏感,可以把隔离级别从可重复读(RR)降到读提交(RC)。这样间隙锁就没了,死锁的概率也会低很多。
很多互联网公司都是用RC隔离级别,就是为了减少锁冲突和死锁。
5. 设置锁等待超时时间
如果真的出现死锁了,不要让事务一直等。可以设置锁等待超时时间:
SET GLOBAL innodb_lock_wait_timeout = 5;
这样如果一个事务等锁超过5秒,就自动报错,不会一直卡住。
六、死锁和锁等待的区别
很多同学分不清死锁和锁等待:
锁等待
一个事务等另一个事务释放锁,但是不存在循环等待,等一会儿就好了。
比如事务A锁了id=1的行,事务B也想锁id=1的行,那B就要等A释放锁,这就是锁等待。
死锁
两个事务互相等对方释放锁,形成了循环等待,谁也动不了。
MySQL会自动检测到死锁,然后回滚其中一个事务。
常见坑
坑1:遇到死锁就重启MySQL
很多同学一遇到死锁就慌了,直接重启MySQL。这是最笨的方法,重启解决不了根本问题,下次还会出现。一定要先排查死锁的原因,从根本上解决。
坑2:以为死锁是MySQL的bug
很多同学觉得死锁是MySQL的问题,其实不是。死锁一般都是业务代码写的有问题,多个事务访问资源的顺序不一致导致的。MySQL只是帮你检测到了死锁,罪魁祸首是你的业务代码。
坑3:死锁日志里的信息看不懂
很多同学拿到死锁日志,里面全是锁的信息,看不懂。其实不用看那么细,重点看两个事务分别执行了什么SQL,操作了哪些表和行,搞清楚它们为什么会互相等就行。
坑4:只靠MySQL自动检测死锁
很多同学觉得MySQL会自动检测死锁、自动回滚,就不用管了。其实不是,死锁虽然不会一直卡住,但是被回滚的那个事务业务上会报错,影响用户体验。最好还是从源头上避免死锁。
坑5:间隙锁导致的死锁
很多同学在可重复读隔离级别下,两个事务往同一个范围插数据,结果死锁了。这就是间隙锁的问题。如果对幻读不是很敏感,降到读提交隔离级别就好了。
总结
MySQL死锁的核心知识点:
- 死锁是什么:两个事务互相持有对方需要的锁,又互相等对方释放,形成循环等待
- 死锁产生的原因:访问资源顺序不一致、事务太长、没用索引锁太多行、间隙锁
- 怎么查看死锁:SHOW ENGINE INNODB STATUS,看LATEST DETECTED DEADLOCK部分
- 怎么排查死锁:看两个事务的SQL、看锁的信息、结合业务逻辑分析
- 怎么避免死锁:
- 按相同的顺序访问资源
- 大事务拆成小事务
- 尽量用索引访问数据
- 降低隔离级别到读提交
- 设置锁等待超时时间
- 死锁和锁等待的区别:锁等待是单向的,等一会儿就好;死锁是循环的,MySQL会自动回滚一个
记住:死锁不可怕,可怕的是不知道为什么会死锁。遇到死锁不要慌,先看死锁日志,找到原因,然后从业务代码上避免。
遇到问题加QQ23979811 协助处理
MySQL 锁机制详解
MySQL 锁机制详解
MySQL 锁是并发控制的核心,搞懂锁,才能避免死锁和性能问题。
1. 表锁 vs 行锁
表锁:锁整个表,开销小,并发低。
- MyISAM 用的就是表锁
- 适合读多写少的场景
行锁:锁单行数据,开销大,并发高。
- InnoDB 用的就是行锁
- 适合写多的场景
2. 共享锁 vs 排他锁
共享锁(S锁):读锁,多个读不互斥。
SELECT ... LOCK IN SHARE MODE
排他锁(X锁):写锁,一个写独占。
SELECT ... FOR UPDATE
3. 间隙锁
InnoDB 用间隙锁防止幻读。锁的是一个范围,不是具体行。
比如你查 id > 10 AND id < 20,间隙锁会把这个区间锁住,别人插不进来。
4. 死锁
两个事务互相等对方的锁,都不放,就死锁了。
避免死锁的方法:
- 按相同顺序访问表
- 事务尽量小,快速提交
- 降低隔离级别
- 加索引,减少行锁范围
5. 查看锁状态
-- 查看当前锁
SHOW OPEN TABLES WHERE In_use > 0;
-- 查看 InnoDB 锁
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
-- 查看锁等待
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
6. 行锁什么时候会变成表锁
行锁失效就会升级成表锁:
- 没用索引
- 索引失效
- 全表扫描
所以加索引不仅是为了查得快,也是为了锁得准。
总结
MySQL 锁的核心:
- InnoDB 默认行锁,MyISAM 是表锁
- 共享锁读、排他锁写
- 间隙锁防幻读
- 死锁要避免:顺序访问、小事务、加索引
搞懂这些,数据库并发问题基本能解决一半。
MySQL 事务与锁:理解隔离级别和死锁的实战指南
前言
事务和锁是 MySQL 最核心也最容易出问题的部分。很多人写了很多年 SQL,却还是搞不清隔离级别和死锁。本文用最直白的方式讲清楚。
一、事务的 ACID
事务就是一组操作,要么全部成功,要么全部失败。四个特性:
- A 原子性:一组操作是一个整体,不能拆分
- C 一致性:事务前后,数据库从一个一致状态到另一个一致状态
- I 隔离性:多个事务之间互不干扰
- D 持久性:事务提交后,数据就永久保存了
二、并发问题
多个事务同时操作数据,会出什么问题?
1. 脏读
事务 A 读到了事务 B 还没提交的数据。
事务 A:修改了余额为 1000,但还没提交
事务 B:读到了余额 1000
事务 A:回滚了,余额变回 500
事务 B:拿着 1000 的错误数据继续操作
2. 不可重复读
事务 A 两次读同一行数据,结果不一样。
事务 A:第一次读余额是 500
事务 B:修改了余额为 1000,提交了
事务 A:第二次读余额是 1000
3. 幻读
事务 A 两次查询,结果集的行数不一样。
事务 A:第一次查询 age > 20 的用户,有 10 条
事务 B:插入了一条 age = 25 的用户,提交了
事务 A:第二次查询 age > 20 的用户,有 11 条
三、四种隔离级别
MySQL 用隔离级别来解决这些并发问题:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交(READ UNCOMMITTED) | 会 | 会 | 会 |
| 读提交(READ COMMITTED) | 不会 | 会 | 会 |
| 可重复读(REPEATABLE READ) | 不会 | 不会 | 会(InnoDB 解决了) |
| 串行化(SERIALIZABLE) | 不会 | 不会 | 不会 |
MySQL 默认是可重复读(REPEATABLE READ),InnoDB 在这个级别下用间隙锁解决了幻读。
查看当前隔离级别
SELECT @@tx_isolation;
修改隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
四、锁的类型
1. 共享锁(S 锁)
读锁,多个事务可以同时持有。
SELECT ... LOCK IN SHARE MODE;
2. 排他锁(X 锁)
写锁,只有一个事务能持有。
SELECT ... FOR UPDATE;
3. 表锁 vs 行锁
- 表锁:锁住整张表,开销小,但并发低
- 行锁:锁住一行数据,开销大,但并发高
五、死锁
什么是死锁
两个事务互相等待对方释放锁。
事务 A:锁住了 id = 1,等待 id = 2
事务 B:锁住了 id = 2,等待 id = 1
两个事务都卡住了,谁也不让谁。
怎么避免死锁
- 按相同顺序访问表和行:所有事务都按 id 从小到大操作
- 事务尽量短:事务越长,锁持有时间越长
- 降低隔离级别:隔离级别越低,锁越少
- 加索引:没有索引会走表锁,更容易死锁
查看死锁
-- 查看最近一次死锁
SHOW ENGINE INNODB STATUS;
六、实战:转账
-- 开启事务
BEGIN;
-- 扣款(加行锁)
UPDATE account SET balance = balance - 100 WHERE id = 1;
-- 检查余额
SELECT balance FROM account WHERE id = 1;
-- 如果余额不够,回滚
-- ROLLBACK;
-- 加钱
UPDATE account SET balance = balance + 100 WHERE id = 2;
-- 提交
COMMIT;
七、常见坑
- 事务太长:锁持有时间太长,容易死锁
- 没有索引:行锁变表锁,并发暴跌
- 隔离级别太高:用串行化,性能很差
- 忘记提交:事务一直开着,锁一直占着
总结
MySQL 事务与锁的核心思路:
- 事务保证 ACID,一组操作要么全成功要么全失败
- 隔离级别越高越安全,但性能越差
- MySQL 默认可重复读,InnoDB 解决了幻读
- 行锁比表锁并发高,但一定要有索引
- 按相同顺序操作,能避免大部分死锁
搞懂这些,你对 MySQL 的理解就超过 80% 的开发者了。