为什么需要MVCC?
什么情况下才会用到MVCC?
MVCC是如何实现的?
背景
MVCC,英文全称是 Multiversion Concurrency Control,翻译过来就是多版本并发控制。
提到并发,那么出发点基本上都是效率问题,MVCC也不例外,其目的就是为了提高读写效率。
我们知道,数据库通常使用锁来实现隔离性,也就是说锁住资源之后会禁止其他线程访问该资源,但是一般情况下都是读多写少的场景,这也就催生了读写锁,因为往往读与读之间没有必要互斥,但是读写锁的效率依然不能满足大家日益增长的效率需求,能不能让读和写也不冲突呢?能!MVCC
做到了
机制
MVCC
将数据添加了版本的概念,每一个事务,将只会看到符合自己“权限”的特定版本的数据,通过这种方式,读与写也可以做到不冲突了。
这个所谓的“权限”,就是判断事务可见哪个数据版本的判断逻辑,这个逻辑在不同的隔离级别下也会有差异。
比如写入事务内的读取操作将能看到最新写入的版本的数据,但是其他事务只能看到之前版本的“快照”数据。
我们常听到的“快照读”和“当前读”,就是指读取快照数据 和 读取最新已提交数据。
不仅是MySQL
,包括Oracle
、PostgreSQL
等其他数据库系统也都实现了MVCC
,但各自的实现机制不尽相同,因为MVCC
没有一个统一的实现标准,典型的有乐观(optimistic)并发控制和悲观(pessimistic)并发控制。
我们这里讨论的是MySQL
中InnoDB
的MVCC
实现原理
如何存储
InnoDB
中的每个数据行,其实不只是我们自己定义的字段,还有一些隐藏的字段,而用以实现MVCC
的就是三个隐藏字段:
DB_TRX_ID
:6 byte,是最近修改(修改/插入)事务 ID,记录创建这条记录/最后一次修改该记录的事务 ID。也就是上面所谓的“版本号”。
DB_ROLL_PTR
:7 byte,回滚指针,指向这条记录的上一个版本,即undolog
中的回滚记录。
DB_ROW_ID
:6 byte,隐含的自增 ID(隐藏主键),如果数据表没有主键,InnoDB 会自动以此ID产生一个聚簇索引。
- 在Insert操作时,只会记入当前事务ID,因为是新插入,所以不会有回滚地址。
- 在update操作时,旧数据行不变,会复制一个新的行,记入当前事务ID,并将回滚地址中填入本数据行上一个版本的undolog日志的地址,便于寻找上一个版本的数据。
- 在delete操作时,与update操作类似,此时不会真正删除数据,只是用另一个隐藏字段记录删除状态。
- select操作只是读取相应的数据,不会产生数据副本。
而之前所说的“版本”的概念,就是用事务ID来标识的,用事务ID当做版本号,来决定哪些事务可以看到哪个版本的数据。而这每个版本的数据就是复用undolog来存储的。每个事务查看数据的可见性的过程,也就是选择可见版本的过程。
如何工作
说到MVCC
具体如何工作,还需要引入一个概念,Read View
读视图。
ReadView
中主要就是有个列表来存储我们系统中当前活跃着的读写事务,也就是begin了还未提交的事务。通过这个列表来判断记录的某个版本是否对当前事务可见。其中最主要的与可见性相关的属性如下:
up_limit_id
:The read should see all trx ids which are strictly smaller (<) than this value. In other words, this is the low water mark".
当前已经提交的事务号 + 1,事务号 < up_limit_id
,对于当前Read View
都是可见的。理解起来就是创建Read View
视图的时候,之前已经提交的事务对于该事务肯定是可见的。
low_limit_id
:The read should not see any transaction with trx id >= this value.
In other words, this is the "high water mark".
当前最大的事务号 + 1,事务号 >= low_limit_id
,对于当前Read View
都是不可见的。理解起来就是在创建Read View
视图之后创建的事务对于该事务肯定是不可见的。
trx_ids
:Set of RW transactions that was active when this snapshot was taken.
活跃事务id列表,即Read View
初始化时当前未提交的事务列表。所以当进行RR读的时候,trx_ids
中的事务对于本事务是不可见的(除了自身事务,自身事务对于表的修改对于自己当然是可见的)。理解起来就是创建RV时,将当前活跃事务ID记录下来,后续即使他们提交对于本事务也是不可见的。
举个栗子,说明下工作过程:
假设,此时,1、2事务已经提交,3事务尚未提交
此时如果有一个事务4来访问此字段,那么此时,事务4的Read View
中,事务3还未提交,在trx_ids
中,属于不可见事务,而事务1、2均已提交,小于up_limit_id
,则属于可见范围,所以能够看到value是222
再假如说,此时事务3提交了,又有一个事务5来更新此字段value为555,那么此时事务4的第二次查询行为将会看到什么呢?这里就会体现出不同隔离级别下的不同行为:
- 假如隔离级别为可重复读,那么
Read View
只会在第一次查询时生成,且在整个事务中只会生成这一次,所以依然只能读取到value为222 - 假如隔离解蔽为已提交读,那么
Read View
会在再次查询时重新生成,所以此时将会能够看到value已经变成了333
而根据上面的描述,我们能够明白MVCC
的另外一个特性:
MVCC
只会在已提交读和可重复读下生效
- 对于未提交读的事务来说,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本即可
- 对于可串行化的事务来说,select语句也需要使用加锁的方式来访问记录,所以根本不需要`MVCC
意义
1. 读写之间阻塞的问题
通过MVCC
可以让读写互相不阻塞,即读不阻塞写,写不阻塞读,这样就可以提升事务并发处理能力。
提高并发的演进思路:
- 普通锁,只能串行执行;
- 读写锁,可以实现读读并发;
- 数据多版本并发控制,可以实现读写并发。
2. 降低了死锁的概率
因为InnoDB
的MVCC
采用了乐观锁的方式,读取数据时并不需要加锁,对于写操作,也只锁定必要的行
3. 解决一致性读的问题
一致性读也被称为“快照读”,当我们查询数据库在某个时间点的快照时,只能看到这个时间点之前事务提交更新的结果,而不能看到这个时间点之后事务提交的更新结果。
引申讨论 - InnoDB的RR级别是否解决了幻读?
InnoDB的RR级别下的情况是,在“快照读”下,因为MVCC
和间隙锁的存在,确实不再会出现幻读的情况,但是如果是“当前读”下,幻读情况将会是依然存在的。
这个问题确实很有意思,相比之下,结果并不是那么重要,大家了解到原理就好~
相关文章
https://github.com/Yhzhtk/note/issues/42
http://mysql.taobao.org/monthly/2017/06/07/
https://tech.meituan.com/2014/08/20/innodb-lock.html
引用
- https://github.com/zhangyachen/zhangyachen.github.io/issues/68
- https://baijiahao.baidu.com/s?id=1629409989970483292&wfr=spider&for=pc
- https://zhuanlan.zhihu.com/p/66791480
- https://blog.csdn.net/qq_35190492/article/details/109044141
- https://blog.csdn.net/SnailMann/article/details/94724197
- https://segmentfault.com/a/1190000037557620
- https://zhuanlan.zhihu.com/p/377162929
- https://github.com/hanchuanchuan/bingo2sql
- https://zhuanlan.zhihu.com/p/35574452
- https://www.modb.pro/db/75331
- https://juejin.cn/post/7001357238648438821