乐趣区

关于java:InnoDB学习五之MVCC多版本并发控制

MVCC 多版本并发管制,是一种数据库管理系统并发管制的办法。MVCC 多版本并发管制下,数据库中的数据会有多个版本,别离对应不同的事务,从而达到事务之间并发数据的隔离。MVCC 最大的劣势是读不加锁,读写不抵触,在读多写少场景中,读写不抵触能够大幅晋升数据库的并发性能。

MVCC 多版本并发管制

在 MYSQL 中,MyISAM 存储引擎应用的是表锁,InnoDB 存储引擎应用的是行锁。而 InnoDB 的事务分为四个隔离级别,其中默认的隔离级别是可反复读,可反复读要求两个并行的事务之间数据的批改互不影响,通过增加行锁的形式尽管能够实现两个事务之间数据据的批改互不影响,然而者两个事务之间存在锁期待的状况,影响数据库效率。所以 InnoDB 的可反复读没有采纳行锁,而是应用了更为弱小的 MVCC。

MVCC 只有在可反复读和读已提交的隔离级别下失效,其它两个隔离级别和 MVCC 不兼容,因为读未提交总是读最新的数据行,和事务版本无关,串行化则是会对所有读取的行加锁。因为可反复读的状况比较复杂,并且是 MySQL 的默认隔离级别,所以本文会用可反复读来解说 MVCC 的原理。

可反复读

数据库有四种隔离级别:读未提交 / 读已提交 / 可反复读 / 串行化,可反复度是 MySQL 的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到统一的数据行。

数据行的一致性蕴含两局部:

  • 状况 1:已有数据的内容变更,在同一个事务中屡次查问,查问后果应该雷同,如果在以后事务中进行了批改,查问后果应该和以后事务中的批改后果雷同;
  • 状况 2:数据行的增减,同一个事务只能查看到事务开启之前数据库中数据,或者由事务自身新增 / 删除的后果集,无奈看到开启事务期间其它事务新增或删除的后果集;

InnoDB 默认的隔离级别是可反复读,能够解决以上两种状况的数据行一致性问题。其中解决状况 1 中的数据行一致性问题就是通过 MVCC 多版本并发管制实现的。

InnoDB 用过 Gap 锁实现状况 2 中的数据行一致性问题,不过本文不会对 Gap 锁进行介绍。

MVCC 的作用

MVCC 能够确保同一个事务,在事务起始到完结读到的某一个数据是统一的,并且多个事务之间互不阻塞。咱们以一张用户表为例,阐明 MVCC 版本控制的作用。

首先咱们须要创立用户表,并向其中插入一条用户数据,SQL 语句如下:

create table user_info
(
    age int ,
    name  varchar(255)
);

insert into user_info(age,name) value (23,'张三');

假如有 A,B,C 三个事务,这三个事务中在不同时刻对读取了插入用户的信息,并对用户信息进行了批改,工夫线如下:

  1. T1 时刻,事务 A 开始,事务 A 读取 age=23 的用户,该用户的 name张三
  2. T2 时刻,事务 B 开始,事务 B 读取 age=23 的用户,该用户的 name张三
  3. T3 时刻,事务 A 批改 age=23 的用户,把 name 批改为 李四
  4. T4 时刻,事务 A 读取 age=23 的用户,该用户的 name李四,事务 A 提交事务;
  5. T5 时刻,事务 B 读取 age=23 的用户,该用户的 name张三,事务 B 提交事务;
  6. T6 时刻,事务 C 开始,事务 C 读取 age=23 的用户,该用户的 name李四,事务 C 提交事务;

MVCC 的作用能够在 T5 时刻体现进去,此时事务 A 曾经提交,并且批改 age=23 的用户的 name李四 ,然而事务 B 看不到这次批改,事务 B 看到的age=23 的用户的 name张三 。这是因为在可反复度的隔离级别下,InnoDB 事务读取到的数据是 快照读 ,即事务 B 开始时为数据生成一个快照,事务 B 读到的数据始终都是这个快照,与 快照读 对应的是 以后读

  • 以后读:读取的是记录的最新版本,读取时还要保障其余并发事务不能批改以后记录,会对读取的记录进行加锁;
  • 快照读:MVCC 应用的就是快照读,在事务启动时为数据生成快照,快照读能够防止了加锁操作,晋升数据库性能;

MVCC 原理

MVCC 的目标就是多版本并发管制,在 InnoDB 中引入 MVCC 就是为了解决读写抵触,MVCC 次要蕴含三局部内容:数据库中的 3 个暗藏字段、UndoLog 日志、ReadView 读视图,这三局部在 MVCC 中的作用别离如下所示:

  1. 暗藏字段:为数据增加额定的版本信息,是 MVCC 版本控制的基石;
  2. UndoLog:存储了多个版本的数据,不同版本数据暗藏字段的内容不同;
  3. ReadView:判断以后事务应该读取哪个版本的数据;

暗藏字段

暗藏字段意味着咱们通过 SQL 语句查找不到这些字段,然而这些字段在数据库中理论存在并占用了存储空间。为了实现 MVCC 版本控制,InnoDB 为每一行数据增加了以下 3 个暗藏字段:

  1. DB_TRX_ID:6 字节,最初批改本记录的事务 ID;
  2. DB_ROLL_PTR:7 字节,回滚指针,指向这条记录的上一个版本(存储于 Rollback Segment);
  3. DB_ROW_ID:6 字节,暗藏主键,如果数据表没有显式主键,InnoDB 用 DB_ROW_ID 构建聚簇索引;

咱们应用以下 SQL 创立用户表,并向表中插入一条数据,新表会默认蕴含三个暗藏字段,表构造如下表所示。

create table user_info
(
    age int,
    name  varchar(255)
);
insert into user_info(age,name) value (23,'张三');

|age|name|DB_TRX_ID|DB_ROLL_PTR|DB_ROW_ID|
|–|–|–|–||
|23| 张三 |1|0x222333|1|

UndoLog 日志

我在另外一篇文章中介绍过 UndoLog 日志,从名字也能够看进去,UndoLog 日志次要用于回滚事务。然而 InnoDB 中的 MVCC 的快照读也应用了 UndoLog。UndoLog 能够分为两大类:

  1. Insert UndoLog:事务中的 Insert 语句对应的 UndoLog,只在事务回滚时须要,所以事务提交后能够被立刻抛弃;
  2. Update UndoLog:事务在进行 Update 或 Delete 时产生的 UndoLog; 不仅在事务回滚时须要,在快照读时也须要;所以不能轻易删除,只有在快照读或事务回滚不波及该日志时,对应的日志才会被 Purge 线程对立革除;

Purge 线程:InnoDB 中,被删除的数据不会间接删除,而是先标记为删除,无用的 Update UndoLog 也不会立刻删除。这些数据都是通过 InnoDB 中的后台任务 Purge 线程进行删除的。

下文中咱们以上文中的用户表以及数据为例,解释 Update UndoLog 的工作流程,如下为起始时 user_info 表空间的数据状态:

  1. T1 时刻,事务 A 开始,事务 Id 为 2,事务 A 读取 age=23 的用户,该用户的 name张三;此时没有批改数据库数据,没有生成 UndoLog,表空间无变动;

  2. T2 时刻,事务 B 开始,事务 Id 为 3,事务 B 读取 age=23 的用户,该用户的 name张三;此时没有批改数据库数据,没有生成 UndoLog,表空间无变动;

  3. T3 时刻,事务 A 批改 age=23 的用户,把 name 批改为 李四;此时因为事务 A 尚未提交,所以会给事务 A 生成一条 UndoLog,UndoLog 中存储了事务 A 批改前的数据,表空间中最新数据中的回滚指针指向这条日志;

  4. T4 时刻,事务 A 读取 age=23 的用户,因为表数据中的记录的事务 ID 和事务 A 的事务 ID 统一,所以事务 A 会读取到表数据中的记录,读取到用户的 name李四,事务 A 提交事务;

  5. T5 时刻,事务 B 读取 age=23 的用户,因为表空间中数据不满足可见性条件(下一节具体介绍),所以事务 B 会查找表数据的 UndoLog,UndoLog 中的数据满足可见性条件,所以查问到 UndoLog 中的用户,用户的 name张三,事务 B 提交事务;

  6. T6 时刻,事务 C 开始,事务 ID 为 3,事务 C 读取 age=23 的用户,因为事务 C 开始时事务 A 曾经提交,所以事务 C 能够查问到已提交的数据,事务 C 读取到用户的 name李四

  7. T7 时刻,事务 C 开始,事务 ID 为 3,事务 C 批改 age=23 的用户,把 name 批改为 王五;此时因为事务 C 尚未提交,所以会给事务 C 生成一条 UndoLog,UndoLog 中存储了事务 C 批改前的数据;

从下面的例子能够看出,不同事务或者雷同事务的对同一记录的批改,会导致该记录的 UndoLog 成为一条记录版本线性链表,UndoLog 的链首就是最新的旧记录,链尾就是最早的旧记录(UndoLog 的节点可能会被 Purge 线程革除掉)

UndoLog 是为回滚而用,具体内容就是复制事务前的数据库记录行到 UndoBuffer,在适宜的工夫把 UndoBuffer 中的内容刷新到磁盘。UndoBuffer 与 RedoBuffer 一样,也是环形缓冲,但当缓冲满的时候,UndoBuffer 中的内容会也会被刷新到磁盘;与 RedoLog 不同的是,磁盘上不存在独自的 UndoLog 文件,所有的 UndoLog 均寄存在主 ibd 数据文件中(表空间),即便客户端设置了每表一个数据文件也是如此。

ReadView 读视图

ReadView 就是事务进行快照读操作的时候生产的读视图,在该事务执行的快照读的那一刻,会生成数据库系统以后的一个快照,记录并保护零碎以后沉闷事务的 ID(当每个事务开启时,都会被调配一个 ID, 这个 ID 是递增的,所以最新的事务,ID 值越大)

所以咱们晓得 ReadView 次要是用来做可见性判断的, 即当咱们某个事务执行快照读的时候,对该记录创立一个 ReadView 读视图,把它比作条件用来判断以后事务可能看到哪个版本的数据,既可能是以后最新的数据,也有可能是该行记录的 UndoLog 外面的某个版本的数据。

ReadView 遵循一个可见性算法,次要是将要被批改的数据的最新记录中的 DB_TRX_ID(即以后事务 ID)取出来,与零碎以后其余沉闷事务的 ID 去比照(由 ReadView 保护),如果 DB_TRX_ID 跟 ReadView 的属性做了某些比拟,不合乎可见性,那就通过 DB_ROLL_PTR 回滚指针去取出 UndoLog 中的 DB_TRX_ID 再比拟,即遍历链表的 DB_TRX_ID(从链首到链尾,即从最近的一次批改查起),直到找到满足特定条件的 DB_TRX_ID, 那么这个 DB_TRX_ID 所在的旧记录就是以后事务能看见的最新老版本。

ReadView 判断可见性的原理如下,在 InnoDB 中,创立一个新事务之后,当新事务读取数据时,数据库为该事务生成一个 ReadView 读视图,InnoDB 会将以后零碎中的沉闷事务列表创立一个正本保留到 ReadView。当用户在这个事务中要读取某行记录的时候,InnoDB 会将该行以后的版本号与该 ReadView 进行比拟。具体的算法如下:

  1. 设该行的以后事务 ID 为 cur_trx_id,ReadView 中最早的事务 ID 为 min_trx_id, 最迟的事务 ID 为 max_trx_id;
  2. 如果 cur_trx_id < min_trx_id,那么表明该行记录所在的事务曾经在本次新事务创立之前就提交了,所以该行记录的以后值是可见的。跳到步骤 6.
  3. 如果 cur_trx_id > max_trx_id,那么表明该行记录所在的事务在本次新事务创立之后才开启,所以该行记录的以后值不可见. 跳到步骤 5;
  4. 如果 min_trx_id<= cur_trx_id <= max_trx_id, 那么表明该行记录所在事务在本次新事务创立的时候处于活动状态,从 min_trx_id 到 max_trx_id 进行遍历,如果 cur_trx_id 等于他们之中的某个事务 id 的话,那么不可见。跳到步骤 5;
  5. 从该行记录的 DB_ROLL_PTR 指针所指向的回滚段中取出最新的 UndoLog 的版本号,将它赋值该 cur_trx_id,而后跳到步骤 2;
  6. 将该可见行的值返回;

总结一下:MVCC 版本控制中,以事务第一次快照读为分界线,事务后续只能查找到第一次快照读及之前提交的数据版本,之后提交的数据版本不可见。

读已提交和可反复度

读已提交和可反复度隔离级别下的 InnoDB 快照读有什么不同?答案是:ReadView 生成机会的不同,从而造成读已提交和可反复度级别下快照读的后果的不同:

  • 可反复读隔离级别下,事务第一次快照读会生成 ReadView 时,ReadView 会记录此时所有其余流动事务的快照,这些事务的批改对于以后事务都是不可见的。而早于 ReadView 创立的事务所做的批改均是可见;
  • 读已提交隔离级别下的,事务每次快照读都会新生成一个快照和 ReadView, 这就是咱们在 RC 级别下的事务中能够看到别的事务提交的更新的起因;

总之在读已提交隔离级别下,是每个快照读都会生成并获取最新的 ReadView;而在可反复读隔离级别下,则是同一个事务中的第一个快照读才会创立 ReadView, 之后的快照读获取的都是同一个 ReadView。

MVCC 与幻读

幻读是指,同一个事务外面间断执行两次同样的 SQL 语句,可能导致不同后果的问题,第二次 SQL 语句可能会返回之前不存在的行。举例说明:T1 时刻事务 A 和事务 B 同时开启,别离进行了快照读,而后事务 A 向数据库中插入一条新的记录,如果事务 B 能够读到这条记录,就呈现了 ” 幻读 ”,因为 B 第一次快照读没有读到这条数据。

MVCC 是否能够解决幻读问题呢?答案是有的状况下能够解决,有的状况下不能够解决。如果事务 B 中的读是快照读,那么 MVCC 版本控制能够解决幻读问题;如果事务 B 中应用的是以后读,那么 MVCC 无奈解决幻读问题。

  • 快照读是基于 MVCC 和 UndoLog 来实现的,实用于简略 Select 语句;
  • 以后读是基于 Gap 锁来实现的,实用于 Insert,Update,Delete,Select … For Update,Select … Lock In Share Mode 语句,以及加锁了的 Select 语句;

事实上,MVCC 对于所有的以后读都有效,比方事务 A 批改数据之后,事务 B 去 Update 对应的数据,Update 语句筛选条件针对的是数据库中以后的数据,而不是快照数据。

我是御狐神,欢送大家关注我的微信公众号:wzm2zsd

参考文档

MySQL 之 MVCC 与幻读 <br/>
正确的了解 MySQL 的 MVCC 及实现原理 <br/>
MySQL 数据库事务各隔离级别加锁状况 –read committed && MVCC

本文最先公布至微信公众号,版权所有,禁止转载!

退出移动版