百木园-与人分享,
就是让自己快乐。

浅谈事务隔离级别、MVCC及相关特性

文采不是太好,应该会有地方表达不清楚,烦请指正。
 
需要事先准备测试表:
CREATE TABLE `test` (
`id` int(11) NOT NULL,
`name` varchar(10) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
插入数据:
insert into test values(1,\'张一\',20),(3,\'王三\',10),(4,\'李四\',34);
 
一、ACID特性
说起事务我们肯定会先想到事务的ACID四大特性:Atomicity、Consistency、Isolation、Durability
A(Atomicity):原子性,对事务的状态要么全部提交,要么全部回滚,没有中间状态;
C(Consistency):一致性,事务开始前和结束后,数据库的完整性没有被破坏;
I(Isolation):隔离性,允许多个并发事务同时对数据库操作;
D(Durability):持久性,事务提交后,对数据的修改就是永久的。
今天主要介绍的就是I(隔离性)。
 
二、MySQL隔离级别
在说隔离级别之前先简单介绍一下读取操作的几种方式:
①普通读取,也就是普通的select查询操作;
②锁定读取,也就是select ... lock in share mode; select ... for update; update等;
注意,该篇文章只针对于普通的读取,不涉及锁定读操作,因为锁定读会涉及到锁的相关信息,本文先不做过多的解释与验证。
mysql中隔离级别有如下几类:
READ UNCOMMITTED(RU)、READ COMMITTED(RC)、REPEATABLE READ(RR)、SERIALIZABLE
其实我们根据单词的意思就能知道这4种隔离级别的表面的含义了:RU读未提交、RC读已提交、RR可重复读、SERIALIZABLE串行化。每个隔离级别都有自己的特点,可根据实际业务需求情况来进行选择,5.7版本默认的是RR隔离级别。
为什么叫隔离级别呢?
根据个人的理解,主要是对不同事务之间的相互影响程度所采取的一种措施,根据级别的由低到高(RU-->RC-->RR-->SERIALIZABLE),所产生的影响程度不断提高(也就是并发度越来越低,效率越来越低),可以说每种隔离级别都有自己的特点,在介绍隔离级别的特点的时候,我们先来理解几个名词的概念:
①脏读:读到了其他未提交事务的数据,但是这个事务有可能提交也有可能回滚,如果回滚的话是不会存储到数据库中的,所以也可以说是读到了可能不存在的数据。
②不可重复读:同一个事务读取同一个数据,侧重的是修改,在事务开始的时候读到的数据和事务结束之前任意时刻读到的数据不一致。
③幻读:其实在幻读和不可重复读之间有一些不太好理解,不可重复读侧重的是两次读取操作的结果不同(该事务读取到了其他事务修改后并提交的数据)。而幻读更侧重的是插入,就是读取到了之前没有读到过的数据,比如A事务先读取某个范围内的数据,此时B事务在该范围内插入数据并提交,而A事务再次读取该范围内的数据的时候发现和第一次读取的不一致了,发现数据多了,读到了之前没有读到的数据。
在知道了上面几个概念之后,我们来分别测试一下在不同的隔离级别下,都有哪些特点。
 
1、RU隔离级别(读未提交):
读未提交意思就是说,一个事务在没有提交的时候,它所做的修改就会被其他事务看到。该隔离级别下会产生脏读、幻读和不可重复读。通过开启不同的事务来进行测试:
首先修改隔离级别为RU:set global transaction isolation level read uncommitted;
①脏读:

Session A Session B
begin; begin;
select * from test;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 张一   |   20 |
|  3 | 王三   |   10 |
|  4 | 李四   |   34 |
+----+--------+------+
 
 
 
insert into test values(2,\'张二\',22);
select * from test;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 张一   |   20 |
|  2 | 张二   |   22 |
|  3 | 王三   |   10 |
|  4 | 李四   |   34 |
+----+--------+------+
 
  rollback;
select * from test;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 张一   |   20 |
|  3 | 王三   |   10 |
|  4 | 李四   |   34 |
+----+--------+------+
 

②不可重复读:

Session A Session B
begin; begin;
select * from test where id=1;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 张一   |   20 |
+----+--------+------+
 
 
 
update test set age=25 where id=1;
  commit;
select * from test where id=1;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 张一   |   25 |
+----+--------+------+
 

③幻读:

Session A Session B
begin; begin;
select * from test where id>3;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  4 | 李四   |   34 |
+----+--------+------+
1 row in set (0.01 sec)
insert into test values(6,\'王六\',66);
  commit;
select * from test where id>3;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  4 | 李四   |   34 |
|  6 | 王六   |   66 |
+----+--------+------+
 

2、RC隔离级别(读已提交):
读已提交,意思就是一个事务提交之后,其他的事务才能看到它所做的变更。该隔离级别下不会产生脏读,但是会产生幻读和不可重复读。通过开启不同的事务来进行测试:
首先修改隔离级别为RC:set global transaction isolation level read committed;
①脏读:

Session A Session B
begin; begin;
select * from test;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 张一   |   20 |
|  3 | 王三   |   10 |
|  4 | 李四   |   34 |
+----+--------+------+
 
 
 
insert into test values(2,\'张二\',22);
select * from test;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 张一   |   20 |
|  3 | 王三   |   10 |
|  4 | 李四   |   34 |
+----+--------+------+
 

可以看到在session B中的事务未提交的时候,它所做的更改在session A中的事务始终是不可见的,所以不存在脏读。
②不可重复读:

Session A Session B
begin; begin;
select * from test where id=1;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 张一   |   20 |
+----+--------+------+
 
 
update test set age=25 where id=1;
  commit;
select * from test where id=1;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 张一   |   25 |
+----+--------+------+
 

③幻读:

Session A Session B
begin; begin;
select * from test where id>3;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  4 | 李四   |   34 |
+----+--------+------+
1 row in set (0.01 sec)
insert into test values(6,\'王六\',66);
  commit;
select * from test where id>3;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  4 | 李四   |   34 |
|  6 | 王六   |   66 |
+----+--------+------+
 

3、RR隔离级别(可重复读):
可重复读,意思就是在事务开始和结束期间,看到的数据是一致的,该事务在期间修改的数据,在事务未结束之前对其他事务也是不可见的。该隔离级别下不会产生脏读和不可重复读,但是可能会发生幻读,通过开启不同的事务来进行测试,这里只测试幻读的情况:
首先修改隔离级别为RR:set global transaction isolation level repeatable read;
①不会产生幻读的情况:

Session A Session B
begin; begin;
select * from test where id>3;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  4 | 李四   |   34 |
+----+--------+------+
1 row in set (0.01 sec)
insert into test values(6,\'王六\',66);
  commit;
select * from test where id>3;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  4 | 李四   |   34 |
+----+--------+------+
 

可以看到在RR隔离级别下,只是在普通读取过程中不会产生幻读的情况,因为Session A中的事务在其他事务提交前后都是读取到的相同的记录,并没有读取到新增的记录。
②会产生幻读的情况:

Session A Session B
begin; begin;
select * from test where id>3;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  4 | 李四   |   34 |
+----+--------+------+
1 row in set (0.01 sec)
insert into test values(6,\'王六\',66);
  commit;
update test set age=68 where id=6;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
 
select * from test where id>3;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  4 | 李四   |   34 |
|  6 | 王六   |   68 |
+----+--------+------+
2 rows in set (0.00 sec)
 

该普通读取产生了幻读,是因为在Session A中的事务执行了一次update操作,该update操作属于是当前读,其他事务已经提交过的sql,在该事务中是可以被当前读所读到的。但是在RR隔离级别下是可以加锁来解决幻读的。
 
4、SERIALIZABLE隔离级别(串行化):
该隔离级别下,是通过读加读写,写加写锁来阻止脏读、幻读、不重复读的发生,这里就不做相关实验了,感兴趣的可以修改隔离级别来进行测试。
 
三、MVCC
上面的实验都是基于普通的select语句进行的,由于其他的当前读会涉及到比较繁琐的加锁过程,所以本文没有阐述。
普通的select语句在RC和RR隔离级别下属于快照读,这就是由MVCC所控制了,MVCC只在RC和RR隔离级别下存在。
1、undo链:
MVCC又称为多版本并发控制,就是在每行记录中会添加行的创建版本号和过期版本号:
①trx_id:该id就是当前事务的id,对记录进行操作的时候都会把当前事务id赋值给trx_id;
②roll_ptr:是回滚指针,指向undo的,当某条记录被修改后,该记录的旧版本会写入undo,此时该记录的roll_id会指向对应的undo。
比如我们此时在test表中插入一条记录,假设此时版本号为10:
insert into test values(6,\'王六\',66); //其实insert undo信息只是起到的回滚的作用,当事务提交后就会删除了。

6 \'王六\' 66 10 roll_ptr(指向对应的undo)

比如我们在两个事务中update一条记录,假设该记录开始版本号为15:

Session A Session B
begin;  假设该事务id为20  
update test set age=13 where id=3;
 
 
commit;  
  begin; 假设该事务id为40
  update test set age=16 where id=3;
  commit;

此时应该是这样的:

这样的话就形成了一个undo版本链儿。
注意上面的箭头并不是代表指向的某个字段记录,而是在undo中的整条记录。
2、read view:
我们通过上面的一些测试,知道了RC和RR隔离级别下都是其它事务提交之后才能读到,那没有提交的时候,其它事务是如何进行读取历史的版本的呢?这就产生了read view(读视图)。
read view中几个比较重要的部分:当前活跃的事务id列表、活跃事务id列表中最小活跃的事务id(记为min_id)、当前的事务id(记为creat_id)、下一个事务id号(max_id)。
这里要知道事务id是递增的,只有在事务真正修改记录的时候才会被分配。
那么在访问数据的时候就会有如下判断:
①被访问的记录的trx_id是否和creat_id一致,如果一致的话,说明是在当前事务中进行读取的,也就是访问的自己本身;
②如果被访问的记录的trx_id小于min_id,表明该记录修改已经在当前事务生成read view之前提交了,也是可以被访问到的;
③如果被访问的记录的trx_id大于max_id,代表该事务是在当前事务生成read view之后开启的,是不能访问的;
④如果被访问的记录的trx_id在min_id 和 max_id之间,那么就要判断一下该事务id是不是在活跃事务id列表中,如果存在的话,说明生成当前事务的read view的时候该事务还是活跃的,是不可见的。如果不存在的话,说明在生成当前事务的read view的时候该事务已经提交了是可见的。
3、RC、RR中的read view:
在RC和RR中生成read view是有些不同的,主要是生成的时间点不同。
1)RC隔离级别下是每次读取数据的时候都会生成一个read view

Session A Session B
begin;  假设该事务id为20,未提交 begin; //假设该事务id为40,执行了一些操作但是未提交
update test set age=13 where id=3;
 
。。。。
update test set age=16 where id=3; 。。。。

此时对应undo链如下:

当Session C事务在RC隔离级别下,去读取id=3的这条记录,那么它读取出来的是trx_id为15的这条版本记录。
原因:
①在Session C事务进行读取的时候,首先生成read view,该read view中包含一个当前活跃的事务id列表也就是[20,40],min_id是20,max_id是41,creat_id是0;
②Session C的事务只是读取,并没有修改,所以creat_id为0,最新的事务版本id是20,存在于当前活跃的链表中所以看不到该条记录;
③继续沿着undo链表往下走,trx_id还为20,还在活跃的链表中,所以也读不到;
④继续沿着undo链表往下走,trx_id为15,小于min_id 20所以可见。
继续测试,将SessionA中的事务进行提交,在Session B中修改该记录:

Session A Session B
begin;  假设该事务id为20,未提交 begin; //假设该事务id为40,执行了一些操作但是未提交
update test set age=13 where id=3;
 
。。。。
update test set age=16 where id=3; 。。。。
commit; update test set age=19 where id=3;

对应的undo链就是:

继续在Session C的事务中去读取id=3的这条记录,那么它读取出来的是trx_id为20的这条记录版本。
因为RC每次读取都会生成新的read view,此时的read view中当前活跃事务id列表也就是[40],min_id是40,max_id是41,creat_id是0。该记录的最新事务版本id是40,在活跃列表中,所以是可以不可以读到的。继续往下走事务id是20不在活跃列表中,是可以读到的。
2)RR隔离级别下是在事务开始的时候都一个read view,在事务结束之前都会去读取该read view
还是以上面的例子为例:

Session A Session B
begin;  假设该事务id为20,未提交 begin; //假设该事务id为40,执行了一些操作但是未提交
update test set age=13 where id=3;
 
。。。。
update test set age=16 where id=3; 。。。。

此时对应undo链如下:

当Session C事务在RR隔离级别下,去读取id=3的这条记录,那么它读取出来的是trx_id为15的这条版本记录。
原因(截止到现在RR隔离级别下和RC是一样的原理):
①在Session C事务进行读取的时候,首先生成read view并且之后都会使用该read view,该read view中包含一个当前活跃的事务id列表也就是[20,40],min_id是20,max_id是41,creat_id是0;
②Session C的事务只是读取,并没有修改,所以creat_id为0,最新的事务版本id是20,存在于当前活跃的链表中所以看不到该条记录;
③继续沿着undo链表往下走,trx_id还为20,还在活跃的链表中,所以也读不到;
④继续沿着undo链表往下走,trx_id为15,小于min_id 20所以可见。
继续测试,将SessionA中的事务进行提交,在Session B中修改该记录:

Session A Session B
begin;  假设该事务id为20,未提交 begin; //假设该事务id为40,执行了一些操作但是未提交
update test set age=13 where id=3;
 
。。。。
update test set age=16 where id=3; 。。。。
commit; update test set age=19 where id=3;

对应的undo链就是:

继续在Session C的事务中去读取id=3的这条记录,那么它读取出来的还是trx_id为15的这条记录版本。
因为RR还在使用的是事务开始时候的read view,此时的read view中当前活跃事务id列表也还是[20,40],min_id是20,max_id是41,creat_id是0。该记录的最新事务版本id是40,在活跃列表中,所以是可以不可以读到的。继续往下走事务id是20还在活跃列表中,是不可以读到的,直到读到事务id是15的时候才可以读到。


来源:https://www.cnblogs.com/ordinarydba/p/16284617.html
本站部分图文来源于网络,如有侵权请联系删除。

未经允许不得转载:百木园 » 浅谈事务隔离级别、MVCC及相关特性

相关推荐

  • 暂无文章