简介

MVCC 是 MySql处理读写冲突的一种机制,全称 :Multi-Version Concurrency Control 多版本控制,目的在于提高数据库高并发场景下的吞吐性能。

为什么使用它

  1. 我们都知道InnoDB支持事务,并且多个事务可以并行执行,那么就会出现我们常说的 脏读、更新丢失、不可重复读、幻读的问题,如果我们只是使用行级锁和表锁,那么效率就是串行的,java里有读写锁,那么InnoDB为啥不能有一种在你写的时候,我也能读,读写互不干扰的一种机制呢,这就出现了MVCC,既可以解决脏读、不可重复读等事务隔离问题,又可以解决并发读-写不阻塞的的问题。

实现原理

MVCC的实现是通过 版本链、undo_log、ReadView来实现的,那么版本链是什么意思呢,就是InnoDB在存数据的时候每一行记录都有两个隐藏列:DB_TRX_ID(最近修改(修改/插入)事务ID)、DB_ROLL_PTR(回滚指针)如果没有主键,则还会多一个隐藏的主键列 DB_ROW_ID,通过回滚指针就可以形成一个链状的数据结构.
如下图👇:
MVCC 解析 - 图1
那么这个东西存在哪里呢,没错、就是Undo_Log 日志 。假如我们一开始通过事务插入一条记录,事务ID为10,后面通过三次事务更新原来的记录,应为每次修改都会提交一个记录到undo_log,这些log连起来就形成了上图的一个链状结构。
我们知道了 DB_ROLL_PTR 是用了查找版本的,undo_log是用来存记录的,那么DB_TRX_ID 事务ID和ReadView是用来干嘛的呢?


ReadView

  1. <br /> ReadView就是当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。 <br /> **Read View有几个属性**
  • trx_ids: 当前系统活跃(未提交)事务版本号集合。
  • low_limit_id: 创建当前read view 时“当前系统最大事务版本号+1”。
  • up_limit_id: 创建当前read view 时“系统正处于活跃事务最小版本号”
  • creator_trx_id: 创建当前read view的事务版本号;


    数据可见公式:

    1. ![](https://cdn.nlark.com/yuque/__latex/587be1f43f5458233bec259981b676ef.svg#card=math&code=trxid%20%3C%20uplimitid%20%20%7C%7C%20%20trxid%20%3D%3D%20creatortrxid&id=lUU22)

**注:事务执行过程中,只有在第一次真正修改记录时(比如使用INSERT、DELETE、UPDATE语句),才会被分配一个单独的事务id,这个事务id是递增的。**

  1. 这么讲也可能比较难懂,我们上面提到过,并发事务会出现 脏读、不可重复读的问题,在READ COMMITED级别可以解决脏读,在REPEATABLE READ级别可以解决不可重复读,那么MVCC是如何解决这些问题的,下面通过几个案例来讲解:<br />**记住 MVCC 只能在 READ COMMITED 读已提交,REPEATABLE READ 重复读 两种隔离级别中起作用 **

在 RC 级别处理方式

时间点 事务A 事务B
T1 begin transaction
T2 update table set X = 吴呵呵 begin transaction
T3
select X from table x= 李哈哈
T4
commit
T5
T6 commit

很明显在事务B 的查询语句中没有读到 X = 吴呵呵,如果X读到了吴呵呵,由于事务A还未提交,如果事务A此时回滚,那么事务B读的X就是错误的,这就是我们说的脏读问题,那么MVCC是如何解决的呢?

事务A的ID 为20,事务B在select操作时,ReadView会将所有未提交的事务id 放入 m_ids 中,此时为[20], 所以 up_limit_id 为 20,而 trx_ids 为 20,所以undo_log中的吴呵呵不能显示,就会往上一条数据找,上一条的事务id
为10,所以trx_id=10的这条记录满足,buzai m_ids中,并且trx_id < up_limit_id, 所以显示的还是李哈哈。

在 RR 级别处理方式

时间点 事务A 事务B
T1 begin transaction
T2 select X from table x= 李哈哈 begin transaction
T3
update table set X = 吴呵呵
T4 select X from table x= 李哈哈 commit
T5
T6 commit

在事务A 的第一次查询语句读到 X = 李哈哈,就算事务B对X进行了修改,我们在事务A中第二次查询也依旧是李哈哈。

事务B的ID 为20,事务A在select操作时,事务B以及开始了,所以ReadView会把20放入 m_ids 中,此时为[20], 所以 找的还是trx_id=10的这条记录。第二次select时,讲道理 事务B 以及commit了,m_ids应该要把20移除掉了,那么最新的事务可能是其他一个比20大的值了,所以读到的是 吴呵呵 这条数据,但现实情况不是这样的,这是因为 ReadView 在RR级别时的生成只会在第一次select的时候,也就是说,他只生成一次,后面的查询都会用第一次的ReadView的数据,所以尽管事务B提交了,但m_ids还是[20],所以返回的数据还是trx_id =10 的那条数据,这就是可重复读的实现

总结

  • 在RC级别下的事务中,每次select都会新生成一个快照和Read View, 这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因
  • 而在RR级别下,只会在执行第一个 SELECT 语句时,后续所有的 SELECT 都是复用这个 ReadView