简介
MVCC 是 MySql处理读写冲突的一种机制,全称 :Multi-Version Concurrency Control 多版本控制,目的在于提高数据库高并发场景下的吞吐性能。
为什么使用它
我们都知道InnoDB支持事务,并且多个事务可以并行执行,那么就会出现我们常说的 脏读、更新丢失、不可重复读、幻读的问题,如果我们只是使用行级锁和表锁,那么效率就是串行的,java里有读写锁,那么InnoDB为啥不能有一种在你写的时候,我也能读,读写互不干扰的一种机制呢,这就出现了MVCC,既可以解决脏读、不可重复读等事务隔离问题,又可以解决并发读-写不阻塞的的问题。
实现原理
MVCC的实现是通过 版本链、undo_log、ReadView来实现的,那么版本链是什么意思呢,就是InnoDB在存数据的时候每一行记录都有两个隐藏列:DB_TRX_ID(最近修改(修改/插入)事务ID)、DB_ROLL_PTR(回滚指针)如果没有主键,则还会多一个隐藏的主键列 DB_ROW_ID,通过回滚指针就可以形成一个链状的数据结构.
如下图👇:
那么这个东西存在哪里呢,没错、就是Undo_Log 日志 。假如我们一开始通过事务插入一条记录,事务ID为10,后面通过三次事务更新原来的记录,应为每次修改都会提交一个记录到undo_log,这些log连起来就形成了上图的一个链状结构。
我们知道了 DB_ROLL_PTR 是用了查找版本的,undo_log是用来存记录的,那么DB_TRX_ID 事务ID和ReadView是用来干嘛的呢?
ReadView
<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的事务版本号;
数据可见公式:
**注:事务执行过程中,只有在第一次真正修改记录时(比如使用INSERT、DELETE、UPDATE语句),才会被分配一个单独的事务id,这个事务id是递增的。**
这么讲也可能比较难懂,我们上面提到过,并发事务会出现 脏读、不可重复读的问题,在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
