写点什么

我身边的高 T,问了 Java 面试者这样的问题......

发布于: 2021 年 02 月 22 日

大家好,我是来自 JDL 京东物流技术发展部邢焕杰, 今天来分享一个京东面试真题,这也是我前阵子听我工位旁边高 T(高,实在是高)面试候选人的时候问的一个问题,他问,你能说说 MySQL 的事务吗?MVCC 有了解吗?话不多说,本文就深度解析一下 MySQL 事务及 MVCC 的实现原理。

事务定义及四大特性

  • 事务是什么?

  • 事务的四大特性(简称 ACID):

  • 原子性(Atomicity):一个事务是一个不可分割的工作单位,事务中包括的操作要么都做,要么都不做。

  • 一致性(Consistency):事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。

  • 隔离性(Isolation):一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰.

  • 持久性(Durability):指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的,接下来的其他操作或故障不应该对其有任何影响。


事务中常见问题

  • 脏读(dirty read):就是一个 A 事务即便没有提交,它对数据的修改也可以被其他事务 B 事务看到,B 事务读到了 A 事务还未提交的数据,这个数据有可能是错的,有可能 A 不想提交这个数据,这只是 A 事务修改数据过程中的一个中间数据,但是被 B 事务读到了,这种行为被称作脏读,这个数据被称为脏数据

  • 不可重复读(non-repeatable read):在 A 事务内,多次读取同一个数据,但是读取的过程中,B 事务对这个数据进行了修改,导致此数据变化了,那么 A 事务再次读取的时候,数据就和第一次读取的时候不一样了,这就叫做不可重复读

  • 幻读(phantom read):A 事务多次查询数据库,结果发现查询的数据条数不一样,A 事务多次查询的间隔中,B 事务又写入了一些符合查询条件的多条数据(这里的写入可以是 update,insert,delete),A 事务再查的话,就像发生了幻觉一样,怎么突然改变了这么多,这种现象这就叫做幻读


隔离级别——产生问题的原因

多个事务互相影响,并没有隔离好,就是我们刚才提到的事务的四大特性中的 隔离性(Isolation) 出现了问题 事务的隔离级别并没有设置好,下面我们来看下事务究竟有哪几种隔离级别

  • 隔离级别

  • 读未提交(read uncommitted RU): 一个事务还没提交时,它做的变更就能被别的事务看到

  • 读提交(read committed RC): 一个事务提交之后,它做的变更才会被其他事务看到。

  • 可重复读(repeatable read RR): 一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。

  • 串行化(serializable ): 顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。

我们来看个例子,更加直观的了解这四种隔离级别和上述问题脏读,不可重复读,幻读的关系

下面我们讨论下当事务处于不同隔离级别情况时,V1,V2,V3 分别是什么不同的值吧

  • 读未提交 (RU): A 事务可以读取到 B 事务修改的值,即便 B 事务没有提交。所以 V1 就是 200

  • V1 : 200

  • V2 : 200

  • V3 : 200

  • 读提交(RC): 当 B 事务没有提交的时候,A 事务不可以看到 B 事务修改的值,只有提交以后才可以看到

  • V1 : 100

  • V2 : 200

  • V3 : 200

  • 可重复读(RR): A 事务多次读取数据,数据总和第一次读取的一样,

  • V1 : 100

  • V2 : 100

  • V3 : 200

  • 串行化(S): 事务 A 在执行的时候,事务 B 会被锁住,等事务 A 执行结束后,事务 B 才可以继续执行

  • V1 : 100

  • V2 : 100

  • V3 : 200


MVCC 原理

MVCC(Multi-Version Concurrency Control)多版本并发控制,是数据库控制并发访问的一种手段。

  • 特别要注意 MVCC 只在 读已提交(RC) 和 可重复度(RR) 这两种事务隔离级别下才有效

  • 是 数据库引擎(InnoDB) 层面实现的,用来处理读写冲突的手段(不用加锁),提高访问性能

MVCC 是怎么实现的呢?它靠的就是版本链一致性视图

1. 版本链

  • 版本链是一条链表,链接的是每条数据曾经的修改记录

那么这个版本链又是如何形成的呢,每条数据又是靠什么链接起来的呢?

其实是这样的,对于 InnoDB 存储引擎的表来说,它的聚簇索引记录包含两个隐藏字段

  • trx_id: 存储修改此数据的事务 id,只有这个事务操作了某些表的数据后当更改操作发生的时候(update,delete,insert),才会分配唯一的事务 id,并且此事务 id 是递增的

  • roll_pointer: 指针,指向上一次修改的记录

  • row_id(非必须): 当有主键或者有不允许为 null 的 unique 键时,不包含此字段

假如说当前数据库有一条这样的数据,假设是事务 ID 为 100 的事务插入的这条数据,那么此条数据的结构如下


后来,事务 200,事务 300,分别来修改此数据


所以此时的版本链如下

我们每更改一次数据,就会插入一条 undo 日志,并且记录的 roll_pointer 指针会指向上一条记录,如图所示

  1. 第一条数据是小杰,事务 ID 为 100

  2. 事务 ID 为 200 的事务将名称从小杰改为了 A

  3. 事务 ID 为 200 的事务将名称从 A 又改为了 B

  4. 事务 ID 为 300 的事务将名称从 B 又改为了 C

所以串成的链表就是 C -> B -> A -> 小杰 (从最新的数据到最老的数据)


2. 一致性视图(ReadView)

需要判断版本链中的哪个版本是是当前事务可见的,因此有了一致性视图的概念。其中有四个属性比较重要

  • m_ids: 在生成 ReadView 时,当前活跃的读写事务的事务 id 列表

  • min_trx_id: m_ids 的最小值

  • max_trx_id: m_ids 的最大值+1

  • creator_trx_id: 生成该事务的事务 id,单纯开启事务是没有事务 id 的,默认为 0,creator_trx_id 是 0。

版本链中的当前版本是否可以被当前事务可见的要根据这四个属性按照以下几种情况来判断

  • 当 trx_id = creator_trx_id 时:当前事务可以看见自己所修改的数据, 可见

  • 当 trx_id < min_trx_id 时   : 生成此数据的事务已经在生成 readView 前提交了, 可见

  • 当  trx_id >= max_trx_id 时   :表明生成该数据的事务是在生成 ReadView 后才开启的, 不可见

  • 当  min_trx_id <= trx_id < max_trx_id 时

  • trx_id 在 m_ids 列表里面 :生成 ReadView 时,活跃事务还未提交,不可见

  • trx_id 不在 m_ids 列表里面 :事务在生成 readView 前已经提交了,可见

如果某个版本数据对当前事务不可见,那么则要顺着版本链继续向前寻找下个版本,继续这样判断,以此类推。

注:RR 和 RC 生成一致性视图的时机不一样 (这也是两种隔离级别实现的主要区别)

  • 读提交(read committed RC) 是在每一次 select 的时候生成 ReadView 的

  • 可重复读(repeatable read RR)是在第一次 select 的时候生成 ReadView 的

下面咱们一起来举个例子实战一下。


RR 与 RC 和 MVCC 的例子实战

假如说,我们有多个事务如下执行,我们通过这个例子来分析当数据库隔离级别为 RC 和 RR 的情况下,当时读数据的一致性视图版本链,也就是 MVCC,分别是怎么样的。

  • 假设数据库中有一条初始数据  姓名是 java 小杰要加油,id 是 1 (id,姓名,trx_id,roll_point),插入此数据的事务 id 是 1

  • 尤其要指出的是,只有这个事务操作了某些表的数据后当更改操作发生的时候(update,delete,insert),才会分配唯一的事务 id,并且此事务 id 是递增的,单纯开启事务是没有事务 id 的,默认为 0,creator_trx_id 是 0。

  • 以下例子中的 A,B,C 的意思是将姓名更改为 A,B,C 读也是读取当前时刻的姓名,默认全都开启事务,并且此事务都经历过某些操作产生了事务 id


读已提交(RC)与 MVCC

  • 一个事务提交之后,它做的变更才会被其他事务看到

每次读的时候,ReadView(一致性视图)都会重新生成

  1. 当 T1 时刻时,事务 100 修改名字为 A

  2. 当 T2 时刻时,事务 100 修改名字为 B

  3. 当 T3 时刻时,事务 200 修改名字为 C

  4. 当 T4 时刻时,事务 300 开始读取名字

  • 此时这条数据的版本链如下

同颜色代表是同一事务内的操作

  • 来我们静下心来好好分析一下此时 T4 时刻事务 300 要读了,究竟会读到什么数据?

当前最近的一条数据是,C,事务 200 修改的,还记得我们前文说的一致性视图的几个属性吗,和按照什么规则判断这个数据能不能被当前事务读。我们就分析这个例子。

此时  (生成一致性视图 ReadView

  • m_ids 是[100,200]: 当前活跃的读写事务的事务 id 列表

  • min_trx_id 是 100:  m_ids 的最小值

  • max_trx_id 是 201: m_ids 的最大值+1

当前数据的 trx_id(事务 id)是 200,符合 min_trx_id<=trx_id<max_trx_id 此时需要判断 trx_id 是否在 m_ids 活跃事务列表里面,一看,活跃事务列表里面是【100,200】,只有两个事务活跃,而此时的 trx_id 是 200,则 trx_id 在活跃事务列表里面,活跃事务列表代表还未提交的事务,所以该版本数据不可见,就要根据 roll_point 指针指向上一个版本,继续这样的判断,上一个版本事务 id 是 100,数据是 B,发现 100 也在活跃事务列表里面,所以不可见,继续找到上个版本,事务是 100,数据是 A,发现是同样的情况,继续找到上个版本,发现事务是 1,数据是小杰,1 小于 100,trx_id<min_trx_id,代表生成这个数据的事务已经在生成 ReadView 前提交了,此数据可以被读到。所以读取的数据就是小杰。

分析完第一个读,我们继续向下分析

  1. 当 T5 时刻时,事务 100 提交

  2. 当 T6 时刻时,事务 300 将名字改为 D

  3. 当 T7 时刻时,事务 400 读取当前数据

  • 此时这条数据的版本链如下

此时 (重新生成一致性视图 ReadView

  • m_ids 是[200,300]: 当前活跃的读写事务的事务 id 列表

  • min_trx_id 是 200:  m_ids 的最小值

  • max_trx_id 是 301: m_ids 的最大值+1

当前数据事务 id 是 300,数据为 D,符合 min_trx_id<=trx_id<max_trx_id 此时需要判断数据是否在活跃事务列表里,300 在这里面,所以就是还未提交的事务就是不可见,所以就去查看上个版本的数据,上个版本事务 id 是 200,数据是 C,也在活跃事务列表里面,也不可见,继续向上个版本找,上个版本事务 id 是 100,数据是 B,100 小于 min_trx_id,就代表,代表生成这个数据的事务已经在生成 ReadView 前提交了,此数据可见,所以读取出来的数据就是 B。

分析完第二个读,我们继续向下分析

  1. 当 T8 时刻时,事务 200 将名字改为 E

  2. 当 T9 时刻时,事务 200 提交

  3. 当 T10 时刻时,事务 300 读取当前数据

  • 此时这条数据的版本链如下

此时 (重新生成一致性视图 ReadView

  • m_ids 是[300]: 当前活跃的读写事务的事务 id 列表

  • min_trx_id 是 300:  m_ids 的最小值

  • max_trx_id 是 301: m_ids 的最大值+1

当前事务 id 是 200,200<min_trx_id ,代表生成这个数据的事务已经在生成 ReadView 前提交了,此数据可见,所以读出的数据就是 E。

当隔离级别是读已提交 RC 的情况下,每次读都会重新生成 一致性视图(ReadView)

  • T4 时刻 事务 300 读取到的数据是小杰

  • T7 时刻 事务 400 读取到的数据是 B

  • T10 时刻 事务 300 读取到的数据是 E

可重复读(RR)与 MVCC

  • 一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的

所以对于事务 300 来讲,它分别在 T4 和 T10 的时候,读取数据,但是它的一致性视图,用的永远都是第一次读取时的视图,就是 T3 时刻产生的一致性视图

RR 和 RC 的版本链是一样的,但是判断当前数据可见与否用到的一致性视图不一样

在此可重复读 RR 隔离级别下,

  1. T4 时刻时事务 300 第一次读时的分析和结果与 RC 都一样,可以见上文分析与结果

  2. T7 时刻时事务 400 第一次读时的分析和结果与 RC 都一样,可以见上文分析与结果

  3. T10 时刻时事务 300 第二次读时的一致性视图和第一次读时的一样,所以此时到底读取到什么数据就要重新分析了

此时  (用的是第一次读时生成的一致性视图 ReadView

  • m_ids 是[100,200]: 当前活跃的读写事务的事务 id 列表

  • min_trx_id 是 100:  m_ids 的最小值

  • max_trx_id 是 201: m_ids 的最大值+1

此时的版本链是

当前数据的事务 id 是 200,数据是 E,在当前事务活跃列表里面,所以数据不可见,根据回滚指针找到上个版本,发现事务 id 是 300,当前事务也是 300,可见,所以读取的数据是 D

  • 我们可以自己思考下,要是没有事务 300 这条更改的这条记录,又该怎么继续向下分析呢?

当隔离级别是可重复读 RR 的情况下,每次读都会用第一次读取数据时生成的一致性视图(ReadView)

  • T4 时刻 事务 300 读取到的数据是小杰

  • T7 时刻 事务 400 读取到的数据是 B

  • T10 时刻 事务 300 读取到的数据是 D


欢迎点击【京东科技】,了解开发者社区

更多精彩技术实践与独家干货解析

欢迎关注【京东科技开发者】公众号


发布于: 2021 年 02 月 22 日阅读数: 61
用户头像

拥抱技术,与开发者携手创造未来! 2018.11.20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东科技开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
我身边的高T,问了Java面试者这样的问题......