MySQL的MVCC与事务隔离级别

93 阅读6分钟

文章分为两部分

  1. 事务隔离级别简介
  2. MVCC的实现原理

事务

事务所带来的问题

首先看一下在一个事务中一组SQL执行会出现哪些问题

  1. 脏读--即一个事务读到另一个事务未提交的数据。假设事务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修改的值被覆盖了,这会到很严重的问题。

  2. 不可重复读--即一个事务读到另一个事务提交过的数据,假设事务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机提示余额不足。显然在这种业务场景下是有问题的。

  3. 幻读--一个事务读到另一个事务插入的数据。

    事务A事务B
    Begin -- 开启事务Begin -- 开启事务
    select * from tableA;
    insert into tableA(id) values(10)
    commit; -- 提交事务
    select * from tableA; -- 会把事务B插入的新记录也查询出来

隔离级别

为了解决事务带来的如上4个问题,SQL标准中事务分为四个级别来支持解决不同的问题。

  1. read uncommitted——可能会出现脏读、重复度、幻读三种问题
  2. read committed——可能会出现重复读、幻读两种问题,但不会出现脏读的问题
  3. repeatable read——可能会出现幻读的问题,但不会出现脏读、可重复读的问题
  4. serializable——串行化执行SQL,不会出现上述的问题。

各个数据库厂商对SQL标准的实现有所不同,比如Oracle只有两种事务隔离级别;而MySQL的事务隔离级别和SQL标准略有不同,MySQL也实现了4种事务隔离级别,但是在repeatable read隔离级别下,不会发生幻读的问题,MySQL的默认隔离级别就是repeatable read

至于怎么在reaptable read隔离级别下解决的幻读问题,得从事务开启时说起。得益于ReadView这种结构

ReadView —— 它是事务隔离级别实现的秘密

前置知识:一条记录中除了存储数据外,还额外的保存了两部分信息 。

  1. trx_id:事务ID
  2. roll_pointer:指向undo页面的回滚指针

版本链中就是由一条记录的各个历史数据串联成的一条链

MySQL在使用begin语句或者其他方式开启事务的时候,会在某个时机创建一个结构体(可以理解为Java中的一个对象)——它的名字我们把它叫做ReadView。这个ReadView对象里面包含哪些属性呢?

  1. 当前事务ID:creator_trx_id
  2. 活跃的事务ID列表:m_ids
  3. 该记录版本链中最大的事务ID:max_trx_id
  4. 该记录版本链中最小的事务ID:min_trx_id
  • 对于read uncommitted级别只需要事务读取最新的版本链中的数据即可

  • 对于read committedreaptable 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
beginbegin
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中的属性就是:

  1. 当前事务ID:100
  2. 活跃事务ID:100
  3. 最大事务ID:201
  4. 最小事务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的时机不同。