MVCC
MVCC概述
MVCC,全称 Multi-Version Concurrency Control ,即多版本并发控制。MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。
MVCC就是在ReadCommitted(读已提交)、RepeatableRead(可重复读) 隔离级别,不加锁的情况下,解决并发事务的脏读、幻读和不可重复读问题。
MVCC 在 MySQL InnoDB 中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。
什么是当前读、快照读?
当前读: 读到的数据都是最新的数据,像排他锁(又称写锁update
、insert
、delete
、select for update
)、共享锁(又称读锁select lock in share mode
),这些操作都是当前读。
快照读: 可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。像不加锁的select操作就是快照读
,如果隔离级别是串行化,快照读会退化成当前读。
数据库并发场景
- 读-读: 不存在任何问题。
- 读-写: 有线程安全问题,可能出现脏读、幻读、不可重复读。
- 写-写: 有线程安全问题,可能存在更新丢失等。
MVCC(多版本并发控制)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。
MVCC实现原理
MVCC 的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的 3个隐式字段,undo日志版本链 , Read View 来实现的。
三个隐式字段
数据库中的每行记录都有DB_TRX_ID(事务id)
、DB_ROLL_PTR(回滚指针)
、DB_ROW_ID(隐藏主键)
这三个字段。
DB_TRX_ID(事务id) | DB_ROLL_PTR(回滚指针) | DB_ROW_ID(隐藏主键) |
---|---|---|
记录创建这条记录,最后一次修改该记录事务的ID。每处理一个事务,其值自动 +1 | 回滚指针,指向这条记录的上一个版本(存储于 rollback segment 里) | 隐含的自增 ID(隐藏主键),如果数据表没有主键,InnoDB 会自动以DB_ROW_ID产生一个聚簇索引 |
undo日志版本链
undo日志版本链是指一行数据被多个事务依次修改过后,在每个事务修改完后,Mysql会保留修改前的数据undo回滚日志,并且用两个隐藏字段DB_TRX_ID(事务id)
和DB_ROLL_PTR(回滚指针)
把这些undo日志串联起来形成一个历史记录版本链。
undo日志分类:
undo log 主要分为两种:
insert undo log
代表事务在insert
新记录时产生的undo log
,只在事务回滚时需要,并且在事务提交后可以被立即丢弃update undo log
事务在进行update
或delete
时产生的undo log
;不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被 purge 线程统一清除。
undo日志版本链流程图
1)事务A,在employee表插入了一条数据,name为zhangsan、age为22、主键id为1、事务id为100、回滚指针和隐式主键为null。这条日志会在insert undo log中。
2)事务B对employee表主键id为1的记录做了修改,修改了name字段为lisi。(在事务B之前都没有事务修改过这条数据)
- 拷贝改行数据到undo log中,作为旧纪录,既在 undo log 中有当前行的拷贝副本。
- 修改name字段为lisi,假设事务id自增到101,回滚指针指向拷贝的副本。
3)事务C对employee表主键id为1的记录做了修改,修改了age字段为18。
- 在拷贝数据到undo log中,发现这行数据已经有undo日志版本链了。修改后的数据,假设事务id为102。回滚指针就会指向undo日志版本链最后修改的那一条记录。
Read View 读视图
在可重复读隔离级别,当事务开启,执行任何查询sql时会生成当前事务的一致性视图read-view,该视图在事务结束之前永远都不会变化
(如果是读已提交隔离级别在每次执行查询sql时都会重新生成read-view),这个视图由执行查询时所有未提交事务id数组(数组里最小的id为min_id)和已创建的最大事务id+1(max_id)组成
,事务里的任何sql查询结果需要从对应版本链里的最新数据开始逐条跟read-view做比对从而得到最终的快照结果。
Read View 主要是用来做可见性判断的,可见性算法如下 (trx_id事务id):
绿色部分( trx_id<min_id
):已提交的事务
红色部分( trx_id>max_id
):未开始的事务
黄色部分(min_id <=trx_id<= max_id
):活跃的事务。
- 如果
row
的trx_id
落在绿色部分(trx_id<min_id
),表示这个版本是已提交的事务生成的,这个数据是可见的; - 如果
row
的trx_id
等于当前事务的trx_id
,这个数据是可见的; - 如果
row
的trx_id
落在红色部分(trx_id>=max_id
),表示这个版本是由将来启动的事务生成的,是不可见的; - 如果
row
的trx_id
落在黄色部分(min_id <=trx_id<max_id
),那就包括两种情况
a. 若row
的trx_id
在视图数组中,表示这个版本是由还没提交的事务生成的,不可见;
b. 若row
的trx_id
不在视图数组中,表示这个版本是已经提交了的事务生成的,可见;
对于删除的情况可以认为是update
的特殊情况,会将版本链上最新的数据复制一份,然后将trx_id
修改成删除操作的trx_id
,同时在该条记录的头信息(record header
)里的(deleted_flag
)标记位写上true
,来表示当前记录已经被删除,在查询时按照上面的规则查到对应的记录如果delete_flag
标记位为true
,意味着记录已被删除,则不返回数据。
关于readview和可见性算法的原理解释
readview
和可见性算法其实就是记录了sql
查询那个时刻数据库里提交和未提交所有事务的状态。
要实现RR隔离级别,事务里每次执行查询操作readview
都是使用第一次查询时生成的readview
,也就是都是以第一次查询时当时数据库里所有事务提交状态来比对数据是否可见,当然可以实现每次查询的可重复读的效果了。
要实现RC隔离级别,事务里每次执行查询操作readview
都会按照数据库当前状态重新生成readview
,也就是每次查询都是跟数据库里当前所有事务提交状态来比对数据是否可见,当然实现的就是每次都能查到已提交的最新数据效果了。
RepeatableRead(可重复读)MVCC可见性算法示例
假设有一张account
表,account
表有主键id
、name
、balance
三个字段。最开始这张表里面的数据是:
1)事务trx_id = 100,修改了balance
字段为200,此时事务100还未提交。
2)事务trx_id = 200,修改了balance
字段为300,此时事务200还未提交。
3)事务trx_id = 300,修改了balance
字段为500,此时事务300已经提交。
此时的undo log
日志版本链如下图:
4)事务trx_id = 400,执行select语句查询id=1,balance
字段的数据,数据库为该行数据生成一个Read View读视图,假设数据看没有其它事务了,此时的readview数组:[100,200],min_id:100,max_id:400+1 = 401
此时的row最新的trx_id为300,因为事务400未作修改操作。从undo log日志版本链最后修改的,一直往上查询。根据上面的可见性算法分析:
- 先判断
trx_id<min_id(300<100)
不满足条件 - 再判断
trx_id=create_trx_id(当前事务id
),(300=400)
不满足条件 - 再判断
trx_id>=max_id(300>=401)
不满足条件 - 再判断
min_id <=trx_id<max_id(100<=300<401)
满足条件 - trx_id=300,很显然不在readview数组[100,200]中,可见
得出结论:事务400查询的数据balance
字段为500
5)事务trx_id = 200,又修改了balance
字段为800,此时事务200已提交。
此时的undo log
日志版本链如下图:
6)事务trx_id = 400,又执行select语句查询id=1,balance
字段的数据。在可重复读隔离级别下,不会重新生成Read View读视图,还是用的之前的Read View读视图。此时的readview数组:[100,200],min_id:100,max_id:400+1 = 401
此时的row最新的trx_id为200,同步骤4分析:
- 先判断
trx_id<min_id(200<100)
不满足条件 - 再判断
trx_id=create_trx_id(当前事务id)
,(200=400)
不满足条件 - 再判断
trx_id>=max_id(200>=401)
不满足条件 - 再判断
min_id <=trx_id<max_id(100<=200<401)
满足条件 - trx_id=200,很显然在readview数组[100,200]中,不可见
上面判断出最新修改的undo log
日志不满足,继续往上查询,同理可以推断出,事务400查询的数据balance
字段为500
ReadCommitted(读已提交)MVCC可见性算法示例
读已提交和可重复读是类似的,读已提交就是每次执行select,就会重新生成Read View读视图。
上面的我就不复制了,唯一不同的就是步骤6,
6)事务trx_id = 400,又执行select语句查询id=1,balance字段的数据。当前的活跃事务就只剩下事务100了,此时的readview数组:[100],min_id:100,max_id:400+1 = 401
此时的row最新的trx_id为200:
- 先判断
trx_id<min_id(200<100)
不满足条件 - 再判断
trx_id=create_trx_id(当前事务id),(200=400)
不满足条件 - 再判断
trx_id>=max_id(200>=401)
不满足条件 - 再判断
min_id <=trx_id<max_id(100<=200<401)
满足条件 - trx_id=200,很显然在readview数组[100,200]中,可见
得出结论:事务400查询的数据balance
字段为800
参考文章:
【MySQL笔记】正确的理解MySQL的MVCC及实现原理