MVCC (Multiversion Concurrency Control) 中文全程叫多版本并发控制,是现代数据库(包括 MySQL、Oracle、PostgreSQL 等)引擎实现中常用的处理读写冲突的手段,目的在于提高数据库高并发场景下的吞吐性能。
如此一来不同的事务在并发过程中,SELECT 操作可以不加锁而是通过 MVCC 机制读取指定的版本历史记录,并通过一些手段保证保证读取的记录值符合事务所处的隔离级别,从而解决并发场景下的读写冲突。
举一个并发流程案例:
在事务 A 提交前后,事务 B 读取到的 x 的值是什么呢?答案是:事务 B 在不同的隔离级别下,读取到的值不一样。
如果事务 B 的隔离级别是读未提交(RU),那么两次读取均读取到 x 的最新值,即 20。
如果事务 B 的隔离级别是读已提交(RC),那么第一次读取到旧值 10,第二次因为事务 A 已经提交,则读取到新值 20。
如果事务 B 的隔离级别是可重复读或者串行(RR,S),则两次均读到旧值 10,不论事务 A 是否已经提交。
可见在不同的隔离级别下,数据库通过 MVCC 和隔离级别,让事务之间并行操作遵循了某种规则,来保证单个事务内前后数据的一致性。
InnoDB 相比 MyISAM 有两大特点,一是支持事务而是支持行级锁,事务的引入带来了一些新的挑战。相对于串行处理来说,并发事务处理能大大增加数据库资源的利用率,提高数据库系统的事务吞吐量,从而可以支持可以支持更多的用户。但并发事务处理也会带来一些问题,主要包括以下几种情况:
以上是并发事务过程中会存在的问题,解决更新丢失可以交给应用,但是后三者需要数据库提供事务间的隔离机制来解决。实现隔离机制的方法主要有两种:
加读写锁
加间隙锁
快照读 即MVCC
但本质上,隔离级别是一种在并发性能和并发产生的副作用间的妥协,通常数据库均倾向于采用 Weak Isolation。
读未提交:解决了脏写。保证了两个事务在没提交的时候不可能去同时更新同一行数据。(产生脏读就是事务A写值为A后未提交,然后事务B更新为B提交,此时A后悔进行回滚,导致事务B以为还是B值。该级别限制了在事务A未提交的时候事务B不能去更新这个值,从而解决脏写问题)(该级别只解决了脏写问题,其他问题并未解决)
读已提交(RC):解决脏读问题,而出现脏读的现象是读取了不同事务中未提交的数据。而读已提交限制只能读已提交的数据,从而解决了脏读的问题。在该级别下不会发生脏读脏写的现象。该级别也简称为RC。
可重复读(RR):解决不可重复读的问题。
串行化:解决幻读问题。这种级别就是不允许多个事务并发执行,多个事务只能串行起来执行,例如先执行A提交后再执行事务B。当然这样效率会低很多。
其中最常用的是RC、RR级别(默认是RR级别,可有些场景需要将级别设置为RC级别)
在 RU 隔离级别下,直接读取版本的最新记录就 OK,对于 SERIALIZABLE 隔离级别,则是通过加锁互斥来访问数据,因此不需要 MVCC 的帮助。因此 MVCC 运行在 RC 和 RR 这两个隔离级别下,当 InnoDB 隔离级别设置为二者其一时,在 SELECT 数据时就会用到版本链
核心问题是版本链中哪些版本对当前事务可见?
InnoDB 为了解决这个问题,设计了 ReadView(可读视图)的概念。
ReadView 中是当前活跃的事务 ID 列表,称之为 m_ids,其中最小值为 up_limit_id,下一个Mysql事务Id为 low_limit_id
在 RR 隔离级别下,每个事务 touch first read 时(本质上就是执行第一个 SELECT 语句时,后续所有的 SELECT 都是复用这个 ReadView,其它 update, delete, insert 语句和一致性读 snapshot 的建立没有关系),会将当前系统中的所有的活跃事务拷贝到一个列表生成 ReadView。
在 RC 隔离级别下,每个 SELECT 语句开始时,都会重新将当前系统中的所有的活跃事务拷贝到一个列表生成 ReadView。二者的区别就在于生成 ReadView 的时间点不同,一个是事务之后第一个 SELECT 语句开始、一个是事务中每条 SELECT 语句开始。
以多版本并发操作案例为例子
假设当前行老数据trx_id 为50
根据RC下ReadView的规则 第 5 步生成的ReadView如下所示
m_ids:[100,200] // A线程 100 B线程 200 当前A线程还没commit所以属于活跃线程
up_limit_id:100
low_limit_id:201
trx_id:100
此时满足总结中的条件2: trx_id在m_ids列表中,说明当前事务在这一次查询的时候还在活跃中,所以不能查看,所以根据 undolog 链条 找到老数据trx_id 为50的数据
因为是RC 所以是每一次查询都会生成ReadView
第 7 步生成的ReadView如下所示
m_ids:[200] // A线程 100 B线程 200 当前A线程已经commit所以不在这个列表
up_limit_id:200
low_limit_id:201
trx_id:100
此时满足总结中的条件条件1,所以可以读取到数据
根据RR下ReadView的规则,会在frist search的时候生成ReadView,所以在第 5 步生成ReadView 如下所示
m_ids:[100,200] // A线程 100 B线程 200 当前A线程还没commit所以属于活跃线程
up_limit_id:100
low_limit_id:201
trx_id:100
此时满足总结中的条件2: trx_id在m_ids列表中,说明当前事务在这一次查询的时候还在活跃中,所以不能查看,所以根据 undolog 链条 找到老数据trx_id 为50的数据
因为RR下的ReadView规则所以第 5 步和 第 7 步的ReadView为同一个,所以皆无法看到数据
MVCC到底能否解决幻读问题?
我认为不能解决幻读的问题,原因是简单的insert可能可以根据MVCC版本控制避免,但是如下复杂情况就会读取到幻影行,在一次事务里面,多次查询之后,结果集的个数不一致的情况叫做幻读。而多或者少的那一行被叫做 幻行
场景1: update
站在事务B的角度来看,修改了空气行结果成功了且还能查出来,我觉得是幻读的问题的一种
场景2: delete
站在事务B的角度来看,我查出来2条记录,我删除成功了0条,再去查结果还能查到数据,删除不了的空气行?
综上所述我觉得MVCC并不能解决幻读问题,Mysql官方文档写的是使用MVCC + next key lock 来解决幻读问题。
如果被访问版本的 trx_id 小于 m_ids 中的最小值 up_limit_id,说明生成该版本的事务在 ReadView 生成前就已经提交了,所以该版本可以被当前事务访问。
如果被访问版本的 trx_id 属性值在 m_ids 列表中最大值和最小值之间(包含),那就需要判断一下 trx_id 的值是不是在 m_ids 列表中。如果在,说明创建 ReadView 时生成该版本所属事务还是活跃的,因此该版本不可以被访问,需要查找 Undo Log 链得到上一个版本,然后根据该版本的 DB_TRX_ID 再从头计算一次可见性;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。
如果被访问版本的 trx_id 大于 m_ids 列表中的最大值 low_limit_id,说明生成该版本的事务在生成 ReadView 后才生成,所以该版本不可以被当前事务访问。需要根据 Undo Log 链找到前一个版本,然后根据该版本的 DB_TRX_ID 重新判断可见性。
此时经过一系列判断我们已经得到了这条记录相对 ReadView 来说的可见结果。此时,如果这条记录的 delete_flag 为 true,说明这条记录已被删除,不返回。否则说明此记录可以安全返回给客户端。
呜呜呜呜,CSDN的脚注功能感觉有问题,Typora用的时候没问题的,这边好像有点难搞