本篇文章主要总结一下我对MVCC的理解以及知识总结。
在总结 undo log 时,undo log 的作用之一为实现mvcc,因此总结一下有关 mvcc 的知识。
一、什么是MVCC
MVCC
(多版本并发控制),主要是为了提高数据库的并发性能,同一行数据平时发生读写请求时,会上锁阻塞住,但mvcc
用更好的方式去处理读写请求,做到在发生读写冲突时不用加锁,这个读指的是快照读,而不是当前读,当前读是一种加锁操作,是悲观锁。
二、当前读 VS 快照读
当前读(select … for update 等语句):它读取的数据库记录,都是当前最新的版本,当前读会对当前读取的数据进行加锁,防止其他事务修改数据。
快照读(普通select语句):快照读的实现是基于多版本并发控制,既然是多版本,那么快照读读到的数据不一定是当前最新的数据,有可能是历史版本的数据。
三、Read View介绍
首先我们需要了解 ReadView
中的四个字段以及作用:
Read View 中的四个主要的字段:
create_trx_id
:在创建当前 Read View 的事务的事务id;m_ids
:在创建 Read View 时,当前数据库中「活跃事务」的 事务id 列表,其中 “活跃事务” 指的就是开启了但还没提交的事务。min_trx_id
:在创建 Read View 时,当前数据库中「活跃事务」中事务id最小的事务,即m_ids的最小值。max_trx_id
:在创建 Read View 时当前数据库中应该给下一个事务的 id 值,即全局事务中最大的事务 id 值 + 1;
四、聚簇索引中的隐藏字段
在聚簇索引记录中,主要有两个隐藏列来结合 Read View 实现 MVCC :
trx_id
:当事务对某条聚簇索引记录进行改动时,会将该事务的 事务id 记录到 trx_id 隐藏列中。roll_pointer
:当聚簇索引记录进行修改时,会将旧版本的数据写入到 undo log 中,通过roll_pointer
指针,指向旧版本记录,因此可以通过这个指针找到修改前的数据。
MySQL中 事务id 的生成策略
事务id本质上就是一个数字,它的分配策略主要如下:
1、服务器会在内存中维护一个全局变量,每当需要为某个事务分配事务id时,就会把该变量的值当做事务id分配给该事务,并且把该变量自增1。
2、每当这个变量的值为256的倍数时,就会将该变量的值刷新到系统表空间中页号为5的页面中一个为Max Trx ID的属性中,这个属性占用8字节的存储空间。
3、当系统下一次重新启动时,会把这个Max Trx ID属性加载到内存中,将该值加上256之后赋值给前面提到的全局变量。
注:
1、分配事务id时,首先是先分配,再自增1。
2、重新启动后增加256是为了防止上次关机时,该全局变量的值可能大于磁盘页面中的Max Trx ID属性值。
3、该策略可以保证整个系统中分配的事务id值是一个递增的数字。先分配事务id的事务得到的是较小的事务id,后分配事务id的事务得到的是较大的事务id。
五、Read View的工作流程
Read View 与 聚簇索引中的 trx_id 结合使用,可以划分为三种情况:
当一个事务去访问记录时,除了本身事务的更新记录可见外,还包含如下几种情况:
- 如果记录中的
trx_id
值 小于Read View
中的min_trx_id
值 ,则说明当前版本的记录是在创建该Read View
前就已经提交的事务生成的记录,因此对该版本记录对当前事务是可见的。 - 如果记录中的
trx_id
值 大于等于Read View
中的max_trx_id
值 ,则说明当前版本的记录是在创建该Read View
后才开启的事务,因此该版本记录对当前事务是不可见的。 - 如果记录中的
trx_id
值 在Read View
的min_trx_id
和max_trx_id
之间,需要判断trx_id
是否在m_ids
列表中:- 如果记录的
trx_id
在m_ids
列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见。 - 如果记录的
trx_id
不在m_ids
列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。
- 如果记录的
通过 Read View
与 trx_id
进行比较的方式,同时通过版本链来控制事务访问数据记录。
六、MVCC在可重复读的实现方式
在可重复读隔离级别下,启动事务时会生成一个 Read View,之后在整个事务执行期间都使用这个 Read View
。
假设当前数据库启动了两个事务,分别为事务A 和 事务B,其中 事务A 的 事务id 为 21,紧接着开启 事务B,事务B 的事务id 为 22,此时两个事务的 Read View 如下所示
上述两个事务的 Read View
中:
- 事务A 的
Read View
中,事务id
为 21,因为当前开启的事务中只有 事务A,因此活跃事务的事务 id 列表只有21,其中活跃事务中的最小事务 id 为 事务A 本身,而`max_trx_id 为 22,即分配给下一个事务的事务 id 为22。 - 事务B 的
Read View
中,事务id
为 22,此时活跃事务的事务 id 列表为 [21, 22],其中活跃事务中的最小事务 id 为 21,而max_trx_id
为 23,即分配给下一个事务的事务 id 为23。
假设 事务A 和 事务B 在可重复读隔离级别下执行了如下的操作:
- 事务 B 读取数据记录,读到 money 是 1000,即 事务B第一次读取记录。
- 事务 A 将当前记录进行修改,修改 money 为 2000,此时并未提交事务;
- 事务B 再次读取该记录,读到 money 仍然为 1000,即 事务B第二次读取记录。
- 事务A 提交事务;
- 事务B 再次读取该记录,读到 money 仍然为 1000,即 事务B第三次读取记录。
此时数据库中的数据情况如下:
- 事务A还未进行记录修改时:
- 事务A修改记录之后:
在经过 事务A 对记录的修改之后,以前的记录就变成了旧版本的记录,新版本的记录与旧版本的记录通过链表连接起来,此时新版本记录的 trx_id
为21。
我们可以通过当前的数据记录中的 隐藏列
以及 两个事务生成的 Read View
分析一下 事务B 在读取记录的情况:
- 当 事务B 第一次读取记录时,会先看当前记录的
trx_id
,此时发现trx_id
为20,与事务B 中Read View
中的min_trx_id
值(21)相比还小,说明修改这条记录的事务早在事务B开启时就已经提交了,因此该版本的记录对 事务B 可见。 - 当 事务B 第二次读取记录时,发现当前记录的
trx_id
值为 21,在事务B 的Read View
中的min_trx_id
和max_trx_id
之间,则此时需要判断当前记录的trx_id
是否在m_ids
范围内,由Read View
可知,m_ids
中包含 21,说明修改这条记录的事务还未提交,此时 事务B 读取不到该版本的记录。之后沿着roll_pointer
往下查找旧版本的记录,直到找到trx_id
小于 事务B 的Read View
中的min_trx_id
值的第一条记录,因此 事务B 第二次读取到的 money 值仍然为1000. - 事务B第三次读取记录时,由于当前的隔离级别为 可重复读,因此事务B使用的
Read View
仍然为事务开启时生成的Read View
来判断当前版本的记录可否可见。因此,即使 事务A 修改记录并提交事务后,事务B 第三次读取记录时,读到的 money 值仍然为1000。
因此,在可重复读隔离级别下,整个事务在执行的过程中,使用的 Read View
一直为事务开启时生成 Read View
。
七、MVCC在读已提交的实现方式
在读已提交隔离级别下,MVCC
的实现 与 可重复读隔离级别相比:
- 读提交 隔离级别是在每个
select
都会生成一个新的Read View
,这说明事务执行期间如果多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务。 - 可重复读 隔离级别是启动事务时生成一个
Read View
,然后整个事务期间都在用这个Read View
,这样就保证了在事务期间读到的数据都是事务启动前的记录。
在该隔离级别下,同样也是通过对比 记录中的 trx_id
与 事务的 Read View
中的字段进行比对,从而查询到可见的数据记录,比较的过程与上述在介绍可重复读隔离级别下读取记录的流程相似。
读已提交下,每次执行 select
语句都会生成新的 Read View
,并且这期间可能会有许多事务的提交与事务的开启,同时这些事务会对记录进行修改,生成的 Read View
中字段值也可能随时发生变化,因此,在事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致的情况。
八、总结
MySQL为了提高数据库的并发性能,在进行快照读时采用MVCC
的方式解决并发事务中的幻读问题,避免读取记录时需要上锁的情况。
事务在不同的隔离级别下,生成 Read View
的时机也是不同的:
- 读提交 隔离级别是在每个
select
都会生成一个新的Read View
; - 可重复读 隔离级别是启动事务时生成一个
Read View
;
MVCC在实现上,主要是通过聚簇索引记录中的 隐藏字段 与 事务生成的 Read View 中的字段进行比较,从而找到可见的数据记录。
以上就是我对于 MVCC 的一个总结,其中也参考了许多的资料后进行一个总结。