MVCC
MVCC (Multiversion Concurrency Control),多版本并发控制,是Innodb实现事务回滚与并发的重要功能
目的:解决不可重复读,用于支持读已提交(RC)和可重复读(RR)隔离级别的实现
数据库的几种并发场景
场景 | 是否存在问题 |
---|---|
读读 | 不存在数据安全问题 |
写写 | 有数据安全问题,更新丢失 |
读写 | 有线程安全问题,脏读、幻读、不可重复读(MVCC就是为了解决这种问题而存在的) |
如果是RC隔离级别,那么每次在进行快照读的时候都会生成一个新的readview
如果是RR隔离级别,那么只有在当前事务第一次进行快照读的时候会生成readview,之后的快照读都会沿用当前的readview
RC和RR之间的区别就在于生成readview的时机不同
MVCC主要是用来解决【读-写】冲突的无锁并发控制,可以解决以下问题:
- 在并发读写数据时,可以做到在读操作时不用阻塞写操作,写操作不用阻塞读操作,提高数据库并发读写的性能。
- 可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决【写-写】引起的更新丢失问题。
MVCC与锁的组合
一般数据库中都会采用以上MVCC与锁的两种组合来解决并发场景的问题,以此最大限度的提高数据库性能。
- MVCC + 悲观锁MVCC解决读-写冲突,悲观锁解决写-写冲突。
- MVCC + 乐观锁MVCC解决读-写冲突,乐观锁解决写-写冲突。
通过上述描述,MVCC的作用可以概括为就是为了解决【读写冲突】,提高数据库性能的,而MVCC的实现又依赖【隐式字段】【undo日志】【版本链】【快照读和当前读】【读视图】。
隐式字段
具体实现:在数据库的每一行中添加额外的三个字段:
隐式字段 | 描述 | 是否必须存在 |
---|---|---|
DB_TRX_ID | 事物Id,也叫事物版本号,占用6byte的标识,事务开启之前,从数据库获得一个自增长的事务ID,用其判断事务的执行顺序,记录插入或更新该行的最后一个事务的事务ID | 是 |
DB_ROLL_PTR | 占用7byte,回滚指针,指向这条记录的上一个版本的undo log记录,存储于回滚段(rollback segment)中 | 是 |
DB_ROW_ID | 隐含的自增ID(隐藏主键),如果表中没有主键和非NULL唯一键时,则会生成一个单调递增的行ID作为聚簇索引 | 否 |
undo日志
一种用于撤销回退的日志,在事务开始之前,会先记录存放到 Undo 日志文件里,备份起来,当事务回滚时或者数据库崩溃时用于回滚事务。
undo日志的主要作用是事务回滚和实现MVCC快照读。
undo log日志分为两种:
insert undo log
代表事务在
insert
新记录时产生的undo log
, 仅用于事务回滚,并且在事务提交后可以被立即丢弃。update undo log
事务在进行
update
或delete
时产生的undo log
;不仅在事务回滚时需要,在实现MVCC快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被清理线程统一清除。
MVCC实际上是使用的update undo log
实现的快照读。
InnoDB 并不会真正地去开辟空间存储多个版本的行记录,只是借助 undo log 记录每次写操作的反向操作。所以B+ 索引树上对应的记录只会有一个最新版本,InnoDB 可以根据 undo log 得到数据的历史版本,从而实现多版本控制。
版本链
一致性非锁定读是通过 MVCC 来实现的。但是MVCC 没有一个统一的实现标准,所以各个存储引擎的实现机制不尽相同。InnoDB 存储引擎中 MVCC 的实现是通过 undo log 来完成的
当事务对某一行数据进行改动时,会产生一条Undo日志,多个事务同时操作一条记录时,就会产生多个版本的Undo日志,这些日志通过回滚指针(DB_ROLL_PTR)连成一个链表,称为版本链。
只要有事务写入数据时,就会产生一条对应的 undo log,一条 undo log 对应这行数据的一个版本,当这行数据有多个版本时,就会有多条 undo log 日志,undo log 之间通过回滚指针(DB_ROLL_PTR)连接,这样就形成了一个 undo log 版本链。
快照读与当前读
快照读
也叫普通读,读取的是记录数据的可见版本,不加锁,不加锁的普通select语句都是快照读,即不加锁的非阻塞读。
快照读的执行方式是生成 ReadView,直接利用 MVCC 机制来进行读取,并不会对记录进行加锁。
【可重复读隔离】级别下,普通的select读都是快照读。读的都是当前快照版本的数据,看不到其他版本的数据,其他事务操作的未提交时,记录的trx_id字段不会更新
1
select * from table;
快照的生成时间根据隔离级别的不同而有所不同
在读未提交隔离级别下,快照是什么时候生成的
没有快照,因为不需要,怎么读都读到最新的。不管是否提交
在读已提交隔离级别下,快照是什么时候生成的
SQL语句开始执行的时候。
在可重复读隔离级别下,快照是什么时候生成的
事务开始的时候
当前读
也称锁定读【Locking Read】,读取的是记录数据的最新版本,并且需要先获取对应记录的锁
除了普通select外的所有操作,如update、insert都会读取最新的数据
update/delete/insert/select…for update/select lock in share model
1
2
3
4
5
6
7
8
9
10# 共享锁
SELECT * FROM student LOCK IN SHARE MODE;
# 排他锁
SELECT * FROM student FOR UPDATE;
# 排他锁
INSERT INTO student values ...
# 排他锁
DELETE FROM student WHERE ...
# 排他锁
UPDATE student SET ...
读视图Read View
Read View提供了某一时刻事务系统的快照,主要是用来做可见性
判断, 里面保存了【对本事务不可见的其他活跃事务】。
当事务在开始执行的时候,会产生一个读视图(Read View),用来判断当前事务可见哪个版本的数据,即可见性判断。
实际上在innodb中,每个SQL语句执行前都会生成一个Read View。
读视图的四个属性
1 | class ReadView { |
事务进行快照读的时候会生成一个读视图,格式
属性 | 作用 |
---|---|
creator_trx_id | 创建当前read view的事务ID |
m_ids | 当前系统中所有的活跃事务的 id,活跃事务指的是当前系统中开启了事务,但还没有提交的事务; |
m_low_limit_id | 高水位线:id大于等于 m_low_limit_id 的事务都不可见。 在生成快照时,它被赋值为“下一个待分配的事务ID”(会大于所有已分配的事务ID) |
m_up_limit_id | 低水位线:id小于m_up_limit_id的事务都不可见。 它是活跃事务ID列表的最小值,在生成快照时,小于m_up_limit_id的事务都已经提交(或者回滚)。 |
若trx_ids为空,则up_limit_id = low_limit_id
ReadView 会根据这 4 个属性,结合 undo log 版本链,来实现 MVCC 机制,决定一个事务能读取到数据那个版本。
读视图可见性查询判断规则
当一个事务读取某条数据时,会通过DB_TRX_ID【Uodo日志的事务Id】在坐标轴上的位置来进行可见性规则判断,如下:
DB_TRX_ID 等于creator_trx_id
表明数据【Undo日志】 是自己生成的,因此是可见的(自己做的Insert/Update,若是DELETE则自己不可见)
DB_TRX_ID >= m_up_limit_id
表示在当前事务【creator_trx_id】开启以后,有新的事务开启,并且新的事务修改了这行数据的值并提交了事务,因为这是【creator_trx_id】后面的事务修改提交的数据,所以当前事务【creator_trx_id】是不能读取到的。
DB_TRX_ID < m_low_limit_id
表示DB_TRX_ID对应这条数据【Undo日志】是在当前事务开启之前,其他的事务就已经将该条数据修改了并提交了事务(事务的 id 值是递增的),所以当前事务【开启Read View的事务】能读取到。
m_up_limit_id =< DB_TRX_ID < m_low_limit_id
DB_TRX_ID 在 m_ids 数组中
表示DB_TRX_ID【写Undo日志的事务】 和当前事务【creator_trx_id】是在同一时刻开启的事务, 如果DB_TRX_ID != creator_trx_id,且DB_TRX_ID还存活,说明 DB_TRX_ID事务修改了数据的值但未提交,所以当前事务【creator_trx_id】不能读取到。
DB_TRX_ID 不在 m_ids 数组中
表示的是在当前事务【creator_trx_id】开启之前,其他事务【DB_TRX_ID】将数据修改后就已经提交了事务,所以当前事务能读取到。
不同隔离级别MVCC实现原理
MVCC实现原理
InnoDB 实现MVCC是通过 Read View与Undo Log
实现的,Undo Log 保存了历史快照,形成版版本链,Read View可见性规则判断当前版本的数据是否可见。
InnnoDB执行查询语句的具体步骤为:
- 执行语句之前获取查询事务自己的事务Id,即事务版本号。
- 通过事务id获取Read View
- 查询存储的数据,将其事务Id与Read View中的事务版本号进行比较
- 不符合Read View的可见性规则,则读取Undo log中历史快照数据
- 找到当前事务能够读取的数据返回
而在实际的使用过程中,Read View在不同的隔离级别下是得工作方式是不一样。
读已提交(RC)MVCC实现原理
在读已提交(Read committed)的隔离级别下实现MVCC,同一个事务里面,【每一次查询都会产生一个新的Read View副本】,这样可能造成同一个事务里前后读取数据可能不一致的问题(不可重复读并发问题)。
假设事务A、事务B同时执行,事务B执行update a = 10 操作
事务开启时,Read View 记录情况(如图):
事务A的Read View中,它的事务id时是1001,由于与事务B同时执行,所以此时活跃的事务的事务id列表(m_ids)为【1001,1002】,活跃的事务id中最小的为事务A的事务id :1001;下一事务id为1003
事务B的Read View中,它的事务id时是1002,由于与事务A同时执行,所以此时活跃的事务的事务id列表(m_ids)为【1001,1002】,活跃的事务id中最小的为事务A的事务id :1001;下一事务id为1003此时,事务A查询id为1的记录,找到记录后,发现该记录的trx_id 为1000,通过和自己的Read View中的m_ids中的事务id比较,发现该事务id不在活跃的事务id列表中,并且小于事务A的事务id,即该记录早在事务A之前已经提交,因此该记录对事务A可见
事务B通过update操作将字段a的值修改为10,但未提交事务,此时最新的记录的trx_id为1002。如图
然后事务A再次读取该记录时,发现记录的trx_id变为1002,比自己的事务id大,并且比下一个事务id 1003小,即事务A读取到的是和自身同时执行的事务B提交的数据。这是事务A并不会读取这条记录,而是沿着undo log的版本链往下寻找,知道找到trx_id 小于等于自身事务id的第一条记录,即a字段值为1的这条记录
此时事务B提交后,事务A再次读取到该记录,发现记录的trx_id比自身的事务id大,并且不在活跃事务的id列表中,则说明事务B已经提交事务,则该记录可以被读取
可重复读(RR)MVCC实现原理
在可重复读(Repeatable read)的隔离级别下实现MVCC,【同一个事务里面,多次查询,都只会产生一个共用Read View】,以此不可重复读并发问题。
假设事务A、事务B同时执行,事务B执行update a = 10 操作
事务开启时,Read View 记录情况(如图):
事务A的Read View中,它的事务id时是1001,由于与事务B同时执行,所以此时活跃的事务的事务id列表(m_ids)为【1001,1002】,活跃的事务id中最小的为事务A的事务id :1001;下一事务id为1003事务B的Read View中,它的事务id时是1002,由于与事务A同时执行,所以此时活跃的事务的事务id列表(m_ids)为【1001,1002】,活跃的事务id中最小的为事务A的事务id :1001;下一事务id为1003
此时,事务A查询id为1的记录,找到记录后,发现该记录的trx_id 为1000,通过和自己的Read View中的m_ids中的事务id比较,发现该事务id不在活跃的事务id列表中,并且小于事务A的事务id,即该记录早在事务A之前已经提交,因此该记录对事务A可见
然后,事务B通过update操作将字段a的值修改为10,这是MySQL会记录undo log,并以链表的方式串联起来,形成版本链,如图所示,此前的记录就会变成旧版记录
然后事务A再次读取该记录时,发现记录的trx_id变为1002,比自己的事务id大,并且比下一个事务id 1003小,即事务A读取到的是和自身同时执行的事务B提交的数据。这是事务A并不会读取这条记录,而是沿着undo log的版本链往下寻找,知道找到trx_id 小于等于自身事务id的第一条记录,即a字段值为1的这条记录
当前读
除了普通select外的所有操作,如update、insert都会读取最新的数据
快照读
【可重复读隔离】级别下,普通的select读都是快照读。读的都是当前快照版本的数据,看不到其他版本的数据,其他事务操作的未提交时,记录的trx_id字段不会更新
案例总结:
通过上述案例说明,同一个事务A的两个相同查询,结果相同,因此在可重复读(RR)隔离级别下,解决了不可重复读并发问题。
其实读已经提交与可重复读的可见性判断的区别就在于事务A第二次查询时使用的Read View不通。
来源
https://www.modb.pro/db/397162
https://www.bilibili.com/video/BV1He4y177fa
https://blog.51cto.com/itzhoujun/2357430