MVCC 是什么?
MVCC 的全称是 Multi-Version Concurrency Control, 通常用于数据库等场景中, 实现多版本的并发控制
Multiversion concurrency control (MCC or MVCC), is a concurrency control method commonly used by database management systems to provide concurrent access to the database and in programming languages to implement transactional memory.
如果没有并发控制, 那么如果同时有用户读写数据, 那么可能出现读出的数据不一致的情况. 比如说, 进行银行账户 A 到 B 的转账, 当 A 账户的钱被扣掉, 而钱还没有加到 B 账户, 此时用户查看自己的余额, 会感觉钱凭空消失了. MySQL 的隔离性就是用来解决这类问题的, 而隔离性是通过不同的并发控制手段来实现的. 对于刚才的问题, 一种简单的并发控制方式, 就是讲读写操作串行化, 在账户间转账时, 不允许查询账户, 虽然这种方式可以解决问题, 但无疑过于简单粗暴, 效率极低. 相比于串行化的并发控制, MVCC 的优势在于读写影响, 对于现代互联网读多写少的场景, 这种方式性能明显更高.
MVCC 是通过保存数据的多个版本来实现并发控制, 当需要更新某条数据时, 实现了 MVCC 的存储系统不会立即用新数据覆盖原始数据, 而是创建该条记录的一个新的版本. 对于多数数据库系统, 存储会分为 Data Part 和 Undo Log , Data Part 用来存储事务已提交的数据, 而 Undo Log 用来存储旧版本的数据. 多版本的存在允许了读和写的分离, 读操作是需要读取某个版本之前的数据即可, 和写操作不冲突, 大大提高了性能.
MVCC 的效果
假如 MVCC 是按照时间来判定数据的版本, 在 Time=1 的时刻, 数据库的状态如下:
Time | Record A | Record B |
---|---|---|
0 | “Record A When time=0” | “Record B when time=0” |
1 | “Record A When time=1” |
这个时候系统中实际存储了三条记录, Record A 在时间 0 和 1 的各一条记录, Record B 的一条记录, 如果一个事务在 Time=0 的时刻开启, 那么读到的数据是:
Record A | Record B |
---|---|
“Record A When time=0” | “Record B when time=0” |
如果这个事务在 Time=1 的时候开启, 那么读到的数据是:
Record A | Record B |
---|---|
“Record A When time=1” | “Record B when time=0” |
上面的 Case 可以看到, 对于读来讲, 事务只能读到某一个版本及这个版本之前的最新一条数据, 假如在 Time=2 的时候, 事务 Transaction X 要插入 Record C , 并更新 Record B , 但事务还未提交, 那么数据库的状态如下:
Time | Record A | Record B | Record C |
---|---|---|---|
0 | “Record A When time=0” | “Record B when time=0” | |
1 | “Record A When time=1” | ||
2(Not Committed) | “Record B when time=2” | “Record C When time=2” |
这时候其它事务会读到的是什么了? 在这个情况下, 其它读事务所能看到系统的最新版本是系统处于 Time=1 的时候, 所以依然不会读到 Transaction X 所改写的数据, 此时读到的数据依然为:
Record A | Record B |
---|---|
“Record A When time=1” | “Record B when time=0” |
基于这种版本机制, 就不会出现另一个事务读取时, 出现读到 Record C 而 Record B 还未被 Transaction X 更新的中间结果, 因为其它事务所看到的系统依然处于 Time=1 的状态.
至于说, 每个事务应该看到具体什么版本的数据, 这个是由不同系统的 MVCC 实现来决定的, 下文我会介绍 MySQL 的 MVCC 实现. 除了读到的数据必须小于等于当前系统已提交的版本外, 写事务在提交时必须大于当前的版本, 而这里如果想想还会有一个问题, 如果 Time=2 的时刻, 开启了多个写或更新事务, 当它们同时尝试提交时, 必然会有一个事务发现数据库已经处于 Time=2 的状态了, 那么这个事务该怎么办了? 大家可以好好想想.
MySQL 的 MVCC
MySQL 的 Innodb 引擎支持多种事务隔离级别, 而其中的 RR 级别 (Repeatable-Read) 就是依靠 MVCC 来实现的, MySQL 中 MVCC 的版本指的是事务 ID(Transaction ID), 首先来看一下 MySQL Innodb 中行记录的存储格式, 除了最基本的行信息外, 还会有一些额外的字段, 这里主要介绍和 MVCC 有关的字段: DATA_TRX_ID 和 DATA_ROLL_PTR , 如下是一张表的初始信息:
Primary Key | Time | Name | DATA_TRX_ID | DATA_ROLL_PTR |
---|---|---|---|---|
1 | 2018-4-28 | Huan | 1 | NULL |
这里面为了便于说明, 表中 DATA_TRX_ID 和 DATA_ROLL_PTR 存的值是 Mock 的值:
DATA_TRX_ID : 最近更新这条记录的 Transaction ID, 数据库每开启一个事务, 事务 ID 都会增加, 每个事务拿到的 ID 都不一样
DATA_ROLL_PTR : 用来存储指向 Undo Log 中旧版本数据指针, 支持了事务的回滚
最开始的记录无法回滚, 所以 DATA_ROLL_PTR 为空.
这个时候开启事务 A(事务 ID:2), 对记录进行了更新, 但还没有提交, 那么当前的数据为:
可以看到, 旧的数据会被存到 Undo Log 中, 通过当前记录中的 DATA_ROLL_PTR 关联, 那么如果另一个事务中想读取该数据, 读到的会是什么数据了? 假如说另一个事务 B 在事务 A 之后开启(事务 ID:3), 既然我们最开始说 Innodb 的 MVCC 是基于事务 ID 做的, 那么既然事务 B 的事务 ID 比事务 A 的大, 那么事务 B 就可以独到 A 还未提交的数据了, 这明显和 Innodb RR 的定义不符合. 实际上, 事务读取时, 判断应该读取哪个版本的记录, 有一个较为复杂的逻辑, 不是单纯的和记录上的事务 ID 进行比较, 假设当前读的事务 ID 为 read _id , 记录当前存储的事务 ID 为 tid , 当前系统中未提交的事务中的最大最小事务 ID 分别为 max_tid 和 min_tid , 那么数据可见性判断流程为:
通过上图(这个图是通过分析网上的一些博客内容得到的, 和实际 MySQL 的逻辑细节可能不一致), 在来分析上文提到的 Case, 由于事务 B 的事务 ID 不满足
read_id=tid||tid<min_tid
的条件, 且该记录当前有 DATA_ROLL_PTP , 所以最后该事务 B 实际读取的是 Undo Log 中的记录:
Primary Key | Time | Name | DATA_TRX_ID | DATA_ROLL_PTR |
---|---|---|---|---|
1 | 2018-4-28 | Huan | 1 | NULL |
需要注意的是, MySQL 的 MVCC 和理论上的 MVCC 实际有所差异, MySQL 同一时刻只允许一个事务去操作某条数据, 该条数据上的操作实际是串行的, 也就是说一条记录的有用版本
实际就只会有当前记录和一条 Undo Log 记录
, 是 悲观锁 的操作方式, 而 MVCC 的定义上实际是 乐观锁 的操作方式, 某一时刻记录可以存在很多个版本.
来源: http://www.tuicool.com/articles/m63Yb22