一、开场白
MVCC(Multi-Version Concurrency Control)即多版本并发控制,通俗讲就是“通过多个版本的记录来实现并发控制”。并发控制的定义所述者众多,此不赘述。
实现并发控制的方法有:锁,MVCC。这篇文化讲的就是MVCC
二、MVCC存在的意义
刚才说了,实现并发控制方法有锁和MVCC。众所周知,如果存在两个干同样事情的东西,我们都要分析对比其优劣。那么MVCC和锁有何优劣对比呢?再说直白一些,既然锁可以实现并发控制,为什么还需要MVCC呢?
因为纯锁的方式只实现了读-读兼容,而读-写,写-读,写-写都是冲突的,这就会产生较多的锁阻塞,并发效率低。 而我们亲爱的MVCC,它实现了读-写,写-读的兼容,大大减少了锁阻塞,提高了并发效率!
三、MVCC如何实现读和写兼容
1、四个隐藏字段
列名 | 字节数 | 含义 | 作用 |
DB_TRX_ID | 6 | 记录的版本号 | Insert或Update行的最后一个事务的事务ID。(Delete也视为更新,并需要将其标记为已删除) |
DB_ROLL_PTR | 7 | 回滚指针 | 指向最近的历史数据 |
DB_ROW_ID | 6 | 行隐藏主键 | 当表没有设置主键时,系统自动为其设置的隐藏主键 |
还有一个删除标志,当该行被执行Delete时,将此标志设为已删除状态,但是记录依然存在,要等事务提交后才会删除。
2、undo log
undo log即回滚日志,属于InnoDB的事务日志。虽然叫回滚日志,但是其作用却不仅仅限于回滚!
undo log记录了历史数据,这些历史数据的作用是:
(1)实现回滚,以保证事务的原子性
(2)实现MVCC,以实现事务的隔离性
本文只说第二点,即undo log在MVCC中发挥的作用
3、版本链
每开启一个事务,事务会用当前的系统版本号作为自己的事务ID,并且系统版本号都会增加1。举个例子:
想象这样一个场景,很多人进入一个会场,会场入口有一个发号员,入场者根据自己拿到的号去对号入座。发号员的发号规则是:按照递增顺序发号!即来一个人,发1号,再来一个人,发2号,又来一个人,发3号...以此类推。这样的结果就是,每个人拿到的号都不一样,各自做自己的位置。(理解了这个例子只后,就忘掉它,因为这个例子对后面的理解没用,甚至会误导你)
每次对记录进行update操作时,都会把旧的记录标记为已删除,写入undo日志中,并且把新记录的回滚指针指向这条旧记录,新纪录DB_TRX_ID字段设为当前事务ID。每次修改都这样操作,形成的版本链是这样的。
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAzNfRHLGZkRGZkRfJ3bs92YsYTMfVmepNHLyUFVPlXVE1UeRpHW4Z0MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnLzAjMzUzM0YTMyEjMxAjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
图片来自 这篇文章
4、MVCC中的Insert和Delete操作
上面说了MVCC中的Update操作的做法,那Insert和Delete操作是如何做的呢?
Insert操作会产生一条新的记录,其DATA_TRX_ID字段为当前事务的ID,其DATA_ROLL_PTR字段为空。
Delete操作会将当前行标记为已删除,DATA_TRX_ID字段为当前事务的ID。
5、MVCC实现可重复读
MVCC通过快照读实现可重复读,即在事务中第一条Select语句开始时生成一个ReadView,之后该事务中所有的简单Select(没有for update和lock in share model)都会在此ReadView上进行,并且对于返回的记录,必须满足:
(1)如果没有被标记为删除,其DATA_TRX_ID字段值小于或等于当前事务ID的记录。当满足此条件的记录有多个时,选DATA_TRX_ID字段值最大的(即最近的历史值)
(2)如果已经标记为删除,其DATA_TRX_ID字段值大于当前事务ID的记录。
6、读-写兼容
有了上述这些控制,在进行数据库读写时,读操作不用阻塞写操作,写操作不用阻塞读操作,而且不会产生并发问题。这里的写操作包括:Insert,Update,Delete
如有错误之处,感谢指出
四、遗留问题
1、MVCC实现方式(悲观锁,乐观锁)
2、undo log的具体写入和保存流程