MySQL MVCC机制探秘:数据一致性与并发处理的完美结合,助你成为数据库高手-CSDN博客

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6


一、前言

在分析 MVCC 的原理之前我们先回顾一下 MySQL 的一些内容以及关于 MVCC 的一些简单介绍。注:下面没有特别说明默认 MySQL 的引擎为 InnoDB

1.1 数据库的并发场景

数据库并发场景有三种分别是

  • 读-读不存在线程安全问题不需要并发控制。
  • 读-写有线程安全问题可能会造成事务隔离性问题可能遇到脏读、不可重复读、幻读等问题。
  • 写-写有线程安全问题可能会存在更新丢失的问题比如第一类更新丢失、第二类更新丢失。

第一类丢失更新事务A回滚时将已经提交的事务B的更新数据覆盖了第二类丢失更新事务A提交覆盖了事务B已经提交的数据造成事务B所做的操作丢失

1.2 什么是 MVCC

  • MVCC全称 Multi-Version Concurrency Control ,即多版本并发控制MVCC 是一种并发控制的方法一般在数据库管理系统中实现对数据库的并发访问在编程语言中实现事务内存。
  • 多版本控制指的是一种提高并发的技术在最早的数据库系统中只有读-读之间可以并发读-写、写-写之间都要阻塞。引入多版本并发控制之后只有写-写之间相互阻塞其他三种操作都可以并行这样大幅的提高了 InnoDB 的并发度。在内部实现中InnoDB 是通过 undo log 实现的通过 undo log 可以找回数据的历史版本。找回的历史版本可以提供给用户读按照隔离级别的定义有些读请求只能看到比较老的数据版本也可以在回滚的时候覆盖数据页上的数据。在 InnoDB 内部中会记录一个全局的活跃读写事务数组其主要用来判断事务的可见行。

一句话概述MVCC 在 MySQL InnoDB 中的实现主要是为了提高数据库并发性能用更好的方式去处理读写冲突做到即使有读写冲突时也能做到不加锁做到非阻塞并发读

1.3 当前读和快照读

  • 当前读

    select xxx lock in share mode; # 共享锁

    #排它锁
    select xxx for update;
    update xxx;
    delete xxx;
    insert xxx;

    像上面的这些操作就是一种当前读因为它读取的是数据的最新版本读取时还要保证其他事务不能修改当前记录会对记录进行加锁。

  • 快照读

    不加锁的 select 就是快照读即不加锁的非阻塞读。快照读的前提是隔离级别不是 serializableserializable 的隔离级别下快照读会退化成当前读 之所以出现快照读的情况是基于提高并发性能的考虑快照读的实现是基于 MVCC 的可以认为 MVCC 是行锁的一个变种但是它在很多情况下避免了加锁操作降低了开销。既然是基于多版本所以快照读可能读到的不一定是数据的最新版本而有可能是之前的历史版本。

1.4 当前读和快照读与 MVCC 的关系

准确的说MVCC 指的是**"维护一个数据的多个版本使得读写操作没有冲突"这么一个概念仅仅是一个理想状态。而在 MySQL 中实现这么一个 MVCC 理想概念我们就需要 MySQL 提供具体的功能去实现而快照读**就是 MySQL 为我们实现 MVCC 理想模型的其中一个具体非阻塞读功能。而相对而言当前读就是一个悲观锁的具体功能实现而要说的在细致一点快照读本身也是一个抽象概念在深入研究MVCC 模型在 MySQL 中的具体实现则是由三个隐式字段、undo log 、Read View 等去完成的。

1.5 MVCC 能解决什么问题

MVCC 是一种解决读写冲突的无锁并发控制手段,也就是为事务分配单向增长的时间戳为每个修改保存一个版本版本与事务时间戳关联读操作只读该事务开始前的数据库的快照所以 MVCC 可以为数据库解决以下问题

  1. 在并发读数据库时可以做到在读操作时不用阻塞写操作写操作也不用阻塞读操作提高了数据库并发读写性能。
  2. 可以解决脏读、不可重复读、幻读等事务隔离问题不能解决更新丢失的问题。

所以说 MVCC 就是开发人员不满意只让数据库采用悲观锁加锁这样性能不佳的形式去解决读-写的问题而提出的解决方案所以在数据库中因为有了 MVCC 所以我们可以形成两个组合

  1. MVCC + 悲观锁

MVCC解决读写冲突悲观锁解决写-写冲突

  1. MVCC + 乐观锁

MVCC解决读写冲突乐观锁解决写-写冲突。

二、MVCC实现原理

2.1 隐式字段

在一张表中除了我们自定义的字段实际上 MySQL 会隐式的定义一些字段。

  • DB_TRX_ID

    6byte最近修改修改/插入事务ID记录创建这条记录/最后一次修改该记录的事务ID

  • DB_ROLL_PTR

    7byte回滚指针指向这条记录的上一个版本存储于 rollback segment 里

  • DB_ROW_ID

    6byte隐含的自增ID隐藏主键如果数据表没有主键InnoDB 会自动以 DB_ROW_ID 产生一个聚簇索引

  • 实际还有一个删除 flag 隐藏字段, 即记录被更新或删除并不代表真的删除而是删除 flag 变了

例如下面是 person 表的某条记录如下图DB_ROW_ID 是数据库为改行记录生产的唯一隐式主键DB_TRX_ID 是当前操作该记录的事务ID而 DB_ROLL_PTR 是一个回滚指针用于配合 undo log 日志指向上一个版本。

59c8504fac0e965a699bd647b86a2075.jpeg

2.2 undo log日志

undo log 类型

  • insert undo log

    是指在 insert 操作中产生的 undo log。因为 insert 操作的记录只对当前事务本身可见对其他事务不可见这是事务隔离性的要求因此这种 undo log 可以在事务提交后直接删除。不需要进行 purge 操作

  • update undo log

    是对 delete 和 update 操作产生的 undo log。该 undo log 可能需要提供给 MVCC 机制使用因此不能在事务提交时就进行删除提交时放入 undo log 链表等待 purge 线程进行最后的删除。

 

purge 线程

为了实现 InnoDB 的 MVCC 机制更新或者删除操作都只是设置一下老记录的 deleted_bit即前面提到的删除 flag 并不真正将过时的记录删除。为了节省磁盘空间InnoDB 有专门的 purge 线程来清理 deleted_bit 为 true 的记录。为了不影响 MVCC 的正常工作purge 线程自己也维护了一个 Read View这个 Read View 相当于系统中最老活跃事务的 Read View 如果某个记录的 deleted_bit 为true并且 DB_TRX_ID 相对于 purge 线程的 Read View 可见那么这条记录一定是可以被安全清除的。

 

对 MVCC 有帮助的实质是 update undo log undo log 存在于 rollback segment 中旧记录链它的执行流程如下

  1. 一个事务插入 person 表插入了一条新记录记录如下name 为 Jerry, age 为24隐式主键是1事务ID和回滚指针我们假设为NULL。
c6833cc0c414d33f0a96a06ae82ec1aa.jpeg

2.现在来了一个 事务1 对记录的 name 进行了修改改为了 Tom执行过程如下

  1. 在事务1修改该行数据时数据会先对这行记录加排它锁。
  2. 然后把该行数据拷贝到 undo log 中作为旧记录即在 undo log 中有当前行的拷贝副本。
  3. 拷贝完毕后修改该行的 name 为 tom并且修改隐藏字段的事务ID为当前事务1的ID我们默认从1开始之后递增回滚指针指向拷贝到 undo log 的副本记录即表示我的上一个版本就是它。
  4. 事务提交后释放排它锁。
bc40d092725fe55a9ce79ee5938e7d41.jpeg

3.又来了个事务2修改 person 表的同一个记录将 age 修改为30岁执行过程如下

  1. 在事务2修改该行数据时数据库也先为该行加锁
  2. 然后把该行数据拷贝到 undo log 中作为旧记录发现该行记录已经有 undo log 了那么最新的旧数据作为链表的表头插在该行记录的 undo log 最前面
  3. 修改该行 age 为30岁并且修改隐藏字段的事务ID为当前事务2的ID, 那就是2回滚指针指向刚刚拷贝到 undo log 的副本记录
  4. 事务提交释放锁
646692e22d5bf072645d4393db1ce22c.jpeg

从上面我们可以看出不同事务或者相同事务对同一记录的修改会导致该记录的 undo log 称为一条记录版本线性表即链表undo log 的表头就是最新的旧记录当然就像之前说的该 undo log 的节点可能会被 purge 线程清除掉像图中的第一条 insert undo log 其实在事务提交之后可能就被删除丢失了不过这里为了演示所以还放在这里

2.3 Read View

 

什么是 Read View

Read View 是事务进行快照读操作时产生的读视图在该事务执行快照读的那一刻会生成数据库系统的当前的一个快照记录并维护当前活跃事务的ID当每个事务开启时都会被分配一个 ID这个 ID 是自增的所以最新的事务ID 值越大

 

可见行判断

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

Read View 遵循一个可见性算法主要是将要被修改的数据的最新记录的 DB_TRX_ID即当前事务ID,与系统当前其他活跃事务的ID去对比由 Read View 维护如果 DB_TRX_ID 跟 Read View 的属性做了某些对比不符合可见性那么就由 DB_ROLL_PTR 回滚指针去取出undo log中的 DB_TRX_ID 再比较即遍历链表的 DB_TRX_ID从链表头到尾即从最近的一次修改查起直到找到满足特定条件的 DB_TRX_ID那么这个 DB_TRX_ID 所在的旧记录就是当前事务能看见的最新的老版本。

 

判断条件是什么

我们可以将 Read View 简单的理解为三个全局属性

  • trx_list 一个数值列表用来维护 Read View 生成时刻此时系统正活跃的事务ID

  • up_limit_id 记录 trx_list 中的最小的事务 ID

  • low_limit_id 在 Read View 生成时刻系统尚未分配的下一个事务 ID即目前不一定是 Read View 中已经出现过的事务 ID 最大值+1

     

    比较步骤

  1. 首先比较 DB_TRX_ID < up_limit_id, 如果小于则当前事务能看到 DB_TRX_ID 所在的记录如果大于等于进入下一个判断。
  2. 接下来判断 DB_TRX_ID 大于等于 low_limit_id , 如果大于等于则代表 DB_TRX_ID 所在的记录在 Read View 生成后才出现的那对当前事务肯定不可见如果小于则进入下一个判断。
  3. 判断 DB_TRX_ID 是否在活跃事务之中trx_list.contains DB_TRX_ID 如果在则代表我 Read View 生成时刻你这个事务还在活跃还没有 Commit你修改的数据我当前事务也是看不见的如果不在则说明你的这个事务在 Read View 生成之前就已经 Commit了你修改的结果我当前事务是能看见的。

2.4 实现流程

我们在了解了隐式字段、undo log 以及 Read View 的概念之后我们模拟一下 MVCC 实现的整体流程。

  1. 假设当前有四个事务当事务2对某行数据执行了快照读数据库为该行数据生成一个 Read View 读视图假设当前事务ID为2此时还有事务1和事务3在活跃中事务4在事务2快照读前一刻提交更新了所以 Read View 记录了系统当前活跃事务13的ID维护在一个列表上假设我们称为 trx_list 。





    事务1事务2事务3事务4事务开始事务开始事务开始事务开始………修改且已提交进行中快照读进行中
    ………
  2. Read View 不仅仅会通过一个列表 trx_list 来维护事务2执行快照读那刻系统正活跃的事务ID还会有两个属性 up_limit_id记录 trx_list 列表中事务ID最小的IDlow_limit_id记录 trx_list 列表中事务ID最大的ID也有人说快照读那刻系统尚未分配的下一个事务ID也就是目前已出现过的事务ID的最大值+1所以在这里例子中 up_limit_id 就是1low_limit_id 就是4 + 1 = 5trx_list 集合的值是1,3Read View 如下图

    59bc447c2ce168b3f880bf039e852c29.jpeg
  3. 我们的例子中只有事务4修改过该行记录并在事务2执行快照读前就提交了事务所以当前该行当前数据的 undo log 如下图所示我们的事务2在快照读该行记录的时候就会拿该行记录的 DB_TRX_ID 去跟 up_limit_id , low_limit_id 和活跃事务ID列表 trx_list 进行比较判断当前事务2能看到该记录的版本是哪个。

    0d06227f939fe73269d74daeb403e0bc.jpeg
  4. 所以先拿该记录 DB_TRX_ID 字段记录的事务ID 4去跟 Read View 的的 up_limit_id 比较看4是否小于 up_limit_id1所以不符合条件继续判断 4 是否大于等于 low_limit_id5也不符合条件最后判断4是否处于 trx_list 中的活跃事务, 最后发现事务ID为4的事务不在当前活跃事务列表中, 符合可见性条件所以事务4修改后提交的最新结果对事务2快照读时是可见的所以事务2能读到的最新数据记录是事务4所提交的版本而事务4提交的版本也是全局角度上最新的版本。

&nbsp;

流程图

1548a3bf6cf883b437b78543175d9aca.jpeg

2.5 RC/RR级别快照读有什么不同

生成 Read View 的时机不同从而造成 RC RR 级别下快照读的结果的不同。

  • 在RR级别下的某个事务对某条记录进行的第一次快照读会创建一个快照 Read View此后在调用快照读的时候使用的还是同一个ReadView所以只要当前事务在其他事务提交更新之前使用过快照读那么之后的快照读使用的都是同一个Read View所以对之后的修改不可见.
  • 而在 RC 隔离级别下事务中每次快照都会生成一个快照和 ReadView这就是我们在 RC 级别下的事务中可以看到别的事务提交更新的原因。

总之在 RC 隔离级别下每次快照读都会生成最新的 ReadView而在 RR 级别下则是同一个事务中的第一个快照读才会创建ReadView之后的快照读获取的都是同一个 ReadView。所以说 RR 在 RC 的基础上通过生成 Read View 的时机不同从而解决了不可重复读的问题

总结

本文讲解了 MySQL 中的隐式字段、undo log 日志和 Read View 的原理以及 MVCC 的实现流程对于我们在日常的开发过程中对于数据库的并发操作以及 MySQL 的各种隔离级别有了清晰的认识。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: mysql数据库

“MySQL MVCC机制探秘:数据一致性与并发处理的完美结合,助你成为数据库高手-CSDN博客” 的相关文章