多版本并发控制机制(Multi-Version Concurrency Control,MVCC)是InnoDB中用来实现不同隔离级别的。
一、undo log 多版本链条
每一条数据都有三个隐藏字段:DB_ROW_ID、DB_TRX_ID、DB_ROLL_PTR。其中这个DB_TRX_ID就是最近一次更新这条数据的事务id
,DB_ROLL_PTR就是指向了更新这条数据的事务所生成的undo log
。
举个例子,假设现在有一个事务A(id=50),插入了一条数据,那么此时这条数据的DB_TRX_ID字段就是50,而DB_ROLL_PTR字段则是空,因为这是一条新插入的数据。如下图所示:
接着假设又来了一个事务B(id=58),将这条记录的值修改为B,那么此时不仅会生成一个undo log记录了原始值A,还会将上面这两个隐藏字段修改为对应的值:
如果又来了一个事务C,同样按照上面的步骤执行:
这样就通过trx_id和roll_pointer实现了一个undo log版本链。
二、ReadView机制
简单来说,执行一个事务的时候,InnoDB会生成一个ReadView,其中包含了4个比较重要的东西:
- m_ids:此时有哪些事务执行了但还没有提交的;
- min_trx_ids:m_ids中的最小值;
- max_trx_ids:下一个要生成的事务id,也就是大于当前所有执行中的事务的id;
- creator_trx_ids:当前这个事务的id。
举例说明ReadView的作用。假设数据库里原本有一条记录,是之前的事务A插入的,事务id是32,插入值是A,如下图所示:
然后又两个并发事务同时过来准备执行,一个是事务B(id=45),另一个是事务C(id=49)。事务B准备读取上面这条记录,事务C准备更新上面这条记录。
现在事务B直接开启了一个ReadView,它的m_ids就包含了这两个事务的id:45和49
,然后min_trx_id是45
,max_trx_id是50(当前最大为49,所以取49+1)
,creator_trx_id是45,也就是它自己的id
。这个时候事务B第一次查询这条数据,会判断一下当前这条数据中的隐藏字段trx_id是否小于ReadView中的min_trx_id
。发现trx_id=32,是小于B的ReadView中的min_trx_id值45的,这就代表在当前事务开启之前,这行数据已经提交过了
,所以此时的查询结果是有效的。如下图所示:
接着事务C开始执行,把这条数据值修改为C,然后提交。上面说过了,它会生成一条新undo log,并且通过roll_pointer关联到前面一条undo log:
如果此时事务B再次查询这条记录会发现一个问题,那就是此时数据行里的trx_id=49是大于事务B的ReadView中的min_trx_id(45)
的,同时也小于max_trx_id(50)
,这就表示修改当前数据的事务适合自己并发执行的。并且不等于自己的creator_trx_id(45)
,说明这条数据不是自己修改的。因此判断出是在自己后面执行的事务修改的,所以这行数据现在肯定是脏数据,也就不能查询了。如下图:
但是总要查出一个结果啊,要去哪里查呢?上面说到的undo log多版本链表就是答案。我们只要顺着这个roll_pointer指针,找到一个最近的undo log,也就是小于当前ReadView中min_trx_id值的undo log
,然后取它里面的值。这里我们可以找到最近的undo log是trx_id=32的unlod,所以此时事务B查询的结果实际上还是原始值A。
这样就通过ReadView+undo log多版本链条的机制,保证了多个事务并发执行的时候不会读到其他事务更新的值,只会读到自己更新的或者更早的值。
我们平常说的MVCC也就是这个ReadView+undo log多版本链条机制
。
三、Read Committed隔离级别的实现原理
RC级别可以避免脏读,但是会发生不可重复读现象,也就是一个事务有可能会读到其他已提交事务更新的数据。此外,RC也会发生幻读现象。
我们通过一个实例来看看RC级别是怎么通过ReadView+undo log机制来实现的。
首先,一个事务A插入了一条数据A并且提交成功。然后事务B和事务C同时并发执行,事务B先查询得到了原始值A,然后事务C修改并提交了值C。此时如果事务B再去查询数据,按照ReadView的机制是查询不到值C的。
但是按照RC级别的定义,其实是应该是可以查询到值C的,那么是怎么实现的呢?其实就是事务B再次查询的时候会创建一个新的ReadView,注意并不是开启新的事务,还是当前事务B
。此时创建的ReadView包含:m_ids=[45],min_trx_id=45,max_trx_id=51,creator_trx_id=45。此时活跃事务列表m_ids里面只有一个45,并没有事务C的id了,也就去除了之前的限制
。其实就是通过创建新的ReadView产生了一个事务B和事务C不是并发执行(因为事务C已经提交完成)的效果。这样顺着undo log链表再去找到trx_id=49的记录时,虽然49大于当前事务id45,但是不在m_id中就说明在当前ReadView创建之前,事务49已经提交完成
。事务B再去查询的时候就可以直接得到值C了。
其实,它的关键点就是每次查询都会生成一个新的ReadView,这样就保证了可以查询到其他事务提交的最新值了
。
四、Repeatable Read隔离级别的实现原理
RR级别可以同时避免脏读、不可重复度、幻读现象。它也是通过MVCC来实现的。就是我们上面《二、ReadView机制》中详细描述的原始的ReadView+undo log机制,它解决了脏读和不可重复读的原理我们也说明了,那么它是如何解决幻读现象的呢?其实还是同样的道理,由于幻读现象多次范围查询的结果不一样,MVCC本身就保证了当前事务不可能查询到其他事务提交的修改,包括更新和插入新的记录,因此也就不会发生幻读现象了
。
THE END.