滴滴一面,痛失40K:因MVCC没说明白
文章很长,且持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录 博客园版 为您奉上珍贵的学习资源 :
免费赠送 :《尼恩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岁老架构师尼恩交流。
技术自由的实现路径:
实现你的 架构自由:
《阿里二面:千万级、亿级数据,如何性能优化? 教科书级 答案来了》
《峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?》
… 更多架构文章,正在添加中
实现你的 响应式 自由:
这是老版本 《Flux、Mono、Reactor 实战(史上最全)》
实现你的 spring cloud 自由:
《Spring cloud Alibaba 学习圣经》 PDF
《分库分表 Sharding-JDBC 底层原理、核心实战(史上最全)》
《一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之间混乱关系(史上最全)》
实现你的 linux 自由:
实现你的 网络 自由:
《网络三张表:ARP表, MAC表, 路由表,实现你的网络自由!!》
实现你的 分布式锁 自由:
实现你的 王者组件 自由:
《队列之王: Disruptor 原理、架构、源码 一文穿透》
《缓存之王:Caffeine 源码、架构、原理(史上最全,10W字 超级长文)》
《Java Agent 探针、字节码增强 ByteBuddy(史上最全)》