简述 MySQL MVCC 的实现原理
# 什么是MVCC?
MVCC是在并发访问数据库时,通过对数据做多版本管理,避免因为写锁的阻塞而造成读数据的并发阻塞问题。
通俗的讲就是MVCC通过保存数据的历史版本,根据比较版本号来处理数据的是否显示,从而达到读取数据的时候不需要加锁就可以保证事务隔离性的效果。
在MySQL中,MVCC只在读取已提交(Read Committed)和可重复读(Repeatable Read)两个事务级别下有效。其是通过Undo日志中的版本链和ReadView一致性视图来实现的。
MVCC就是在多个事务同时存在时,SELECT语句找寻到具体是版本链上的哪个版本,然后在找到的版本上返回其中所记录的数据的过程。
- mvcc特性:读不加锁,读写不冲突。
- MVCC只在READ COMMITTED (提交读)、REPEATABLE READ (可重复读)下工作。不兼容其他隔离级别。
# Innodb MVCC实现的核心知识点
# 事务版本号
每次事务开启前都会从数据库获得一个自增长的事务ID,可以从事务ID判断事务的执行先后顺序。
# 表的隐藏列。
在数据表后加三个隐藏列,事物版本(DB_TRX_ID),回滚指针(DB_ROLL_PTR)、隐藏ID(DB_ROW_ID)。每开启一个事物版本号自增1;
- DB_TRX_ID: 事务ID,记录的是当前事务在做INSERT或UPDATE语句操作时的事务ID;
- DB_ROLL_PTR:指向上一个版本数据在undo log 里的位置指针,回滚指针,通过它可以将不同的版本串联起来,形成版本链。
- DB_ROW_ID: 隐藏ID ,当创建表没有合适的索引作为聚集索引时,会用该隐藏ID创建聚集索引;
# undo log
Undo log 主要用于记录数据被修改之前的日志,在表信息修改之前先会把数据拷贝到undo log 里,当事务进行回滚时可以通过undo log 里的日志进行数据还原。
Undo log 的用途
保证事务进行rollback时的原子性和一致性,当事务进行回滚的时候可以用undo log的数据进行恢复。
用于MVCC快照读的数据,在MVCC多版本控制中,通过读取undo log的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本。
# read view
在innodb 中每个事务开启后都会得到一个read_view。
副本主要保存了当前数据库系统中正处于活跃(没有commit)的事务的ID号,其实简单的说这个副本中保存的是系统中当前不应该被本事务看到的其他事务id列表。
# Read view 的几个重要属性
trx_ids: 当前系统活跃(未提交)事务版本号集合。
low_limit_id: 创建当前read view 时“当前系统最大事务版本号+1”。
up_limit_id: 创建当前read view 时“系统正处于活跃事务最小版本号”
creator_trx_id: 创建当前read view的事务版本号;
# Read view 匹配条件
- 数据事务ID <up_limit_id 则显示 如果数据事务ID小于read view中的最小活跃事务ID,则可以肯定该数据是在当前事务启之前就已经存在了的,所以可以显示。
- 数据事务ID>=low_limit_id 则不显示 如果数据事务ID大于read view 中的当前系统的最大事务ID,则说明该数据是在当前read view 创建之后才产生的,所以数据不予显示。
- p_limit_id <=数据事务ID<low_limit_id 则与活跃事务集合trx_ids里匹配
如果数据的事务ID大于最小的活跃事务ID,同时又小于等于系统最大的事务ID,这种情况就说明这个数据有可能是在当前事务开始的时候还没有提交的。
所以这时候我们需要把数据的事务ID与当前read view 中的活跃事务集合trx_ids 匹配:
- 如果事务ID不存在于trx_ids 集合(则说明read view产生的时候事务已经commit了),这种情况数据则可以显示。
- 如果事务ID存在trx_ids则说明read view产生的时候数据还没有提交,但是如果数据的事务ID等于creator_trx_id ,那么说明这个数据就是当前事务自己生成的,自己生成的数据自己当然能看见,所以这种情况下此数据也是可以显示的。
- 如果事务ID既存在trx_ids而且又不等于creator_trx_id那就说明read view产生的时候数据还没有提交,又不是自己生成的,所以这种情况下此数据不能显示。
- 不满足read view条件时候,从undo log里面获取数据
当数据的事务ID不满足read view条件时候,从undo log里面获取数据的历史版本,然后数据历史版本事务号回头再来和read view 条件匹配 ,直到找到一条满足条件的历史数据,或者找不到则返回空结果;
# 版本链
所有版本的数据都只会存一份,然后通过回滚指针连接起来,之后就是通过一定的规则找到具体是哪个版本上的数据就行了。
假设现在有一张account表,其中有id和name两个字段,那么版本链的示意图如下:
# MVCC原理
如果落在绿色区间(DB_TRX_ID < min_id):这个版本比min_id还小(事务ID是从小往大顺序生成的),说明这个版本在SELECT之前就已经提交了,所以这个数据是可见的。或者(这里是短路或,前面条件不满足才会判断后面这个条件)这个版本的事务本身就是当前SELECT语句所在事务的话,也是一样可见的;
如果落在红色区间(DB_TRX_ID > max_id):表示这个版本是由将来启动的事务来生成的,当前还未开始,那么是不可见的;
果落在黄色区间(min_id <= DB_TRX_ID <= max_id):这个时候就需要再判断两种情况: 如果这个版本的事务ID在ReadView的未提交事务数组中,表示这个版本是由还未提交的事务生成的,那么就是不可见的;
如果这个版本的事务ID不在ReadView的未提交事务数组中,表示这个版本是已经提交了的事务生成的,那么是可见的。
如果在上述的判断中发现当前版本是不可见的,那么就继续从版本链中通过回滚指针拿取下一个版本来进行上述的判断。
# 举例说明
假设当前事物版本为current_version
** select: ** 查询会自动加where条件 and current_version>=def_create_version and (DB_TRX_ID = 'undefined' or DB_TRX_ID>current_version ) delete: DB_ROW_ID = current_version update: 先执行delete再执行insert insert: 插入数据并且默认DB_TRX_ID = current_version and DB_ROW_ID = 'undefined'
# MVCC不存在幻读问题(RR级别的情况下)
首先确认一点MVCC属于快照读的,在进行快照读的情况下是不会对数据进行加锁,而是基于事务版本号和undo历史版本读取数据,其实上面的文章已经说得很清楚了,我们根据上面的MVCC流程来推导,无论如何在MVCC的情况下都是不会出现幻读的问题的,如下图。
1、开启事务1,获得事务ID为1。
2、事务1执行查询,得到readview。
3、开始事务2。
4、执行insert。
5、提交事务2。
6、执行事务1的第二次查询 (因为这里是RR级别,所以不会再去获得readview,还是使用第一次获得的readview)
7、最后得到的结果是,插入的数据不会显示,因为插入的数据事务ID大于等于 readview里的最大活跃事务ID。