文章分为两部分
- 事务隔离级别简介
- MVCC的实现原理
事务
事务所带来的问题
首先看一下在一个事务中一组SQL执行会出现哪些问题
-
脏读--即一个事务读到另一个事务未提交的数据。假设事务A和事务B的隔离级别都设置为
read uncommitted事务A 事务B Begin -- 开启事务 Begin -- 开启事务 update tableA set col1 = val1 where id = 1 -- 设置col1列值为val1 update tableA set col1 = val2 where id = 1 -- 设置col1列值为val2 此时事务A与事务B都未提交,col1列的值为val2,而事务A修改的值被覆盖了,这会到很严重的问题。
-
不可重复读--即一个事务读到另一个事务提交过的数据,假设事务A和事务B的隔离级别都设置为
read committed事务A 事务B Begin -- 开启事务 Begin -- 开启事务 update tableA set col1 = val1 where id = 1 -- 设置col1列值为val1 update tableA set col1 = val2 where id = 1 -- 设置col1列值为val2 commit; -- 提交事务 select col1 from tableA where id = 1; -- 查出来的值为val2 事务A未提交,事务B已提交,执行
select col1 from tableA where id = 1;查询出来的值是事务B修改过后的值。也就是说事务B更改的值,在提交过后,对事务A也可见。如果是银行应用的场景,小明正在ATM机前取钱,查看存款还有10000元,正准备取钱的时候,小明老婆在另一台取款机上取款10000元,小明的ATM机提示余额不足。显然在这种业务场景下是有问题的。 -
幻读--一个事务读到另一个事务插入的数据。
事务A 事务B Begin -- 开启事务 Begin -- 开启事务 select * from tableA; insert into tableA(id) values(10) commit; -- 提交事务 select * from tableA; -- 会把事务B插入的新记录也查询出来
隔离级别
为了解决事务带来的如上4个问题,SQL标准中事务分为四个级别来支持解决不同的问题。
read uncommitted——可能会出现脏读、重复度、幻读三种问题read committed——可能会出现重复读、幻读两种问题,但不会出现脏读的问题repeatable read——可能会出现幻读的问题,但不会出现脏读、可重复读的问题serializable——串行化执行SQL,不会出现上述的问题。
各个数据库厂商对SQL标准的实现有所不同,比如Oracle只有两种事务隔离级别;而MySQL的事务隔离级别和SQL标准略有不同,MySQL也实现了4种事务隔离级别,但是在repeatable read隔离级别下,不会发生幻读的问题,MySQL的默认隔离级别就是repeatable read。
至于怎么在reaptable read隔离级别下解决的幻读问题,得从事务开启时说起。得益于ReadView这种结构
ReadView —— 它是事务隔离级别实现的秘密
前置知识:一条记录中除了存储数据外,还额外的保存了两部分信息 。
- trx_id:事务ID
- roll_pointer:指向undo页面的回滚指针
版本链中就是由一条记录的各个历史数据串联成的一条链
MySQL在使用begin语句或者其他方式开启事务的时候,会在某个时机创建一个结构体(可以理解为Java中的一个对象)——它的名字我们把它叫做ReadView。这个ReadView对象里面包含哪些属性呢?
- 当前事务ID:creator_trx_id
- 活跃的事务ID列表:m_ids
- 该记录版本链中最大的事务ID:max_trx_id
- 该记录版本链中最小的事务ID:min_trx_id
-
对于
read uncommitted级别只需要事务读取最新的版本链中的数据即可 -
对于
read committed和reaptable read级别只需要控制版本链中的记录可见性。read committed读取版本链中最新的数据即可reaptable read只需要读取当前事务创建ReadView之前的已提交的最新的数据即可
-
serializable不存在任何问题,不解释了
read committed
假设有一张表:tableA,就一个字段(为了方便)。tableA的建表语句,与表中记录
create tableA{
id int primiry key
}
select * from tableA;
> 1
并且表中只有一条记录。该记录的最初版本链如下
id: 1, trx_id: 0
现在使用两个事务分别对这条记录进行操作。隔离级别为read committed
| 事务A | 事务B |
|---|---|
| begin | begin |
| select * from tableA where id = 1; | |
| update tableA set id = 2 where id = 1; | |
| commit; | |
| select * from tableA whree id = 1; |
更新完后,ID等于1的版本链记录就是:
id: 2 : trx_id: 200
id: 1 , trx_id: 0
如上事务A和事务B同时开启,事务A先查询ID为1的记录,此时假设能查到一条记录;事务B更新了ID为1的记录,并且提交。事务A再次查询ID为1的记录,那么它还能查询到这条记录吗?换言之事务B的更新提交后对事务A可见吗?。在MySQL内部是这样进行查询的。每次查询前都会生成一个ReadView。事务A第二次查询时也会生成一个ReadView。假设事务A的事务ID为100,事务B的事务ID为200。
事务A生成的ReadView中的属性就是:
- 当前事务ID:100
- 活跃事务ID:100
- 最大事务ID:201
- 最小事务ID:100
第一步:拿记录的trx_id和readView中的当前事务ID比较,如果相等,表示该对该事务都是可见的。如果不相等,表明是其他事务修改该记录。当前例子显然该记录最新的事务ID是200,不是当前事务ID
第二步:拿记录的trx_id与最大事务ID比较,如果大于等于最大事务ID,表示该版本是新事务开启的readView,并且对当前事务不可见。当前例子显然该记录最新版本的事务ID小于最大事务ID201,继续第三步进行判断
第三步:拿记录的trx_id与活跃的事务ID列表比较,如果在活跃的事务列表中,表明事务未提交,不符合可见性,继续找下一条记录。如果不在,说明事务已经被提交,其他事务可以看到该记录。当前例子显然事务B已经提交,200不在活跃的事务ID列表中。所以符合可见性要求,会直接返回版本链中这条记录。当然,第四步也不再判断了。否则重复这四步判断,直到有符合的记录返回给存储引擎。否则查询得到的返回记录就是空
第四步:trx_id与最小事务ID比较,如果小于最小事务ID,表示该记录已经被提交。符合可见性要求,就会直接返回该记录。
reaptable read
它和read committed的区别就在于生成ReadView的时机不同。
reaptable read只会在第一次查询的时候生成ReadView,并且整个事务在提交前只使用这一个。
而read committed会在每次执行查询前生成一个最新的ReadView。
判断的过程和read committed的步骤一模一样。不再赘述。只是生成ReadView的时机不同。