为什么大多数公司使用MySql的事务隔离级别是RC?
本文所有的前提是MySQL的存储引擎是Innodb。
我们知道MySql的默认事务隔离级别是RR(可重复读),那么为什么大多数公司使用MySql时选择的事务隔离级别却是RC(读已提交)呢?
一、事务隔离级别
数据库的事务隔离级别有读未提交(read uncommitted)、读已提交(read committed)、可重复读(repeatable read)、串行化(serializable)。其中在读未提交隔离级别下会存在脏读的问题,串行化执行效率低下,所以一般在读提交和可重复读两种隔离级别下选择。
读已提交避免了脏读的现象,但没有解决幻读的问题;而可重复读解决了幻读的问题。
二、前置准备
2.1 环境准备
以下操作均在mysql-5.6.50版本下进行,存储引擎为InnoDB。
创建测试表t:
CREATE TABLE `t` (
`id` int NOT NULL,
`c` int DEFAULT NULL,
`d` int DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `c` (`c`)
) ENGINE=InnoDB;
2.2 相关SQL语句
-- 查询事务隔离级别
select @@tx_isolation;
-- 设置事务隔离级别
set global transaction isolation level [隔离级别];
-- [隔离级别]包括 serializable,read uncommitted,read committed,repeatable read.
-- 查看是否是自动提交
show global variables like 'autocommit';
-- 修改自动提交为关闭,方便模拟事务操作,修改完全局的属性要重新连接
set global autocommit=0
2.3 快照读与当前读
当前读,指的是读取的是当前最新数据,并且会对读取的记录加锁,加什么样的锁跟事物隔离级别相关,主要包括如下情况:
-- 当前读
select ... for update;
-- 以下更新操作也会加锁
update...;
insert...;
delete...;
快照读,指的是单纯的select语句。
三、幻读
3.1 什么是幻读?
在一个事务中,前后两次进行当前读,返回的结果不一致,需要注意的是,不一致是指后一次查询的结果有前一次查询结果所没有的数据,也就是有新的数据被插入到这个范围中。
3.2 幻读案例演示
下面在事务隔离级别为读已提交下演示幻读的案例。
1、插入数据
insert into t values (1,1,1),(5,5,5),(10,10,10),(20,20,20);
2、在RC级别下演示幻读
3、快照读演示
时刻 | 事务A | 事务B |
---|---|---|
T1 | begin; select * from t where id=6; | |
T2 | begin; insert into t values(6,6,6); | |
T3 | select * from t where id=6; | |
T4 | commit; | |
T5 | select * from t where id=6; commit; |
在RC隔离级别下,事务A三次查询select * from t where id=6的结果如下图:
简单说明下:
- 在时刻T1,此时表t中不存在id=6的数据,查询出来为空
- 在时刻T3,由于在时刻T2事务B插入一条id=6的数据,但是还未提交,所以在T3时刻事务A查询id=6的数据还是返回空
- 在时刻T5,由于在时刻T4事务B提交了,所以在时刻T5事务A查询出来的有一条数据
上面这种现象就是幻读,在RC事务隔离级别下是存在幻读的问题的,当前读与快照读都无法避免幻读。如果将上面的例子将select * from t where id=6修改为select * from t where id=6 for update,结果是怎样呢?
4、当前读演示 (执行下面的例子前需先将id=6这一行删除)
时刻 | 事务A | 事务B |
---|---|---|
T1 | begin; select * from t where id=6 for update; | |
T2 | begin; insert into t values(6,6,6); | |
T3 | select * from t where id=6 for update; | |
T4 | commit; | |
T5 | select * from t where id=6 for update; commit; |
- 时刻T1,事务A查询出的结果为空
- 时刻T2,事务B插入一条数据(6,6,6)
- 时刻T3,事务A的查询会阻塞住,因为事务B插入了id=6的数据还未提交,会加锁
- 时刻T4,事务B提交,T3时刻阻塞的事务A会返回一条结果
- 时刻T5,事务A的查询也会返回一条数据
下图为时刻T3在阻塞中的状态:
事务A | 事务B |
---|---|
![]() | ![]() |
在读已提交事务(RC)隔离级别下,是存在幻读的,当前读和快照读都无法避免幻读。在RC下,只能对需要加锁的数据加行锁,只能对已经存在的记录行进行加锁,而插入的是一个没有存在的新的记录行,所以无法避免幻读。
四、RR是怎么解决幻读的
在可重复读的事务隔离级别下,快照读是没有幻读的问题(MVCC保证在一个事务里可重复读的结果一致);对于当前读,使用间隙锁(Gap Lock)和临键锁(Next-key Lock)来解决幻读。
- 间隙锁,锁定索引记录之间的间隙,用开区间来表示。
- 临键锁,间隙锁和行锁组成临键锁,前开后闭区间来表示
4.1 案例
针对上面相同表,首先将事务隔离级别修改为repeatable read:
set global transaction isolation level repeatable read;
然后清除表t的数据,并插入初始数据,如下:
begin;
delete from t where 1=1;
insert into t values (1,1,1),(5,5,5),(10,10,10),(20,20,20);
commit;
时刻 | 事务A | 事务B |
---|---|---|
T1 | begin; select * from t where id=6 for update; | |
T2 | begin; insert into t values(6,6,6); |
在T2时刻,事务B会被阻塞,因为事务A在时刻T1查询id=6时没有数据无法加行锁,进而退化加了间隙锁(5,10),所以只有当事务A提交后,插入才能成功,避免了幻读的出现。
4.2 死锁案例
时刻 | 事务A | 事务B |
---|---|---|
T1 | begin; select * from t where id=12 for update; | |
T2 | begin; select * from t where id=13 for update; | |
T3 | insert into t values(12,12,12) | |
T4 | insert into t values(13,13,13) |
- 时刻T1,事务A加间隙锁(10,15),加锁成功
- 时刻T2,事务B加间隙锁(10,15),加锁成功
- 时刻T3,事务A要插入id=12,需要加行锁,等待事务B释放间隙锁(10,15)
- 时刻T4,事务B要插入id=13,需要加行锁,等待事务A释放间隙锁(10,15)
- 最终会导致互相等待形成死锁
间隙锁与间隙锁之间不冲突。
五、为什么很多公司选择mysql的事务隔离级别是RC?
从以下两个方面来看,
- 使用RR事务隔离级别,能避免幻读,但是由于引入间隙锁导致加锁的范围可能扩大,从而会影响并发,还容易造成死锁,因为间隙锁和间隙锁是不冲突的;
- 在大多数业务场景下,事务隔离级别RC基本上能满足业务需求,幻读出现的机率较少;
从够用的角度来看,选择RC隔离级别是可以的。
转载自:https://juejin.cn/post/7010372915002605604