带着问题来学习

MVCC是为了解决哪些问题 ?

并发访问(读或写)数据库时,对正在事务内处理的数据做多版本的管理。我们知道锁机制可以用来控制并发操作,但是其系统开销较大。而MVCC可以在大多数情况下代替行级锁,使用MVCC可以降低系统开销。

MVCC是如何实现的 ?

MVCC通过保存数据在某个时间点的快照来实现。不同存储引擎的MVCC实现是不同的。当我们创建表之后,mysql会自动为每个表添加数据版本号(最后更新数据的事务id)和删除版本号(数据删除的事务id),事务id由mysql数据库自动生成,且递增。

在哪些隔离级别下实现了mvcc ?

RRRC 隔离级别都实现了 MVCC 来满足读写并行。
两者相同点
它们读取的都是快照数据,并不会被写操作阻塞,所以这种读操作称为 快照读(Snapshot Read)。
两者不同点
RC每次读取数据前都生成一个ReadView。RR在第一次读取数据时生成一个ReadView。所以,RC 总是读取记录的最新版本,如果该记录被锁住,则读取该记录最新的一次快照,而 RR 是读取该记录事务开始时的那个版本。

在RU和Serializable为什么没有mvcc ?

RU 隔离级别下,每次都是读取最新版本的数据行,所以不能用 MVCC 的多版本,而 Serializable 隔离级别每次读取操作都会为记录加上读锁,也和 MVCC 不兼容,所以只有 RC 和 RR 这两个隔离级别才有 MVCC。

参考文献

mvcc思维导图: https://kdocs.cn/l/sd1gXtGXheTz
mvcc 多事务执行excel:https://kdocs.cn/l/se4n2ocRoaU1
mvcc 多事务执行版本链:https://kdocs.cn/l/sb2gsOmOQwP8
优秀博文:https://zhuanlan.zhihu.com/p/117476959

基本概念

当前读(Current Read)

select lock in share mode(共享锁), select for update ; update, insert ,delete(排他锁) 这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。

快照读(Snapshot Read)

像不加锁的select操作就是快照读,即不加锁的非阻塞读;快照读的实现是基于多版本并发控制(mvcc)。可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。

MVCC数据修改过程:

每行数据都存在一个版本,每次数据更新时都更新该版本。
修改时Copy出当前版本随意修改,个事务之间无干扰。
保存时比较版本号,如果成功(commit),则覆盖原记录;失败则放弃copy(rollback)。

MVCC数据修改和悲观锁数据修改对比

InnoDB的悲观锁数据修改过程是:事务以排他锁的形式修改原始数据,把修改前的数据存放于undo log,通过回滚指针与主数据关联,修改成功(commit)啥都不做,失败则恢复undo log中的数据(rollback)。

总上来看,二者最本质的区别是,当修改数据时是否要排他锁定,如果锁定了还算不算是MVCC呢?
Innodb的实现真算不上MVCC,因为并没有实现核心的多版本共存,undo log中的内容只是串行化的结果,记录了多个事务的过程,不属于多版本共存。但理想的MVCC是难以实现的,当事务仅修改一行记录使用理想的MVCC模式是没有问题的,可以通过比较版本号进行回滚;但当事务影响到多行数据时,理想的MVCC据无能为力了。

比如,如果 T1 执行理想的MVCC,修改Row1成功,而修改Row2失败,此时需要回滚Row1,但因为Row1没有被锁定,其数据可能又被 T2 所修改,如果此时回滚Row1的内容,则会破坏 T2 的修改结果,导致 T2 违反ACID。
理想MVCC难以实现的根本原因在于企图通过乐观锁代替二段提交。修改两行数据,但为了保证其一致性,与修改两个分布式系统中的数据并无区别,而二提交是目前这种场景保证一致性的唯一手段。二段提交的本质是锁定,乐观锁的本质是消除锁定,二者矛盾,故理想的MVCC难以真正在实际中被应用,Innodb只是借了MVCC这个名字,提供了读的非阻塞而已。

事务视图(ReadView)详解

ReadView上的几个参数:

  • m_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。
  • min_trx_id:表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。
  • max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的id值。
  • creator_trx_id:表示生成该ReadView的事务的事务id

小贴士1: 注意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。

小贴士2: 我们前边说过,只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE这些语句时)才会为事务分配事务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时生成该版本的事务已经被提交,该版本可以被访问。

如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。

undolog版本链详解

图示:https://kdocs.cn/l/sjj4G4Mjce4q

undo-log 版本链字段解释

row trx_id
数据的版本表示字段,用来标识数据的版本号

transaction id
事务ID,它在事务开始的时候向事务系统申请,按时间先后顺序递增

roll_pointer
指向到 undo-log 中的指针

图例过程演示

在 A (还未开始)的时候,roll_pointer 指向一个空的 undo log,因为之前这条数据是没有的。

修改的时候执行的流程如下

1、用 排它锁 锁定这一行的数据
2、对于要修改的数据,生成 undo-log
3、把指针指向到 undo-log (便于数据回滚)
4、进行修改数据
5、把修改的数据指针指向到 row trx_id

以时间轴角度理解 undo log 版本链

总结

  • 第一次的时候,因为是新插入的数据,没有指向一个空的 undo log
  • 往后开始,每次指向的 undo log 都是修改前的数据生成的 undo log

案例分析mvcc机制

4个事务的执行流程

以上事务对应的 undo log 链如下

说明
1、每个 trx_id 由 excel 中的事务序号标识(这样假设的话更好理解)
2、事务4,这里先不作体现,后续演示版本比较的时候再说明

image-20210304172806806

事务4执行时,如何寻找 undo-log

当事务4执行的时候,对应的undo-log链如下:

image-20210304172840748

事务4第一个update开始执行

因为事务1、事务3 还没有 commit
所以得到的 max trx id = 3 min trx id = 1
得到的版本链数组是 [1,3]
所以事务4这样开始判断 redo log 链来读取数据:

如果当前的事务id,小于undo log 链id,说明这个事务,比最早的事务早,则可以读

trx_id = 4 开始和 trx_id = 3 开始对比,判断出,4不比3小,其次存在于版本链数组中,则不可读
trx_id = 4 开始和 trx_id = 2 开始对比,判断出,4不比2小,但是 trx_id = 2 不存在与存在于版本链数组中,则可读,读到的是 name = B

事务4第二个update开始执行

依据上面的演示,则可以推导出,读取的是 trx_id = 3 的数据

总结

对于一个事务视图(ReadView)来说,它能够读到那些版本数据,要遵循以下规则

  • 当前事务内的更新,可以读到;
  • 版本未提交,不能读到;
  • 版本已提交,但是却在快照创建后提交的,不能读到;
  • 版本已提交,且是在快照创建前提交的,可以读到;

对于 undo log 版本链的判断,存在以下规则

  • 事务id < 未提交事务的最小id:可读
  • 最小id <= 事务id <= 事务的最大id:则判断事务id是否在未提交事务id的数组中,若在则不可读(只有自己可读)
  • 事务id > 事务的最大id:则不可读