滴滴一面,痛失40K:因MVCC没说明白

发布时间 2023-09-12 16:40:30作者: 疯狂创客圈

文章很长,且持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录 博客园版 为您奉上珍贵的学习资源 :

免费赠送 :《尼恩Java面试宝典》 持续更新+ 史上最全 + 面试必备 2000页+ 面试必备 + 大厂必备 +涨薪必备
免费赠送 :《尼恩技术圣经+高并发系列PDF》 ,帮你 实现技术自由,完成职业升级, 薪酬猛涨!加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷1)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷2)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷3)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领

免费赠送 资源宝库: Java 必备 百度网盘资源大合集 价值>10000元 加尼恩领取


滴滴一面,痛失40K:因MVCC没说明白

说在前面

在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如阿里、网易、有赞、希音、百度、网易、滴滴的面试资格,遇到很多次遇到MVCC相关的面试题:

  • 说一下MVCC的实现原理?

  • 请你讲下MVCC是什么?

前几天,小伙伴给尼恩反馈,在滴滴的面试中, 遇到这个问题,没有说清楚,导致面试失败。

在 MySQL 中,MVCC (多版本并发控制)主要解决并发访问数据库带来的一系列问题。例如,读写之间阻塞的问题、减少死锁的发生、解决一致性读(快照读)的问题。MVCC 可以在尽量减少锁使用的情况下,用更高效、更好的方式去处理读写冲突,极大提高了数据库并发性能

MVCC 是面试的核心问题。这里尼恩给大家做一下系统化、体系化的梳理,使得大家可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”

本篇,我们深入理解 MVCC(多版本并发控制)原理。

也一并把这个题目以及参考答案,收入咱们的 《尼恩Java面试宝典PDF》V106版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。

最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取

本文目录

展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”

本篇,我们深入理解 MVCC(多版本并发控制)原理。

也一并把这个题目以及参考答案,收入咱们的 《尼恩Java面试宝典PDF》V106版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。

最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,回复:领电子书

1、什么是 MVCC

MVCC ,即多版本并发控制,全拼 Version Concurrency Control

MVCC 为每个事务创建多个数据版本,每个版本对应一个特定时间点的数据库状态,不同事务可以基于各自的时间点来进行读取和写入操作,而不会相互干扰。

2、什么是当前读、快照读?

在深入了解 MVCC 之前,我们先来探讨一下 MySQL InnoDB 的当前读和快照读。

当前读和快照读是 MVCC 机制下的两种数据读取方法,各自适用于各种不同的应用场景。

当前读(Current Read)

  • 当前读是指事务在读取数据时,总是读取最新提交的数据版本。
  • 当前读能够读取其他事务已经提交的数据,同时在当前事务有未提交的修改时,也会读取自己所做的修改,可能会读取到未提交的数据。
  • 当前读适用于需要获取最新数据状态的场景,比如,实时查询账户余额。然而,需要注意的是,在并发环境下,当前读可能会引发一致性问题。

快照读(Snapshot Read)

  • 快照读,也称为一致性读,是指事务在读取数据时,会读取一个事务开始时的数据版本,即创建事务时的快照。
  • 快照读仅会读取已提交的数据版本,不会读取其他事务未提交的数据。
  • 快照读适用于需要事务隔离和数据一致性的场景。比如,在事务内部进行多次读取操作。
  • 快照读能够提供事务开始时的数据一致性视图,避免了并发冲突和未提交数据的影响,但可能不够实时。

根据事务隔离级别和应用需求的不同,我们可以选择适合的读取方式。

3、MVCC 的作用

数据库的三种并发场景是读 - 读读 - 写写 - 写

  • 读 - 读:不存在任何问题,也不需要并发控制;
  • 读 - 写:有线程安全问题,事务可能出现隔离性问题,例如脏读、幻读、不可重复读;
  • 写 - 写:有线程安全问题,可能存在更新丢失问题。

在 MySQL InnoDB 中,MVCC 主要解决并发访问数据库带来的一系列问题

  • 读-写之间阻塞的问题;
  • 减少死锁的发生;
  • 解决一致性读(快照读)的问题。

如果没有MVCC,读-写之间,就必须加锁。

锁,是一种性能低下的组件。

MVCC就是一种不使用锁,去解决读写冲突问题,可以理解为是一种类似的写时复制(copy on write)、或者读时复制(copy on ready)机制。

本质上,MVCC是通过无锁的方式,去解决高并发场景下,读写、和写写冲突的问题。在尼恩的3高架构知识宇宙体系中,属于一种无锁编程的架构。

在多个事务同时读取和修改数据库时,MVCC 可以在尽量减少锁使用的情况下,用更高效、更好的方式去处理读写冲突,

使用MVCC,即便出现了读写冲突,也可以做到不加锁、非阻塞并发读,极大提高了数据库并发性能

数据库的四种隔离级别:

隔离界别 脏读 不可重复读 幻读
READUNCOMMITTED:未提交读 可能发生 可能发生 可能发生
READ COMMITTED:已提交读 解决 可能发生 可能发生
REPEATABLE READ:可重复读 解决 解决 可能发生
SERIALIZABLE:可串行化 解决 解决 解决

以上四个级别, 都没有脏写。

为啥呢? 脏写最为严重,四种隔离级别都不允许出现脏写,因此没有脏写。

MVCC 支持数据库的不同事务隔离级别,例如读未提交、读已提交、可重复读和串行化

如何做到的呢?

4、MVCC 的实现原理

MVCC的实现原理是依靠表记录中的3个隐含字段、undo log日志、ReadView来实现的。

MVCC 的实现主要依赖于这三个隐藏字段、Undo log 及 ReadView。

首先,看看第一个部分:三个隐藏字段

在 InnoDB 存储引擎为每行数据添加了三个隐藏字段:trx_id、roll_pointer、row_id。

列名 是否必须 描述
row_id 行 ID,唯一标识一条记录 (如果定义主键,它就没有啦)
transaction_id 事务 ID
roll_pointer DB_ROLL_PTR是一个回滚指针, 用于配合undo日志,指向上一个旧版本

对应到表隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID字段如下

  • DB_TRX_ID:6字节,最近修改事务id,记录创建这条记录或者最后一次修改该记录的事务id
  • DB_ROLL_PTR:7字节,回滚指针,指向这条记录的上一个版本,用于配合undolog,指向上一个旧版本
  • DB_ROW_JD:6字节,隐藏的主键,如果数据表没有主键,那么innodb会自动生成一个6字节的row_id

记录如图所示:

在上图中,DB_ROW_ID是数据库默认为该行记录生成的唯一隐式主键,由于已经存在id,这个字段就不用了。

在上图中,DB_TRX_ID是当前操作该记录的事务ID,DB_ROLL_PTR是一个回滚指针,用于配合undo日志,指向上一个旧版本

undo log

undolog被称之为回滚日志,表示在进行insert,delete,update操作的时候产生的方便回滚的日志

当进行insert操作的时候,产生的undolog只在事务回滚的时候需要,并且在事务提交之后可以被立刻丢弃

当进行update和delete操作的时候,产生的undolog不仅仅在事务回滚的时候需要,在快照读的时候也需要,所以不能随便删除,

只有在快照读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除(当数据发生更新和删除操作的时候,都只是设置一下老记录的deleted_bit,并不是真正的将过时的记录删除,因为为了节省磁盘空间,innodb有专门的purge线程来清除deleted_bit为true的记录,如果某个记录的deleted_bit为true,并且DB_TRX_ID相对于purge线程的ReadView 可见,那么这条记录一定是可以被清除的)。

下面我们来看一下undolog生成的记录链

roll_pointer 的作用是,可以构成undo log数据的版本链

版本链在每次进行 update 或者 delete 操作时,会将每次的操作细节,详细记录在 undo log 中。

每条 undo log 中,都记录了 rol_pointer 信息,通过 roll_pointer 进行关联,可以构成数据的版本链。

(1)假设有一个事务编号为10的事务向表中插入一条记录,那么此时行数据的状态为:

(2)假设有第二个事务编号为2对该记录的name做出修改,改为 校长

首先,在事务20修改该行记录数据时,数据库会对该行加排他锁。

然后把该行老数据拷贝到undolog中,作为 旧记录,即在undolog中有当前行的拷贝副本。拷贝完毕后,真正开始干活:修改该行name为 校长,并且修改隐藏字段的事务id为20,回滚指针指向拷贝到undolog的副本记录的 地址。

最后,事务提交后,释放锁。

(3)假设有第三个事务编号为30对该记录的name做了修改,改为李四

首先,在事务30修改该行记录数据时,数据库会对该行加排他锁。

然后把该行老数据拷贝到undolog中,作为 旧记录,即在undolog中有当前行的拷贝副本。拷贝完毕后,真正开始干活:修改该行name为李四,并且修改隐藏字段的事务id为30,回滚指针指向拷贝到undolog的副本记录的 地址。

最后,事务提交后,释放锁。

(4)假设有第三个事务编号为40对该记录的name做了修改,改为王五

首先,在事务40修改该行记录数据时,数据库会对该行加排他锁。

然后把该行老数据拷贝到undolog中,作为 旧记录,即在undolog中有当前行的拷贝副本。

拷贝完毕后,真正开始干活:修改该行name为 王五,并且修改隐藏字段的事务id为40,回滚指针指向拷贝到undolog的副本记录的 地址。

最后,事务提交后,释放锁。

从上述的一系列图中,大家可以发现,不同事务或者相同事务的对同一记录的修改,会导致该记录的undolog生成一条记录版本线性表,即链表,undolog的链首就是最新的旧记录,链尾就是最早的旧记录。

所以,一个记录会被一堆事务进行修改,一个记录中就会存在很多 Undo log。

那对某个事务来说,这么多 Undo log,到底应该选择哪些 Undo log 执行回滚呢?

即,哪个版本可以被事务看到呢?

ReadView 机制 就是用来为事务做可见性判断的,它可以判断版本链中的哪个版本是当前事务可见的。

上面的流程如果看明白了,那么大家需要再深入理解下ReadView的概念了。

5、ReadView 机制

5.1 什么是 ReadView

ReadView (读视图)是多版本并发控制(MVCC)中的一个重要概念。

ReadView 用于控制事务读取数据的逻辑视图,确保事务在整个过程中看到一致的数据状态。它是如何判断的呢?

ReadView是事务进行快照读操作的时候生产的读视图,

ReadView 是在该事务执行快照读的那一刻,会生成一个数据系统当前的快照,记录并维护系统当前活跃事务的id,事务的id值是递增的。

ReadView的最大作用是用来做可见性判断的,

当某个事务在执行快照读的时候,对该记录创建一个ReadView的视图,把它当作条件,去判断当前事务能够看到哪个版本的数据,

比如说,有可能读取到的版本,是最新的数据,

再比如说,也有可能读取的是数据版本,是当前行记录的undolog中某个版本的数据。

首先,看看ReadView 最重要的 4 个部分

注意:请点击图像以查看清晰的视图!

5.2 ReadView 读取规则

ReadView 仅仅记录一个事务开始的时候,系统的事务id列表,和相关的事务信息。

如何通过 ReadView ,去判断当前事务,应该去读取哪个记录的数据版本?

围绕ReadView,有一套可见性算法。

将要被修改的数据的最新记录中的 DB_TRX_ID(即当前事务 ID)取出来,与系统当前其他活跃事务的 ID 去对比(由ReadView维护)。

可见性算法大致流程如下

将要被修改的数据的最新记录中的DB_TRX_ID(当前事务id)取出来,与系统当前其他活跃事务的id去对比,如果DB_TRX_ID跟ReadView的属性做了比较,不符合可见性,那么就通过DB_ROLL_PTR回滚指针去取出undolog中的DB_TRX_ID做比较,即遍历链表中的DB_TRX_ID,直到找到满足条件的DB_TRX_ID,这个DB_TRX_ID所在的旧记录,就是当前事务能看到的最新老版本数据。

具体如下图:

首先要知道ReadView中的三个全局属性:

trx_list:一个数值列表,用来维护ReadView生成时刻系统正活跃的事务ID(1,2,3)

up_limit_id(up-id):记录trx_list列表中事务ID最小的ID(1)

low_limit_id:ReadView生成时刻系统尚未分配的下一个事务ID,(4)

具体的比较规则如下:

首先比较DB_TRX_ID < up_limit_id, 如果小于,则当前事务能看到DB_TRX_ID所在的记录,如果大于等于进入下一个判断

接下来判断DB_TRX_ID >= low_limit_id,如果大于等于则代表DB_TRX_ID所在的记录在ReadView生成后才出现的,那么对于当前事务肯定不可见,如果小于,则进入下一步判断

判断DB_TRX_ID是否在活跃事务中,如果在,则代表在ReadView生成时刻,这个事务还是活跃状态,还没有commit,修改的数据,当前事务也是看不到,

如果不在,则说明这个事务在ReadView生成之前就已经开始commit,那么修改的结果是能够看见的

下面,进行场景细致梳理,当被访问版本的 trx_id 属性值:

  • 如果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 时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。

5.3 ReadView 生成规则

但是,读取已提交可重复读 这两种隔离级别所产生的 ReadView是不同的。

  • 在读已提交(READ COMMITTED)的隔离级别下:事务中每次对数据进行 SELECT ,都会生成一个 ReadView。

  • 在可重复读( REPEATABLE READ)的隔离级别下:在一个事务中对一行数据第一次进行 SELECT 查询,会生成一个 ReadView,之后事务都将使用该 ReadView 进行数据的读取。

注意:请点击图像以查看清晰的视图!

总的来说,ReadView读视图就是在进行快照读时会产生一个ReadView视图、在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)。

ReadView是用来记录发生快照读那一刻所有的记录,当你下次就算有执行新的事务记录改变了,ReadView没变,读出来的数据依然是不变的。

而隔离级别中的RR(可重复读)、和RC(提交读)不同就是差在快照读时

RR创建一个快照和ReadView,并且下次快照读时使用的还是同一个ReadView,所以其他事务修改数据对他是不可见的、解决了不可重复读问题。

RC则是每次快照读时都会产生新的快照和ReadView、所以就会产生不可重复读问题。

5.4 ReadView 如何解决幻读

接下来,说明InnoDB 是如何解决幻读的。注意是 在可重复读( REPEATABLE READ)的隔离级别下。

假设现在表 user中只有一条数据,数据内容中,主键 id=1,隐藏的 trx_id=10,它的 undo log 如下图 所示。

假设现在有事务 A 和事务 B 并发执行, 事务 A 的事务 id 为 20 , 事务 B 的事务 id 为 30 。

步骤1:事务 A 开始第一次查询数据,查询的 SQL 语句如下。

select * from user where id >= 1; 

在开始查询之前,MySQL 会为事务 A 产生一个 ReadView,此时 ReadView 的内容如下:

trx_ids=[20,30] ,up_limit_id=20,

low_limit_id=31 , creator_trx_id=20

由于此时表 中只有一条数据,且符合 where id>=1 条件,因此会查询出来。

然后根据 ReadView机制,发现该行数据的trx_id=10,小于事务 A 的 ReadView 里 up_limit_id,这表示这条数据是事务 A 开 启之前,其他事务就已经提交了的数据,因此事务 A 可以读取到。

结论:事务 A 的第一次查询,能读取到一条数据,id=1。

步骤2:接着事务 B(trx_id=30),往表 中新插入两条数据,并提交事务。

insert into user(id,name) values(2,'李四');

insert into user(id,name) values(3,'王五');

此时表中就有三条数据了,对应的 undo 如下图所示:

步骤3:接着事务 A 开启第二次查询,根据可重复读隔离级别的规则,此时事务 A 并不会再重新生成ReadView。此时表 student 中的 3 条数据都满足 where id>=1 的条件,因此会先查出来。

然后根据ReadView 机制,判断每条数据是不是都可以被事务 A 看到。

1)首先 id=1 的这条数据,前面已经说过了,可以被事务 A 看到。

2)然后是 id=2 的数据,它的 trx_id=30,此时事务 A 发现,这个值处于 up_limit_id 和 low_limit_id 之 间,因此还需要再判断 30 是否处于 trx_ids 数组内。由于事务 A 的 trx_ids=[20,30],因此在数组内,这表 示 id=2 的这条数据是与事务 A 在同一时刻启动的其他事务提交的,所以这条数据不能让事务 A 看到。

3)同理,id=3 的这条数据,trx_id 也为 30,因此也不能被事务 A 看见。

结论:最终事务 A 的第二次查询,只能查询出 id=1 的这条数据。这和事务 A 的第一次查询的结果是一样 的,因此没有出现幻读现象,所以说在 MySQL 的可重复读隔离级别下,不存在幻读问题。

6、总结

通过本文,我们了解并掌握了MVCC 的概念、作用、工作原理等。

MVCC 为每个事务创建多个数据版本,每个版本对应一个特定时间点的数据库状态,不同事务可以基于各自的时间点来进行读取和写入操作,而不会相互干扰,极大提高了数据库并发性能

MVCC 依赖于InnoDB 下的三个隐藏字段、Undo log 及 ReadView 来实现,在一定程度上实现了 读写并发

说在最后

MVCC相关面试题,是非常常见的面试题。

以上的内容,如果大家能对答如流,如数家珍,基本上 面试官会被你 震惊到、吸引到。

最终,让面试官爱到 “不能自已、口水直流”。offer, 也就来了。

学习过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。

技术自由的实现路径:

实现你的 架构自由:

吃透8图1模板,人人可以做架构

10Wqps评论中台,如何架构?B站是这么做的!!!

阿里二面:千万级、亿级数据,如何性能优化? 教科书级 答案来了

峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?

100亿级订单怎么调度,来一个大厂的极品方案

2个大厂 100亿级 超大流量 红包 架构方案

… 更多架构文章,正在添加中

实现你的 响应式 自由:

响应式圣经:10W字,实现Spring响应式编程自由

这是老版本 《Flux、Mono、Reactor 实战(史上最全)

实现你的 spring cloud 自由:

Spring cloud Alibaba 学习圣经》 PDF

分库分表 Sharding-JDBC 底层原理、核心实战(史上最全)

一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之间混乱关系(史上最全)

实现你的 linux 自由:

Linux命令大全:2W多字,一次实现Linux自由

实现你的 网络 自由:

TCP协议详解 (史上最全)

网络三张表:ARP表, MAC表, 路由表,实现你的网络自由!!

实现你的 分布式锁 自由:

Redis分布式锁(图解 - 秒懂 - 史上最全)

Zookeeper 分布式锁 - 图解 - 秒懂

实现你的 王者组件 自由:

队列之王: Disruptor 原理、架构、源码 一文穿透

缓存之王:Caffeine 源码、架构、原理(史上最全,10W字 超级长文)

缓存之王:Caffeine 的使用(史上最全)

Java Agent 探针、字节码增强 ByteBuddy(史上最全)

实现你的 面试题 自由:

4800页《尼恩Java面试宝典 》 40个专题

免费获取11个技术圣经PDF: