MVCC、Read View
- 概述
- MVCC实现原理
- 隐式字段
- undo log日志
- undo log记录数据修改流程
- Read View-读视图(重要!!!)
- Read View属性
- 可见性比较算法(重要!!!)
- 生成Read View时机的区别(重要!!!)
- MVCC逻辑流程-插入
- MVCC逻辑流程-删除
- MVCC逻辑流程-更新
- 当前读(current read)
- 快照读(snapshot read)
- 当前读,快照读和MVCC的关系(重要!!!)
- 总结
- MVCC的好处(能解决的问题)
- MVCC解决幻读
- MVCC解决不可重复读
- 阅读参考
概述
Multi-Version Concurrency Control-多版本并发控制
用来 在数据库中 控制(动词) 并发 的 方法,实现对数据库的并发访问用的
在MySQL中,MVCC 只在 读取已提交(Read Committed)和 可重复读(Repeatable Read)两个事务级别下有效
MVCC 就是 在多个事务 同时存在时,SELECT语句 找寻到 具体是 版本链上的 哪个版本,然后 再 找到的对应版本上 返回其中所记录的数据 的 过程
MVCC实现原理
MVCC的实现原理 主要是 依赖 记录(数据)中的隐式字段、undo日志 和 Read View 来实现的
隐式字段
每行记录除了自定义的字段外,还有数据库隐式定义的 DB_TRX_ID
, DB_ROLL_PTR
,DB_ROW_ID
等字段
-
DB_TRX_ID
,6 byte,最近修改(修改/插入)事务 ID:记录 创建这条记录 / 最后一次修改该记录 的 事务 ID -
DB_ROLL_PTR
,7 byte,回滚指针,指向这条记录 的 上一个版本(存储于 rollback segment 里) -
DB_ROW_ID
,6 byte,隐含的自增 ID(隐藏主键),如果数据表没有主键,InnoDB 会自动以DB_ROW_ID
产生一个聚簇索引 - 还有一个删除 flag 隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除 flag 变了
DB_TRX_ID
是当前操作该记录的事务 IDDB_ROLL_PTR
是一个回滚指针,用于配合 undo日志,指向上一个旧版本
undo log日志
-
insert undo log
,代表事务在 insert时产生的undo log
, 只在事务回滚时需要,并且在事务提交后可以被立即丢弃 -
update undo log
,事务在进行 update 或 delete 时产生的undo log
,不仅在事务回滚时需要,在快照读时也需要,所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被 purge线程统一清除
关于删除时purge线程相关内容请参考MySQL事务(二)-事务的实现、redo重做日志、undo回滚日志
undo log记录数据修改流程
对 MVCC 有帮助的实质是 update undo log
,undo log
实际上就是 存在 rollback segment 中的 旧记录链,执行流程如下:
- 事务0向表插入了一条新记录,隐式主键是1,事务ID
DB_TRX_ID
和 回滚指针DB_ROLL_PTR
,假设为NULL - 来了一个事务1 对该记录的某个字段做出了修改
2.1 在事务1修改该行(记录)数据时,数据库 会 先对该 行加 排他锁
2.2 然后把该行数据(修改前的数据) 拷贝到undo log
中,作为旧记录,既在undo log
中有当前行的拷贝副本
2.3 拷贝完毕后,修改该行数据,并且 修改 隐藏字段的事务IDDB_TRX_ID
为 当前事务1的ID,假设默认从1开始,之后递增,回滚指针DB_ROLL_PTR
指向 拷贝到undo log
中的副本记录,既表示上一个版本就是它
2.4 事务提交后,释放锁 - 又来了个事务2 修改同一个记录
3.1 在事务2修改该行数据时,数据库也先为该行加锁
3.2 然后把该行数据拷贝到undo log
中,作为旧记录,发现该行记录已经有undo log
了,那么 最新的旧数据 作为 链表的表头,插在该行记录的undo log
最前面
3.3 修改该行数据,并且 修改 隐藏字段的事务IDDB_TRX_ID
为 当前事务2的ID,那就是2 ,回滚指针DB_ROLL_PTR
指向 刚刚拷贝到undo log
的副本记录
3.3 事务提交,释放锁
从上面流程就可以看出,不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log
成为一条记录版本线性表,既链表undo log
的** 链首 就是 最新的旧记录,链尾就是最早的旧记录**(该 undo log
的节点可能是会 purge线程清除掉,如果第一条 是insert undo log
,在事务提交之后可能就被删除丢失了)
Read View-读视图(重要!!!)
Read View
就是 事务 进行 快照读操作 的 时候 生成的视图
在 事务执行快照读操作的那一刻,会生成数据库系统当前的一个快照,记录并维护当前活跃事务的 ID (当每个事务开启时,都会被分配一个 ID,这个 ID 是递增的,所以最新的事务,ID 值越大)
Read View
主要是用来 做 可见性判断的,里面保存了“对 本事务不可见 的 其他活跃事务”
Read View属性
-
low_limit_id
,目前出现过的最大的事务ID+1,即将 给 下一个事务 分配 的ID -
trx_ids
:Read View
创建时 其他 未提交的 活跃事务ID列表,意思就是 创建Read View
时,将 当前 未提交事务ID 记录下来,后续即使它们修改了记录行的值,对于当前事务也是不可见的。注意:Read View
中trx_ids
的活跃事务,不包括 当前事务自己 和 已提交的事务(正在内存中) -
up_limit_id
:活跃事务列表trx_ids
中 最小的 事务ID,如果trx_ids
为空,则up_limit_id
为low_limit_id
,因为trx_ids
中的 活跃事务号是逆序的,所以最后一个为最小活跃事务ID -
creator_trx_id
:当前创建事务的ID,是一个递增的编号
可见性比较算法(重要!!!)
Read View
主要 是用来 做 可见性判断的
即当某 个事务 执行快照读的时候,对 该记录 创建一个 Read View
读视图,把 它(视图) 当做条件 用来判断 当前事务 能够看到 哪个版本的数据,既可能是 当前最新的数据,也有可能是 该行记录undo log
里面的某个版本的数据
假设 当前事务 要读取 某一个记录行
该记录行 的 最新修改该行 的 事务IDDB_TRX_ID
为trx_id
Read View
的 活跃事务列表trx_ids
中 最早的 事务ID为up_limit_id
将在 生成这个Read Vew
时 系统出现过的 最大的事务ID+1 记为 low_limit_id
具体的比较算法流程如下:
- 如果 最新修改该行 的 事务ID
trx_id
< 活跃事务列表trx_ids
中 最早的 事务IDup_limit_id
,那么 表明 “最新修改该行的事务trx_id
” 在 “当前事务”创建快照 之前 就提交了,所以 该记录行的值 对 当前事务是可见的,跳到步骤5,结束 - 如果 最新修改该行 的 事务ID
trx_id
>= 即将 给 下一个事务 分配 的IDlow_limit_id
,那么 表明 “最新修改该行的事务” 在 “当前事务”创建快照 之后 才修改该行,所以 该记录行的值 对 当前事务不可见,跳到步骤4 - 如果
trx_id
>=up_limit_id
且trx_id
<low_limit_id
, 表明 “最新修改该行的事务” 在 “当前事务”创建快照的时候 可能处于 “活动状态” 或者 “已提交状态”;所以 就要对 活跃事务列表trx_ids
进行查找(源码中是用的二分查找,因为是有序的)
3.1 如果在活跃事务列表trx_ids
中 能找到 id 为trx_id
的事务,表明:在 “当前事务”创建快照 前,“该记录行的值”被“id为trx_id
的事务”修改了,但没有提交;或者 在 “当前事务”创建快照 后,“该记录行的值”被“id为trx_id
的事务”修改了(不管有无提交);这些情况下,这个记录行的值 对 当前事务 都是 不可见的,跳到步骤4;
3.2 在活跃事务列表trx_ids
中找不到,则表明“id为trx_id
的事务” 在 修改“该记录行的值” 后,在 “当前事务”创建快照 前 就已经 提交了,所以 记录行 对 当前事务可见,跳到步骤5 - 在该记录行的
DB_ROLL_PTR
指针 所指向的 undo log回滚段中,取出 最新的的旧事务号DB_TRX_ID
,将它赋给trx_id
,然后跳到步骤1重新开始判断 - 将该可见行的值返回
生成Read View时机的区别(重要!!!)
READ COMMITTED
和REPEATABLE READ
隔离级别 的 一个 非常大的区别 就是 它们生成Read View
的时机不同
READ COMMITTED
隔离级别的事务 在 每次查询 开始时 都会生成 一个独立的Read ViewREPEATABLE READ
隔离级别的事务 在 第一次 读取数据 时 生成一个Read View,后面就不再重复生成
阅读参考:
MVCC逻辑流程-插入
插入的过程中 会把 全局事务ID 记录到 列的DB_TRX_ID
中去
MVCC逻辑流程-删除
执行完删除语句之后 数据 并没有被 真正地删除,而是对 删除版本号 做改变
MVCC逻辑流程-更新
修改数据的时候 会先复制一条 当前记录行数据
同时 标记这条数据 的 DB_TRX_ID
为 当前事务ID
最后 把原来的数据行 的 DB_ROLL_PTR
标记为 当前事务ID
当前读(current read)
对于 会对数据修改的操作(update、insert、delete) 都是采用 当前读的模式
在执行这几个操作时 会读取 记录的最新的版本,写操作后 把版本号 改为了 当前事务的版本号
当前读 在读取时 还要保证 其他并发事务 不能修改当前记录,会对 读取的记录 进行加锁
select ... lock in share mode;
select ... for update;
insert; update;
delete;
以上操作 都是采用 当前读模式
快照读(snapshot read)
普通的select语句(不包括 select ... lock in share mode; select ... for update;
),也就是 不加锁的select操作 都是采用 快照读的模式
快照读 不需要加锁(地)读取 数据库中的数据,它**(快照读) 依赖 多版本机制(MVCC)**
在开启事务后,读取操作 会被分配一个 最新的事务ID,它只会读取 小于等于 当前事务ID之前的数据,也就是有可能他会读到历史版本的数据,但是读到的数据是不会变化的,这也就是MVCC的实现机制,这也就是为什么MVCC 能够解决 不可重复读的原因(实现可重复读)
快照的生成 是在 第一次执行select的时候
假设当A开启了事务,然后没有执行任何操作
这时候B insert了一条数据然后commit
这时候A执行 select,那么返回的数据中就会有B添加的那条数据
之后无论再有其他事务commit都没有关系,因为快照已经生成了,后面的select都是根据快照来的
快照读 的前提是 隔离级别不是串行级别,串行级别下的快照读 会退化成 当前读
快照读的实现 是基于 多版本并发控制,即 MVCC
可以认为 MVCC 是 行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销
既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本
当前读,快照读和MVCC的关系(重要!!!)
- MVCC 多版本并发控制是只是一个抽象概念,并非具体实现
- 快照读 就是 MySQL 实现 MVCC 理想模型的其中一个非阻塞读功能
- 当前读 就是 悲观锁 的 具体功能实现
- 再细致一些,快照读本身也是一个抽象概念,MVCC 模型在 MySQL 中的具体实现则是由 3 个隐式字段,undo 日志 ,Read View 等去完成的
总结
MVCC的好处(能解决的问题)
在并发读写数据库时,可以做到 在读操作时 不用阻塞 写操作,写操作 也不用阻塞 读操作,提高了数据库并发读写的性能
MVCC解决幻读
- 在快照读情况下,mysql通过mvcc来避免幻读(同下面的解决不可重复读)
- 在当前读情况下,mysql通过X锁或next-key锁 来避免 其他事务修改
阅读参考:
MVCC解决不可重复读
是基于事务隔离级别为REPEATABLE READ
,MVCC在此级别下只在第一次查询的之后创建Read View,后面的查询就不再创建,在通过可见性比较算法处理之后得到的查询结果和之前一样