The most detailed MVCC principle

Continue to create, accelerate growth! This is the second day of my participation in the "Nuggets Daily New Plan · June Update Challenge", click to view the details of the event

principle

The implementation principle of MVCC mainly relies on three hidden fields, undolog and readview in each row of records.


hidden fields

  • DB_TRX_ID: 6 bytes, the id of the most recently modified transaction, the transaction id of the record that created this record or the last time that modified the record
  • DB_ROLL_PTR: 7 bytes, rollback pointer, pointing to the previous version of this record, used to cooperate with undolog, pointing to the previous old version
  • DB_ROW_JD: 6 bytes, hidden primary key, if the data table does not have a primary key, then innodb will automatically generate a 6-byte row_id

In the figure, DB_ROW_ID is the only implicit primary key generated by the database for the row record by default, DB_TRX_ID is the transaction ID of the current operation of the record, DB_ROLL_PTR is a rollback pointer, used to cooperate with the undo log, pointing to the previous old version


undo log

undolog is called a rollback log, which represents a log that is easy to rollback when insert, delete, and update operations are performed.

When an insert operation is performed (there is no record originally), the generated undolog is only needed when the transaction is rolled back, and can be discarded immediately after the transaction is committed

When the update and delete operations are performed (the original record and row data are modified), the generated undolog is not only required when the transaction is rolled back, but also when the snapshot is read, so it cannot be deleted casually , only in the snapshot read or transaction. When the rollback does not involve the log, the corresponding log will be uniformly cleared by the purge thread (when the data is updated and deleted, it is only to set the deleted_bit of the old record, not to delete the outdated record, because in order to save Disk space, innodb has a special purge thread to clear records whose deleted_bit is true, if the deleted_id of a record is true, and DB_TRX_ID is visible relative to the read view of the purge thread, then this record can be cleared at a certain time)

Let's take a look at the record chain generated by undolog

1、假设有一个事务编号为1的事务向表中插入一条记录,那么此时行数据的状态为:

2、假设有第二个事务编号为2对该记录的name做出修改,改为lisi

在事务2修改该行记录数据时,数据库会对该行加排他锁

然后把该行数据拷贝到undolog中,作为 旧记录,即在undolog中有当前行的拷贝副本

拷贝完毕后,修改该行name为lisi,并且修改隐藏字段的事务id为当前事务2的id,回滚指针指向拷贝到undolog的副本记录中

事务提交后,释放锁

3、假设有第三个事务编号为3对该记录的age做了修改,改为32

在事务3修改该行数据的时,数据库会对该行加排他锁

然后把该行数据拷贝到undolog中,作为旧纪录,发现该行记录已经有undolog了,那么最新的旧数据作为链表的表头,插在该行记录的undolog最前面

修改该行age为32岁,并且修改隐藏字段的事务id为当前事务3的id,回滚指针指向刚刚拷贝的undolog的副本记录

事务提交,释放锁

从上述的一系列图中,大家可以发现,不同事务或者相同事务的对同一记录的修改,会导致该记录的undolog生成一条记录版本线性表,即链表,undolog的链首就是最新的旧记录,链尾就是最早的旧记录。


Read View

Read View是事务进行快照读操作的时候生产的读视图,在该事务执行快照读的那一刻,会生成一个数据系统当前的快照,记录并维护系统当前活跃事务的id,事务的id值是递增的。

其实Read View的最大作用是用来做可见性判断的,也就是说当某个事务在执行快照读的时候,对该记录创建一个Read View的视图,把它当作条件去判断当前事务能够看到哪个版本的数据,有可能读取到的是最新的数据,也有可能读取的是当前行记录的undolog中某个版本的数据

Read View遵循的可见性算法 主要是将要被修改的数据的最新记录中的DB_TRX_ID(当前事务id)取出来,与系统当前其他活跃事务的id去对比,如果DB_TRX_ID跟Read View的属性做了比较,不符合可见性,那么就通过DB_ROLL_PTR回滚指针去取出undolog中的DB_TRX_ID做比较,即遍历链表中的DB_TRX_ID,直到找到满足条件的DB_TRX_ID,这个DB_TRX_ID所在的旧记录就是当前事务能看到的最新老版本数据。

Read View的可见性规则如下所示:

首先要知道Read View中的三个全局属性:

trx_list:一个数值列表,用来维护Read View生成时刻系统正活跃的事务ID(1,2,3)

up_limit_id:记录trx_list列表中事务ID最小的ID(1)

low_limit_id:Read View生成时刻系统尚未分配的下一个事务ID,(4)

具体的比较规则如下:

1、首先比较DB_TRX_ID < up_limit_id,如果小于,则当前事务能看到DB_TRX_ID所在的记录,如果大于等于进入下一个判断

2、接下来判断DB_TRX_ID >= low_limit_id,如果大于等于则代表DB_TRX_ID所在的记录在Read View生成后才出现的,那么对于当前事务肯定不可见,如果小于,则进入下一步判断

3、判断DB_TRX_ID是否在活跃事务中,如果在,则代表在Read View生成时刻,这个事务还是活跃状态,还没有commit,修改的数据,当前事务也是看不到,如果不在,则说明这个事务在Read View生成之前就已经开始commit,那么修改的结果是能够看见的。


MVCC整体处理流程

接下来我们根据可见性算法来分类解析MVCC的处理流程

1、无法读取修改但未提交的数据

假设有四个事务同时在执行,如下图所示:

事务1 事务2 事务3 事务4
事务开始 事务开始 事务开始 事务开始
...... ...... ...... 修改但未提交
进行中 快照读 进行中
...... ...... ......

从表中,我们可以看出,当事务2对某行数据执行快照读操作的时候,数据库为该行创建一个Read View视图,事务1、2、3、4均为活跃状态,up_limit_id的值为1,而low_limit_id为5。如下图:

该行当前数据的undolog如下所示:

当事务2在快照读该行记录时,会拿着该行记录的DB_TRX_ID去跟up_limit_id,lower_limit_id和活跃事务列表进行比较,判读事务2能看到该行记录的版本是哪个。

具体流程如下:先拿该行记录的事务ID(4)去跟Read View中的up_limit_id相比较,判断是否小于,通过对比发现不小于,所以不符合条件,继续判断4是否大于等于low_limit_id,通过比较发现也不大于,所以不符合条件,判断事务4是否处理trx_list列表中,发现事务4是活跃列表,不符合可见性条件,所以事务4修改未提交的结果对事务2 的快照是不可见的。


2、能读取修改并提交的数据

事务1 事务2 事务3 事务4
事务开始 事务开始 事务开始 事务开始
...... ...... ...... 修改且已提交
进行中 快照读 进行中
...... ...... ......

从上述表格中,我们可以看到,当事务2对某行数据执行了快照读,数据库为该行数据生成一个Read View视图,可以看到事务1和事务3还在活跃状态,事务4在事务2快照读的前一刻提交了更新,所以,在Read View中记录了系统当前活跃事务1,2,3,维护在一个列表中。同时可以看到up_limit_id的值为1,而low_limit_id为5,如下图所示:

在上述的例子中,只有事务4修改过该行记录,并在事务2进行快照读前,就提交了事务,所以该行当前数据的undolog如下所示:

当事务2在快照读该行记录的是,会拿着该行记录的DB_TRX_ID去跟up_limit_id,lower_limit_id和活跃事务列表进行比较,判读事务2能看到该行记录的版本是哪个。

具体流程如下:先拿该行记录的事务ID(4)去跟Read View中的up_limit_id相比较,判断是否小于,通过对比发现不小于,所以不符合条件,继续判断4是否大于等于low_limit_id,通过比较发现也不大于,所以不符合条件,判断事务4是否处理trx_list列表中,发现不再次列表中,那么符合可见性条件,所以事务4修改后提交的最新结果对事务2 的快照是可见的,因此,事务2读取到的最新数据记录是事务4所提交的版本,而事务4提交的版本也是全局角度的最新版本。

3、修改并提交事务后再快照读

事务1 事务2 事务3 事务4
事务开始 事务开始 事务开始 事务开始
修改并提交 ...... ...... ......
快照读 进行中 进行中
...... ......

从表中,我们可以看出,当事务2对某行数据执行快照读操作的时候,数据库为该行创建一个Read View视图,事务2、3、4均为活跃状态,up_limit_id的值为2,而low_limit_id为5。如下图:

此时DB_TRX_ID为1,小于 up_limit_id(2),所以当前事务能看到事务1修改的记录

4、快照读在数据修改之前

事务1 事务2 事务3 事务4
事务开始 事务开始 事务开始
修改并提交 ...... ......
快照读 进行中
...... ...... 事务开始
修改未提交

从表中,我们可以看出,当事务2对某行数据执行快照读操作的时候,数据库为该行创建一个Read View视图,事务2、3均为活跃状态,up_limit_id的值为2,而low_limit_id为4。如下图:

由上面的分析可以知道,事务1对数据的修改对事务2的快照读是可见的,但是事务4对数据的修改,由于此时DB_TRX_ID = low_limit_id,也就是说事务4是在快照读创建之后再修改数据的,所以事务2的快照读无法读取到事务4对该行数据的修改


RC隔离级别

RC隔离级别下,每个快照读都会生成并获取最新的readview。

事务1 事务2 事务3 事务4
事务开始 事务开始 事务开始
修改并提交 ...... ......
快照读 进行中
...... ...... 事务开始
...... ...... 修改未提交
快照读 ...... ......

由于上面的分析已经很详细了,这边只是简单地分析并给出结果

针对事务2第一次快照读,此时活跃事务为2、3,up_limit_id的值为2,而low_limit_id为4,事务2第一次快照读能读取到事务1的修改不能读到事务4的修改。

事务2第二次快照读能读到事务1的修改,活跃事务为2、3、4,up_limit_id的值为2,low_limit_id为5,但是针对事务4的修改DB_TRX_ID=4<5所以判断是否为活跃事务,是活跃事务,所以不能看到事务4对数据的修改。

事务1 事务2 事务3 事务4
事务开始 事务开始 事务开始
修改并提交 ...... ......
快照读 进行中
...... ...... 事务开始
...... ...... 修改已提交
快照读 ......

事务2第一次快照读的时候,活跃事务为2和3。up_limit_id的值为2,而low_limit_id为4。事务2第一次快照读能读取到事务1的修改。

事务2第二次快照读的时候,活跃事务依然为2和3,能读到事务1的修改,针对事务4的修改DB_TRX_ID=4<low_limit_id,所以判断是否为活跃事务,不是活跃事务,所以能看到事务4对数据的修改。


RR隔离级别

RR隔离级别就是第一次执行快照读的时候就创建Read View,每次快照读都用这个ReadView。


总结

1、一个数据版本,对于一个事务视图来说,除了自己的更新总是可见意外,有三种情况

  1. 版本未提交,不可见;
  2. 版本已提交,但是是在视图创建后提交的,不可见;
  3. 版本已提交,而且是在视图创建前提交的,可见。

2、因为Read View生成时机的不同,从而造成RC、RR级别下快照读的结果的不同。

1、在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照即Read View,将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见

2、在RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动和事务的快照,这些事务的修改对于当前事务都是不可见的,而早于Read View创建的事务所做的修改均是可见

3、在RC级别下,事务中,每次快照读都会新生成一个快照和Read View,这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因。

Summary: Under the RC isolation level, each snapshot read will generate and obtain the latest Read View, while under the RR isolation level, the first snapshot read in the same transaction will create a Read View, and subsequent snapshots will create a Read View. Read and get the same Read View.

3. MVCC only exists in the RC and RR isolation levels. The other two isolation levels do not need to use MVCC, because the original data can be directly modified by reading uncommitted, and other transactions can see the data immediately when viewing the data, and there is no need for the version field at all. Serialization itself is a read operation that blocks other transactions, and MVCC is a lock-free optimization when reading, so it cannot be used in the serialization isolation level.

references

"Phoenix Architecture"

"In-depth understanding of distributed transactions"

Supongo que te gusta

Origin juejin.im/post/7102676257149550622
Recomendado
Clasificación