Mvcc 学习笔记

Leaving things better than you found them

MVCC 笔记


MVCC为了解决什么问题?

多版本并发控制,针对在并发访问数据库时对于数据版本的控制以及隔离性问题,Mysql使用了MVCC的思路来进行版本控制

MVCC的MYSQL 实现浅析?

Mysql 的 MVCC实现大致是通过隐藏列中的DB_ROLL_PTR字段以及undo log的方式生成数据版本链,在创建事务时生成ReadView来进行版本比对,从而筛选出当前事务可见的数据行

事务并发执行会遇到的问题?

脏写(Dirty Write):一个事务修改了另一个事务未提交过的数据

脏读(Dirty Read):一个事务读取到了另一个事务未提交过的数据

不可重复读(Non-Repeatable Read):一个事务只能读取到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事物都能查到最新的值(在一个事务中的多次查询,可以查询到多个其他事务提交的最新值)

幻读(Phantom):一个事务根据某些条件查询出一些记录之后,另一个事务又向表中插入了符合这些条件的记录,原先的事务用相同的条件再次查询时,能把另外一个事务插入的数据也查询出来

按问题严重性排序:

脏写 > 脏读 > 不可重复读 > 幻读

标准的四种 SQL事务隔离级别(并非Mysql定义)

Read UnCommittd:未提交读

Read Committd:已提交读

Repeatable Read:可重复读

Serializable:可串行化

隔离级别 脏读 不可重复读 幻读
Read UnCommittd Possible Possible Possible
Read Committd Not Possible Possible Possible
Repeatable Read Not Possible Not Possible Possible
serializable Not Possible Not Possible Not Possible

也就是说:

Read UnCommittd 隔离级别下,可能发生脏读不可重复读幻读问题

Read Committd 隔离级别下,可能发生不可重复读幻读问题

Repeatable Read 隔离级别下,可能会发生幻读但是在Mysql中,Repeatable Read隔离级别可以处理幻读的问题

serializable 隔离级别下,各种问题都不会发生

至于脏写,应为脏写实在太严重了,所以无论哪个隔离级别都不允许脏写的情况发生。

什么是版本链 ? undo日志 ?

undo日志:用于记录事务中未提交的变更记录,主要用于保证事务的原子性,任何对数据的操作都会记录到undo日志中,直到提交事务或者rollback,才会进行清理。

说起版本链,我们得先有行格式的概念,我们大概看一下Compact格式下所看到的一行数据的格式:

Mvcc 学习笔记_第1张图片
行格式.png

在一个正常的行信息中,除了记录了用户的真实记录以外,innoDB还会为每条记录都添加2个隐藏列以及一个可选列

列名 是否必须 占用空间 描述
DB_TRX_ID 6字节 事务ID
DB_ROLL_PTR 7字节 回滚指针
DB_ROW_ID 6字节 行id,唯一标识一条记录

DB_ROW_ID 在没有自定义主键以及存在非Null的Unique键时才会添加该列

这里来简单阐述一下DB_TRX_ID以及DB_ROLL_PTR在版本链中的作用

DB_TRX_ID:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id赋值给该记录的DB_TRX_ID隐藏列,注意事务id是递增的。

DB_ROLL_PTR:每次对某条聚簇索引记录改动时,都会将旧的版本写入到 undo日志中,然后然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。

注意insert是不会产生DB_ROLL_PRT的,因为insert时并没有更早的版本存在

了解到这里我们大概就能看到版本链的雏形了,也就是利用了DB_ROLL_PTR来链接上一个版本的数据;

我们以一个hero表为例:


Mvcc 学习笔记_第2张图片
hero table.png

假如我们有一个hero表其中number为1的记录name初始化为刘备,我们执行如下两个语句:

Mvcc 学习笔记_第3张图片
hero 事务.png

它的版本链大概就是下面这个样子:


Mvcc 学习笔记_第4张图片
version list1.png

每次对该记录更新后,都会将旧值放到 undo 日志中,随着更新次数的增多,所有版本都会被DB_ROLL_PRT属性链接成为一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值,另外,每个版本中还包含生成该版本时对应的事务id

ReadView 是什么?

ReadView 可以按字面意思理解为读视图,也就是在事务开始时生成的一个快照,ReadView的设计主要是为了解决 "判断版本链中哪个版本是当前事务可见" 的问题

SERIALIZABLE隔离级别采用加锁的方式来访问记录,而READ COMMITTEDREPEATABLE READ隔离级别在事务的不同阶段会创建ReadView

Read committed 隔离级别下,每次读取数据前都会生产一个ReadView

Repeatable Read 隔离级别下,在第一次读取数据时生产一个ReadView

关于两种隔离级别下产生ReadView时机不同带来的影响,后面描述

ReadView 主要组成结构:

m_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务id列表

min_trx_id:表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中最小的值

max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的事务id

  max_trx_id并非是是m_ids中的最大值,事务id是递增分配的,比方说现在有id为1,2,3这三个事务,之后id为3的事务提交了,那么一个新的读事务在生产ReadView时,m_ids时就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4

creator_trx_id:表示生成该ReadView的事务的事务id

  只有在对表中的记录做改动时(执行Insert、update、delete)才会为事务分配事务id,否则在一个只读事务中,事务id都默认为0

当生成了这个ReadView,这样在访问某条记录时,只需按照下边的步骤判断记录的某个版本是否可见(可见性要求):

  • 如果被访问版本的 trx_id 属性值与 ReadView 中的 creator_trx_id 值相同,意味着当前事务在访问他自己修改过的记录,所以该版本可以被当前事务访问

  • 如果被访问版本的trx_id属性值小于ReadView中的min_trx_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问

  • 如果被访问版本的trx_id属性值大于ReadView中的max_trx_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问

  • 如果被访问版本的trx_id属性值在ReadViewmin_trx_idmax_trx_id之间,那就需要判断一下trx_id属性是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问,如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问

Read Committd 每次读取数据前都生成一个ReadView

假如现在系统中有两个事务在执行,事务id分别是100200

Mvcc 学习笔记_第5张图片
trx 2.png

版本链如下:


Mvcc 学习笔记_第6张图片
version list2.png

假设现在有一个使用Read Committd隔离级别的事务开始执行:

Mvcc 学习笔记_第7张图片
trx read committed.png

那么这个Select1的执行过程如下:

  1. 在执行SELECT 语句时会现生成一个ReadViewReadViewm_ids列表内容为[100,200],min_trx_id为100,max_trx_id为201,creator_trx_id为0

  2. 然后从版本链中挑选可见记录,从图中可以看出,最新版本的列name的内容是 '张飞',该版本的事务id为100,在m_ids内,所以不符合可见性要求,根据DB_ROLL_PTR(roll_pointer)找到下一个版本

  3. 下一个版本的列name的值为 '关羽',该数据的事务id也为100,在m_ids范围内,不符合可见性要求,继续跳到下一个版本

  4. 下一个版本的列name的值为 '刘备',该版本的事务id为80,小于ReadView中的min_trx_id值100,所以这个版本符合可见性要求,最终返回给用户的就是这条name列为 '刘备' 的数据

之后,我们把事务id100的事务提交一下:

Mvcc 学习笔记_第8张图片
commit transaction.png

然后再到事务id为200的事务中更新一下hero表中number为1的数据:


Mvcc 学习笔记_第9张图片
update trx 200.png

此刻表hero中number为1的记录的版本链如下:


Mvcc 学习笔记_第10张图片
version list3.png

然后我们再到刚才使用Read Committd隔离级别的事务中继续查找这个number为1的记录,如下:

Mvcc 学习笔记_第11张图片
SELECT 2.png

其中的Select2的执行过程如下:

  1. 在执行SELECT2 语句时又会单独生成一个ReadView,该ReadViewm_ids列表内容是[200](事务id为100的那个事务已经提交了,所以再次生成快照时就没有它了),min_trx_id为200,max_trx_id为201,creator_id为0

  2. 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列name值为 '诸葛亮',该版本的事务id为200,在m_ids列表内,所以不符合可见性要求,根据DB_ROLL_PTR(roll_pointer)跳到下一个版本。

  3. 下一个版本的列name的值为 '赵云',该版本的事务id为200,在m_ids列表内,所以也不符合要求,继续跳到下一个版本

  4. 下一个版本的列name的值为 '张飞',该版本的事务id为100,小于ReadViewmin_trx_id的值200,所以符合要求,最后返回给用户的版本就是这条列name为 '张飞' 的记录

可以看到在Read Committd的隔离级别下,出现了不可重复读的场景

Read Committd隔离级别下,事务在每次查询开始时都会创建一个独立的ReadView,关于Repeatable Read隔离级别下版本链以及执行过程大概类似这里就不阐述了(欢迎讨论),只是在Repeatable Read隔离级别下,在事务中多次读数据时,只会在第一次读取数据时创建ReadView,后面的查询都会复用第一次创建的ReadView,这就保证了前后两次查询到的结果一致,可以尝试使用Repeatable Read隔离级别的特性去看看上面的版本链,select2在Repeatable Read级别下应该返回什么?怎么去理解可重复度?


总结:

从上边的描述中我们可以看出来,所谓的MVCC(Multi-Version Concurrency Control ,多版本并发控制)指的就是在使用Read CommittdRepeatable Read这两种隔离级别的事务在执行普通的SEELCT操作时访问记录的版本链的过程,这样子可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。Read CommittdRepeatable Read这两个隔离级别的一个很大不同就是:生成ReadView的时机不同,Read Committd在每一次进行普通SELECT操作前都会生成一个ReadView,而Repeatable Read只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView就好了。

疑问?

undo log理论上会在事务提交后进行删除,那么版本链如何形成呢?

实际 insert undo 在事务提交之后就可以被释放了,update undo由于还需要支持MVCC,不能立即删除掉,实际在行结构中除了隐藏列还有一个delete mark的标记位,1代表删除,0代表未删除,用来记录数据是否被删除,所以在上面的版本链判断数据时并非是简单的判断事务id,同时还会考虑这个delete_mark标记,同时在mysql中,作者为了减少因为移除数据后的磁盘重新排列的性能问题,还搞了一个所谓的垃圾链表,在这个链表中的记录占用的空间称之为所谓的可重用空间,之后如果有新记录要插入到表中的话,可能会把这些被删除记录占用的存储空间给覆盖掉,当然也并非所有被标记了删除都数据都是覆盖处理,这里就涉及到mysql的后台的purge线程的作用了,后面再去了解

你可能感兴趣的:(Mvcc 学习笔记)