MySQL InnoDB的MVCC实现机制
- 1.MVCC概述
- 2.MVCC的实现原理
- 隐式字段
- undo日志
- Read View(读视图)
- RR隔离级别的Read View方案
1.MVCC概述
什么是MVCC?
MVCC,即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存
MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读
什么是当前读和快照读?
- 当前读
像select lock in share mode
(共享锁),select for update ; update, insert ,delete
(排他锁)这些操作都是一种当前读,就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁 - 快照读
像不加锁的select操作就是快照读,即不加锁的非阻塞读,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本
说白了MVCC就是为了实现读-写冲突不加锁,而这个读指的就是快照读, 而非当前读,当前读实际上是一种加锁的操作,是悲观锁的实现😯
总之,MVCC就是因为大牛们,不满意只让数据库采用悲观锁这样性能不佳的形式去解决读-写冲突问题,而提出的解决方案,所以在数据库中,因为有了MVCC,所以我们可以形成两个组合:
- MVCC + 悲观锁 MVCC解决读写冲突,悲观锁解决写写冲突
- MVCC + 乐观锁 MVCC解决读写冲突,乐观锁解决写写冲突
这种组合的方式就可以最大程度的提高数据库并发性能,并解决读写冲突,和写写冲突导致的问题!
2.MVCC的实现原理
它的实现原理主要是依赖记录中的 4个隐式字段,undo日志 ,Read View 来实现的😪
隐式字段
每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID
等字段
DB_ROW_ID
6byte, 隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID
产生一个聚簇索引DB_TRX_ID
6byte, 最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务IDDB_ROLL_PT
R 7byte, 回滚指针,指向这条记录的上一个版本(存储于rollback segment里)DELETED_BIT
1byte, 记录被更新或删除,并不代表真的删除,而是删除flag变了
DB_ROW_ID
是数据库默认为该行记录生成的唯一隐式主键;DB_TRX_ID
是当前操作该记录的事务ID;而DB_ROLL_PTR
是一个回滚指针,用于配合undo日志,指向上一个旧版本;delete flag没有展示出来😫
undo日志
InnoDB把这些为了回滚而记录的这些东西称之为undo log。这里需要注意的一点是,由于查询操作(SELECT)并不会修改任何用户记录,所以在查询操作执行时,并不需要记录相应的undo log。undo log主要分为3种:
Insert undo log
:插入一条记录时,至少要把这条记录的主键值记下来,之后回滚的时候只需要把这个主键值对应的记录删掉就好了。Update undo log
:修改一条记录时,至少要把修改这条记录前的旧值都记录下来,这样之后回滚时再把这条记录更新为旧值就好了。Delete undo log
:删除一条记录时,至少要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了。
删除操作都只是设置一下老记录的
DELETED_BIT
,并不真正将过时的记录删除🥱
对MVCC有帮助的实质是update undo log
,undo log实际上就是存在rollback segment中旧记录链,它的执行流程如下:
- 比如一个有个事务插入persion表插入了一条新记录,记录如下,name为Jerry, age为24岁,隐式主键是1,事务ID和回滚指针,我们假设为NULL
- 现在来了一个事务1对该记录的name做出了修改,改为Tom
- 在事务1修改该行(记录)数据时,数据库会先对该行加排他锁
- 然后把该行数据拷贝到undo log中,作为旧记录,即在undo log中有当前行的拷贝副本
- 拷贝完毕后,修改该行name为Tom,并且修改隐藏字段的事务ID为当前事务1的ID, 我们默认从1开始,之后递增,回滚指针指向拷贝到undo log的副本记录,即表示我的上一个版本就是它
- 事务提交后,释放锁
- 又来了个事务2修改person表的同一个记录,将age修改为30岁
- 在事务2修改该行数据时,数据库也先为该行加锁
- 然后把该行数据拷贝到undo log中,作为旧记录,发现该行记录已经有undo log了,那么最新的旧数据作为链表的表头,插在该行记录的undo log最前面
- 修改该行age为30岁,并且修改隐藏字段的事务ID为当前事务2的ID, 那就是2,回滚指针指向刚刚拷贝到undo log的副本记录
- 事务提交,释放锁
同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线性表,即链表,undo log的链首就是最新的旧记录,链尾就是最早的旧记录
Read View(读视图)
Read View主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,即可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。
Read View遵循一个可见性算法,主要是将要被修改的数据的最新记录中的DB_TRX_ID
(即当前事务ID)取出来,与系统当前其他活跃事务的ID去对比(由Read View维护),如果DB_TRX_ID
跟Read View的属性做了某些比较,不符合可见性,那就通过DB_ROLL_PTR
回滚指针去取出Undo Log中的DB_TRX_ID
再比较,即遍历链表的DB_TRX_ID
(从链首到链尾,即从最近的一次修改查起),直到找到满足特定条件的DB_TRX_ID
, 那么这个DB_TRX_ID
所在的旧记录就是当前事务能看见的最新老版本
上述语言可能难以理解,我们来看下面几张图来辅助一下理解:
首先,Read View维护的视图到底是怎样的?(这里以RC隔离级别举例)
首先,
m_ids
为当前系统正在活跃的事务列表(即事务没有被提交)min_trx_id
为活跃事务列表的最小值(即开启时间最靠前的一个事务)max_trx_id
为活跃事务列表的最大值(即开启时间最靠后的一个事务)create_trx_id
为当前快照读所在的事务
那么这个判断条件是什么呢?
很简单
RR隔离级别的Read View方案
RR隔离级别解决可重复读的问题,本质上是通过对Read View视图的复用来实现的😴
也就是说,对于在一个事务中不同的快照读,共享一个最早的Read View视图,以保证数据的一致性!