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;表示这个版本数据就是由当前事务自己生成的,当前事务能读取到

  1. 如果 row 的 trx_id < min_id;表示这个版本数据,是当前事务开启之前还没执行,别的事务保持提交的,所以当前事务能读取到;

  2. 如果 row 的 trx_id > max_id;表示这个版本数据,是当前事务开启之后执行完了,别的事务保持提交的,所以当前事务不能能读取到;

  3. 如果 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 并不能阻止事务去更新数据,更新数据都是先读后写并且是当前读,读取到的是最新版本的数据

posted @ 2024-07-16 21:55  jock_javaEE  阅读(94)  评论(0)    收藏  举报