MySQL的MVCC(多版本控制)
一、MVCC 全称 Multi-Version Concurrency Control,即多版本并发控制,用来解决读写冲突的无锁并发控制,可以在发生读写请求冲突时不用加锁解决,这个读是指的快照读(也叫一致性读或一致性无锁读),而不是当前读
当前读:像 select lock in share mode (共享锁), select for update; update; insert; delete (排他锁)这些操作都是一种当前读,为什么叫当前读?
就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁
SELECT * FROM xx_table LOCK IN SHARE MODE;
SELECT * FROM xx_table FOR UPDATE;
INSERT INTO xx_table ...
DELETE FROM xx_table ...
UPDATE xx_table ...
快照读:简单的select(不加锁)就是快照读,快照读实现基于 MVCC,因为是多版本并发,所以快照读读到的数据不一定是当前最新的数据,有可能是历史版本的数据
数据库并发场景有三种,分别为:
-
读-读:不存在任何问题,也不需要并发控制
-
读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
-
写-写:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失
二、MVCC 带来的好处是?
多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。
所以 MVCC 可以为数据库解决以下问题:
-
在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
-
同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题
三、提高读写和写写的并发性能的组合方式:
-
MVCC + 悲观锁:MVCC解决读写冲突,悲观锁解决写写冲突
-
MVCC + 乐观锁:MVCC 解决读写冲突,乐观锁解决写写冲突
四、MVCC 的实现原理
MVCC 的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的 3个隐式字段,undo日志 ,Read View 来实现的
1、隐式字段
每行记录除了我们自定义的字段外,还有数据库隐式定义的 DB_TRX_ID、 DB_ROLL_PTR、 DB_ROW_ID 等字段

例子:

五、undo日志
undo log 主要分为两种:
-
insert undo log:代表事务在 insert 新记录时产生的 undo log, 只在事务回滚时需要,并且在事务提交后可以被立即丢弃
-
update undo log:事务在进行 update 或 delete 时产生的 undo log ; 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被 purge 线程统一清除
undo log版本链的形成过程:
案例一
1、比如一个有个事务插入 persion 表插入了一条新记录,记录如下,name 为 Jerry , age 为 24 岁,隐式主键是 1,事务 ID和回滚指针,均为 NULL

2、 现在来了一个事务 1对该记录的 name 做出了修改,改为 Tom

3、又来了个事务 2修改person 表的同一个记录,将age修改为 30 岁

从上面,我们就可以看出,不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线性表,既链表,undo log 的链首就是最新的记录,链尾就是最早的旧记录
(当然就像之前说的该 undo log 的节点可能是会 purge 线程清除掉,向图中的第一条 insert undo log,其实在事务提交之后可能就被删除丢失了,不过这里为了演示,所以还放在这里)
案例二

六、Read View(读快照)
Read View(读快照) 是 快照读 SQL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务(未提交的)id,用来做可见性判断,根据读快照判断当前事务能够看到哪个版本的数据
Read View 几个属性:
-
m_ids:当前数据库中【活跃事务】的事务 id 列表
-
min_trx_id:m_ids 中最小的事务id
-
max_trx_id:并不是 m_ids 中的最大的事务id,而是 max(m_ids) + 1
-
creator_trx_id:生成该 Read View 的事务的事务 id(当前事务)
Read View :一致性视图

在可重复读隔离级别,当事务开启,执行任何查询sql时会生成当前事务的一致性视图read-view,该视图在事务结束 之前都不会变化(如果是读已提交隔离级别在每次执行查询sql时都会重新生成),这个视图由执行查询时所有未提交事务id数组(数组里最小事务的id =min_id)和已创建的最大事务id(max_id )组成,事务里的任何sql查询结果需要从对应版本链里的最新数据开始逐条跟read-view做比对从而得到最终的快照结果
版本链数据访问规则

版本链比对规则:
row 的 trx_id 为 undo log记录版本链中每条记录的 trx_id
1、如果 row 的 trx_id = creator_trx_id;表示这个版本数据就是由当前事务自己生成的,当前事务能读取到
-
如果 row 的 trx_id < min_id;表示这个版本数据,是当前事务开启之前还没执行,别的事务保持提交的,所以当前事务能读取到;
-
如果 row 的 trx_id > max_id;表示这个版本数据,是当前事务开启之后执行完了,别的事务保持提交的,所以当前事务不能能读取到;
-
如果 row 的 min_id < trx_id < max_id,判断 db_trx_id 是否在 m_ids 中,那就包括两种情况:
a. 若 row 的 trx_id 在m_ids(活跃事务列表)中,表示这个版本是由还没提交的事务生成的,不可见(若 row 的 trx_id 就是当前自己的事务是可见的); b. 若 row 的 trx_id 不在m_ids(活跃事务列表)中,表示这个版本是已经提交了的事务生成的,可见。
六、原理分析
1、RC-读已提交的隔离级别分析
RC隔离级别是解决脏读。在事务提交之后才能读到修改后的记录。这个是怎么做到呢,以下图为例子,事务5在每一次快照读的时候都会生成一个readview

根据undo log版本链记录和readview的匹配规则,我们来分析以下,事务5第一个查询,获取的结果是哪一个呢?

按照上图中DB_TRX_ID为4,和事务5的第一次查询的readview进行对比,发现条件1、条件2、条件3、条件4都不匹配。
然后匹配下一条,DB_TRX_ID为3,和事务5的第一次查询的readview进行对比,发现条件1、条件2、条件3、条件4都不匹配。
然后匹配下一条,DB_TRX_ID为2,和事务5的第一次查询的readview进行对比,发现条件2是匹配的。那就说明此快照读,返回的是此版本链的数据。
事务5第二个查询,获取的结果是哪一个呢?

按照上图中DB_TRX_ID为4,和事务5的第二次查询的readview进行对比,发现条件1、条件2、条件3、条件4都不匹配。
然后匹配下一条,DB_TRX_ID为3,和事务5的第二次查询的readview进行对比,发现条件2是匹配的。那就说明此快照读,返回的是此版本链的数据。
2、RR-重复读隔离级别分析
RR隔离级别下,仅在事务中第一次执行快照读时生成ReadView,后续复用该ReadView。 而RR 是可 重复读,在一个事务中,执行两次相同的select语句,查询到的结果是一样的。以下图为例子,事务5在第一次快照读的时候会生成一个readview,后面就会服用此readview。上述已经分析过每次查询的记录在undo log中DB_TRX_ID为2的版本链是匹配的

七、总结
MVCC使用隐藏字段、undo log,readview实现的,没修改一次都会在undo log中生成一个版本记录,对于同一条记录会生成一个版本链,每条记录中包含隐藏字段。
而readview则是在每次快照读的时候生成,通过版本链中的trx_id字段,对比readview中的记录,通过readview的规则,判断哪条版本记录是匹配的,从而得到最终结果。所以MVCC+锁实现了事务的隔离性
八、RC RR
Read View 用于支持 RC(Read Committed,读已提交)和 RR(Repeatable Read,可重复读)隔离级别的实现,所以 SELECT 在 RC 和 RR 隔离级别使用 MVCC 读取记录
RR、RC 生成时机:
-
RC 隔离级别下,每次读取数据前都会生成最新的 Read View(当前读)
-
RR 隔离级别下,在第一次数据读取时会创建 Read View(快照读), 第二次读取时会复用第一次的Read View(快照读)
RC、RR 级别下的 InnoDB 快照读区别
-
RC 级别下,事务中每次快照读都会新生成一个 Read View,这就是在 RC 级别下的事务中可以看到别的事务提交的更新的原因
-
RR 级别下,某个事务的对某条记录的第一次快照读会创建一个 Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,使用的是同一个 Read View,所以一个事务的查询结果每次都是相同的
RR 级别下,通过 START TRANSACTION WITH CONSISTENT SNAPSHOT 开启事务,会在执行该语句后立刻生成一个 Read View,不是在执行第一条 SELECT 语句时生成(所以说 START TRANSACTION 并不是事务的起点,执行第一条语句才算起点)
解决幻读问题:
-
快照读:通过 MVCC 来进行控制的,在可重复读隔离级别下,普通查询是快照读,是不会看到别的事务插入的数据的,但是并不能完全避免幻读
-
当前读:通过 临建锁(行锁 + 间隙锁)来解决问题
场景:RR 级别,T1 事务开启,创建 Read View,此时 T2 去 INSERT 新的一行然后提交,然后 T1 去 UPDATE 该行会发现更新成功,并且把这条新记录的 trx_id 变为当前的事务 id,所以对当前事务就是可见的。
因为 Read View 并不能阻止事务去更新数据,更新数据都是先读后写并且是当前读,读取到的是最新版本的数据

浙公网安备 33010602011771号