欢迎关注公众号 【11来了】 ,持续 MyBatis 源码系列内容!
在我后台回复 「资料」 可领取
编程高频电子书
!
在我后台回复「面试」可领取硬核面试笔记
!文章导读地址:点击查看文章导读!
感谢你的关注!
面试官:MySQL 什么时候会出现死锁问题?为什么不推荐使用RR隔离级别?
MySQL 的死锁问题比较容易在面试中碰到,接下来将会模拟 MySQL 中的死锁现象,通过查看 MySQL 死锁日志来摸清死锁产生原因,并且从死锁产生原因来了解为什么不推荐使用 RR (可重复读)事务隔离级别?
删除数据死锁场景模拟
创建表结构
CREATE TABLE `goods` (
`id` int(11) NOT NULL,
`num` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `goods` VALUES (1, 15);
INSERT INTO `goods` VALUES (5, 30);
模拟死锁
可以在 Navicat 客户端打开两个窗口,按照下列顺序执行对应 SQL 语句,即可模拟出死锁现象
事务 A | 事务 B |
---|---|
start transaction; | |
delete from goods where id = 1; | start transaction; |
delete from goods where id = 5; | |
delete from goods where id = 5; | |
delete from goods where id = 1; # 死锁 |
如下,可以用鼠标选中要执行的语句,按照上述顺序执行特定语句:
死锁日志查看
当执行完事务 B 的最后一个语句,Navicat 就会提示死锁
接下来查看死锁的日志,在 MySQL 可以通过 show engine innodb status;
来查看 InnoDB 存储引擎的状态信息,包含了事务、锁等信息
如下图:
接下来将 Status 里的数据粘贴到 Sublime(文本编辑器)中,方便分析日志,如下图,锁的一些信息主要在下方黄色方框内部:
接下来逐个分析,可以看到总共有两个事务,事务 ID 分别为 9511、9512,接下来分别看这两个事务相关的锁信息,先看第一个事务,可以发现第一个事务在等待 id = 5 这一条数据的 X 锁
接下来看一下第二个事务,该事务持有了 id = 5 这条数据的 X 锁,同时在等待 id = 1 这条数据的 X 锁:
由于事务 A 和事务 B 都互相等待对方的锁,因此发生了死锁,通过日志可以看到最后是回滚了第二个事务:
锁日志含义
在使用 show engine innodb status;
查看存储引擎状态时,每一个锁信息都有 4 行记录,这里说一下每条记录的含义:
Record lock, heap no 4 PHYSICAL RECORD: n_fields 4; compact format; info bits 32
0: len 4; hex 80000005; asc ;; # 聚集索引的值
1: len 6; hex 000000002528; asc %(;; # 事务 ID
2: len 7; hex 78000001bd113d; asc x =;; # undo 回滚段指针
3: len 4; hex 8000001e; asc ;; # 非主键字段值
每个字段的含义在上边已经给出了,第一行记录是 聚集索引的值 ,即 5,那么也就是 id = 5 这一条记录
最后一行记录的值是非主键字段的值,即 1e
,翻译为十进制也就是 30,也就是 id = 5,非主键索引值为 30 这一条记录,如下:
RR 事务隔离级别下造成的死锁
一般在互联网公司中,都不推荐使用 MySQL 的 可重复读 隔离级别,而是更推荐使用 读已提交 隔离级别
原因: 这是因为在 RR 隔离级别下的 间隙锁 容易造成锁等待或死锁,因为 RR 隔离级别需要保证可重复读,MySQL 通过 MVCC + 间隙锁来保证可重复读,如果由于 SQL 语句写的不合适,加的 间隙锁范围过大 ,就会导致在间隙锁范围内无法插入数据,造成锁等待或者死锁
接下来将会模拟 RR 隔离级别下的 间隙锁和插入意向锁冲突 ,从而造成死锁的案例以及对应的日志分析
创建表结构
CREATE TABLE `goods` (
`id` int(11) NOT NULL,
`num` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `goods` VALUES (1, 15);
INSERT INTO `goods` VALUES (5, 30);
模拟死锁
如下,同样开启两个 mysql 窗口,按如下顺序执行两个事务,即会发生死锁现象
事务 A | 事务 B |
---|---|
start transaction; | |
delete from goods where id = 2; | start transaction; |
delete from goods where id = 4; | |
insert into goods values(2, 30) | |
insert into goods values(4, 30); # 死锁 |
接下来同样查看死锁日志: show engine innodb status;
死锁日志分析
当执行事务 A 在执行 delete from goods where id = 2
时,由于不存在 id = 2 的数据,因此会在 id 范围(1,5)上添加间隙锁
当事务 A 在执行 insert into goods values(2, 30)
时,如下图,该语句的插入意向锁会与事务 B 持有的范围(1,5)的间隙锁冲突,如下:
对于事务 B 来说,执行 delete from goods where id = 4
时,由于不存在 id = 4 的数据,因此会在 id 范围(1,5)上添加间隙锁
当事务 B 在执行 insert into goods values(4, 30)
时,插入意向锁会与事务 A 持有的范围(1,5)的间隙锁冲突,如下:
因此就会导致死锁现象,上边还是有些绕,最后再简单总结一下,造成死锁的流程为:
- 事务 A 先执行
delete from goods where id = 2
,由于并不存在 id = 2 的数据,因此事务 A 会对(1,5)添加间隙锁 - 事务 B 执行
delete from goods where id = 4
,同样不存在 id = 4 的数据,因此事务 B 会对(1,5)添加间隙锁 - 这里事务 A 和事务 B 的间隙锁并不会冲突,因为他是用来防止在间隙中插入新值的,因此会和插入意向锁冲突
- 之后,事务 A 执行
insert into goods values(2, 30)
,此时会去申请插入意向锁,但是 id = 2 是在范围(1,5)内的,因此该意向锁会和事务 B 持有的(1,5)间隙锁冲突,发生锁等待 - 之后,事务 B 执行
insert into goods values(4, 30)
,此时就发生了死锁,因为事务 B 申请了 id = 4 的插入意向锁,同样和事务 A 的间隙锁冲突
通过上边两个案例,就可以了解 MySQL 中死锁出现的现象、如何去查看死锁以及为什么不推荐使用 RR 隔离级别
MySQL 中如何查看事务加锁的信息?
需要三步:
- 设置参数:
set global innodb_status_output_locks = ON;
- 开启事务,并加锁
- 查看锁信息:
show engine innodb status;
接下来演示一下,在 Navicat 打开一个查询窗口,按照顺序执行上边三个步骤:
最后一步会打印出来 innodb 引擎中的所有状态信息,包含了锁信息,如下:
总共获取锁的步骤为:
1、在获取表中某行数据的独占锁之前,会先获取表的 IX 锁
2、在最大索引后边加上间隙锁,避免在 RR 隔离级别下发生幻读
3、表中只有两条记录 id = 1、id = 5,因此会在两条记录上添加 X 锁,即临键锁
Lock Mode 对应含义
在使用 show engine innodb status
查看锁时,有很多 lock mode IX 等等,列举一下锁模式对应的含义:
- IX:代表意向排他锁
- X:代表Next-Key Lock锁定记录本身和记录之前的间隙(X)
- S:代表Next-Key Lock锁定记录本身和记录之前的间隙(S)
- X, REC_NOT_GAP:代表只锁定记录本身(X)
- S, REC_NOT_GAP:代表只锁定记录本身(S)
- X, GAP:代表间隙锁,不锁定记录本身(X)
- S, GAP:代表间隙锁,不锁定记录本身(S)
- X, GAP, INSERT_INTENTION:代表插入意向锁