likes
comments
collection
share

为什么大多数公司使用MySql的事务隔离级别是RC?

作者站长头像
站长
· 阅读数 36

本文所有的前提是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);

为什么大多数公司使用MySql的事务隔离级别是RC?

2、在RC级别下演示幻读

为什么大多数公司使用MySql的事务隔离级别是RC?

3、快照读演示

时刻事务A事务B
T1begin; select * from t where id=6;
T2begin; insert into t values(6,6,6);
T3select * from t where id=6;
T4commit;
T5select * from t where id=6; commit;

在RC隔离级别下,事务A三次查询select * from t where id=6的结果如下图:

为什么大多数公司使用MySql的事务隔离级别是RC? 简单说明下:

  • 在时刻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
T1begin; select * from t where id=6 for update;
T2begin; insert into t values(6,6,6);
T3select * from t where id=6 for update;
T4commit;
T5select * 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
为什么大多数公司使用MySql的事务隔离级别是RC?为什么大多数公司使用MySql的事务隔离级别是RC?

在读已提交事务(RC)隔离级别下,是存在幻读的,当前读和快照读都无法避免幻读。在RC下,只能对需要加锁的数据加行锁,只能对已经存在的记录行进行加锁,而插入的是一个没有存在的新的记录行,所以无法避免幻读。

四、RR是怎么解决幻读的

在可重复读的事务隔离级别下,快照读是没有幻读的问题(MVCC保证在一个事务里可重复读的结果一致);对于当前读,使用间隙锁(Gap Lock)和临键锁(Next-key Lock)来解决幻读。

  • 间隙锁,锁定索引记录之间的间隙,用开区间来表示。
  • 临键锁,间隙锁和行锁组成临键锁,前开后闭区间来表示

4.1 案例

针对上面相同表,首先将事务隔离级别修改为repeatable read:

set global transaction isolation level repeatable read;

为什么大多数公司使用MySql的事务隔离级别是RC? 然后清除表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;

为什么大多数公司使用MySql的事务隔离级别是RC?

时刻事务A事务B
T1begin; select * from t where id=6 for update;
T2begin; insert into t values(6,6,6);

在T2时刻,事务B会被阻塞,因为事务A在时刻T1查询id=6时没有数据无法加行锁,进而退化加了间隙锁(5,10),所以只有当事务A提交后,插入才能成功,避免了幻读的出现。

4.2 死锁案例

时刻事务A事务B
T1begin; select * from t where id=12 for update;
T2begin; select * from t where id=13 for update;
T3insert into t values(12,12,12)
T4insert 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?

从以下两个方面来看,

  1. 使用RR事务隔离级别,能避免幻读,但是由于引入间隙锁导致加锁的范围可能扩大,从而会影响并发,还容易造成死锁,因为间隙锁和间隙锁是不冲突的;
  2. 在大多数业务场景下,事务隔离级别RC基本上能满足业务需求,幻读出现的机率较少;

从够用的角度来看,选择RC隔离级别是可以的。

转载自:https://juejin.cn/post/7010372915002605604
评论
请登录