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 是当前操作该记录的事务 ID
DB_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 logundo log 实际上就是 存在 rollback segment 中的 旧记录链,执行流程如下:

  1. 事务0向表插入了一条新记录,隐式主键是1,事务IDDB_TRX_ID 和 回滚指针DB_ROLL_PTR,假设为NULL
  2. 来了一个事务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 事务提交后,释放锁
  3. 又来了个事务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_idsRead View创建时 其他 未提交的 活跃事务ID列表,意思就是 创建Read View时,将 当前 未提交事务ID 记录下来,后续即使它们修改了记录行的值,对于当前事务也是不可见的。注意:Read Viewtrx_ids的活跃事务,不包括 当前事务自己 和 已提交的事务(正在内存中)
  • up_limit_id:活跃事务列表trx_ids中 最小的 事务ID,如果trx_ids为空,则 up_limit_idlow_limit_id,因为trx_ids中的 活跃事务号是逆序的,所以最后一个为最小活跃事务ID
  • creator_trx_id:当前创建事务的ID,是一个递增的编号

可见性比较算法(重要!!!)

Read View 主要 是用来 做 可见性判断的
即当某 个事务 执行快照读的时候,对 该记录 创建一个 Read View 读视图,把 它(视图) 当做条件 用来判断 当前事务 能够看到 哪个版本的数据,既可能是 当前最新的数据,也有可能是 该行记录undo log里面的某个版本的数据

假设 当前事务 要读取 某一个记录行
该记录行 的 最新修改该行 的 事务IDDB_TRX_IDtrx_idRead View 的 活跃事务列表trx_ids中 最早的 事务ID为up_limit_id 将在 生成这个Read Vew时 系统出现过的 最大的事务ID+1 记为 low_limit_id

具体的比较算法流程如下:

  1. 如果 最新修改该行 的 事务IDtrx_id < 活跃事务列表trx_ids中 最早的 事务IDup_limit_id,那么 表明 “最新修改该行的事务trx_id” 在 “当前事务”创建快照 之前 就提交了,所以 该记录行的值 对 当前事务是可见的,跳到步骤5,结束
  2. 如果 最新修改该行 的 事务IDtrx_id >= 即将 给 下一个事务 分配 的IDlow_limit_id,那么 表明 “最新修改该行的事务” 在 “当前事务”创建快照 之后 才修改该行,所以 该记录行的值 对 当前事务不可见,跳到步骤4
  3. 如果 trx_id >= up_limit_idtrx_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
  4. 在该记录行的DB_ROLL_PTR指针 所指向的 undo log回滚段中,取出 最新的的旧事务号DB_TRX_ID,将它赋给trx_id,然后跳到步骤1重新开始判断
  5. 将该可见行的值返回

生成Read View时机的区别(重要!!!)

READ COMMITTEDREPEATABLE READ隔离级别 的 一个 非常大的区别 就是 它们生成Read View的时机不同

READ COMMITTED隔离级别的事务 在 每次查询 开始时 都会生成 一个独立的Read View
REPEATABLE 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,后面的查询就不再创建,在通过可见性比较算法处理之后得到的查询结果和之前一样