赞
踩
在40岁老架构师 尼恩的读者交流群(50+)中,很多小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试机会,遇到很多很重要的面试题:
1.请解释Mysql MVCC,它的 作用是什么?
2.在MySQL中,MVCC是如何实现的?请简述其工作原理。
3.MVCC是如何解决读-写和写-写冲突的?
4.在并发环境中,当多个事务同时读取同一行数据时,MVCC是如何保证每个事务看到的数据版本是一致的?
5.MVCC如何帮助提高数据库的并发性能?
最近有小伙伴在面试阿里,又遇到了MVCC相关的面试题。小伙伴 支支吾吾的说了几句,没说清楚,面试挂了。
所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
这里,尼恩团队把MVCC 进行了全面的梳理,穿透式的梳理,
梳理为一个PDF文档 《MVCC 学习圣经:一次穿透MYSQL MVCC 》, 并且持续迭代。
这个文档将成为大家 面试的杀手锏, 此文当最新PDF版本,可以找40岁老架构师尼恩获取。
当然,上面的面试题以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V171版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,回复:领电子书
本文作者:
MVCC
机制的全称为Multi-Version Concurrency Control
,即多版本并发控制。
MVCC主要是为了提升数据库并发性能而设计的,其中采用更好的方式处理了读-写并发冲突,做到即使有读写冲突时,可以实现并发执行,从而提升并发能力,确保了任何时刻的读操作都是非阻塞的。
在众多的MySQL开源存储引擎中,几乎只有InnoDB实现了MVCC机制,其他的存储引擎如:MyISAM、memory等存储引擎中并未实现MVCC。
MVCC(Multi-Version Concurrency Control,多版本并发控制)一种并发控制机制,用于解决并发事务访问数据库时可能出现的一些问题,如脏读、不可重复读和幻读。
在MVCC机制中,数据库中的每个数据行都可以存在多个版本,并且每个事务看到的数据版本可能不同。具体来说,MVCC机制通过以下方式实现并发控制:
总的来说,MVCC机制通过维护多个数据版本,实现了事务的隔离性和并发性,保证了数据库的一致性和可靠性。它是许多现代数据库系统(如MySQL、PostgreSQL等)中常用的并发控制技术。
在并发读写数据库时,读操作可能会不一致的数据(脏读)。
为了避免这种情况,需要实现数据库的并发访问控制,最简单的方式就是加锁访问。
由于,加锁会将读写操作串行化,所以不会出现不一致的状态。
但是,读操作会被写操作阻塞,大幅降低读性能。
首先, 这里讲 事务的并发处理分为四大场景,分别是
读-读
、写-写
、读-写
、写-读
,这四种情况分别对应并发事务执行时的四种场景,为了后续分析MVCC
机制时方便理解,因此先将这几种情况说明。
读-读场景即是指多个事务/线程在并发读取一个相同的数据,比如事务T1
正在读取ID=16
的行记录,事务T2
也在读取这条记录,两个事务之间是并发执行的。
MySQL
执行查询语句,绝对不会对引起数据的任何变化,因此对于这种情况而言,不需要做任何操作,因为不改变数据就不会引起任何并发问题。
写-写场景也比较简单,也就是指多个事务之间一起对同一数据进行写操作,
比如事务T1
对ID=16
的行记录做修改操作,事务T2
则对这条数据做删除操作,事务T1
提交事务后想查询看一下,结果连这条数据都不见了,这也是所谓的脏写问题,也被称为更新覆盖问题,
对于这个问题在所有数据库、所有隔离级别中都是零容忍的存在,最低的隔离级别也要解决这个问题。
读-写、写-读实际上从宏观角度来看,可以理解成同一种类型的操作,但从微观角度而言则是两种不同的情况,
并发事务中同时存在读、写两类操作时,这是最容易出问题的场景,脏读、不可重复读、幻读都出自于这种场景中,当有一个事务在做写操作时,读的事务中就有可能出现这一系列问题,因此数据库才会引入各种机制解决。
对于写-写、读-写、写-读这三类存在线程安全问题的场景,最为简单粗暴的方式,通过 加锁 的方案确保线程安全。
但是,加锁会导致部分的串行化、整体串行化,因此效率会下降,而MVCC
机制的诞生则解决了这个问题。
因此MySQL
推出了MVCC
机制,在读-写并存(读-写、写-读)的场景,使用局部无锁架构,提升性能。
MVCC 机制 在线程安全问题和加锁串行化之间做了一定取舍,让两者之间达到了很好的平衡,即防止了脏读、不可重复读及幻读问题的出现,
又无需对并发读-写事务加锁处理。
Copy-On-Write(COW,写时复制)是一种常见的并发编程思想。
Copy-On-Write基本思想是,当多个线程需要对共享数据进行修改时,不直接在原始数据上进行操作,而是先将原始数据复制一份(即写时复制),然后在副本上进行Write。
Copy-On-Write 通过操作写操作副本,引入局部无锁架构,解决并且处理之间的数据冲突,提高了并发性能。
Copy-On-Write的实现步骤如下:
Copy-On-Write的优点包括:
然而,Copy-On-Write也有一些缺点,主要是由于数据复制和更新引用所带来的额外开销,可能会导致内存和性能方面的消耗增加。因此,适用场景需要根据具体情况进行评估和选择。
COW思想写操作之间是要互斥的,并且每次写操作都会有一次copy,所以只适合读大于写的情况。所以,COW思想 专门用于优化读的次数远大于写次数的场景。比如,Java的 并发容器CopyOnWriteArrayList。
CopyOnWriteArrayList 是jdk1.5以后并发包中提供的一种并发容器,写操作通过创建底层数组的新副本来实现,是一种读写分离的并发策略,我们也成为“写时复制容器”。
public boolean add(E e) { //加锁,对写操作保证线程安全 final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; //拷贝原容器,长度为原容器+1 Object[] newElements = Arrays.copyOf(elements, len + 1); //在新副本执行添加操作 newElements[len] = e; //底层数组指向新的数组 setArray(newElements); return true; } finally { lock.unlock(); } }
CopyOnWriteArrayList底层实现添加的原理是先copy出一个容器(可以简称副本),再往新的容器里添加这个新的数据,最后把新的容器的引用地址赋值给了之前那个旧的的容器地址,但是在添加这个数据的期间,其他线程如果要去读取数据,仍然是读取到旧的容器里的数据。
一图胜千言,40岁老架构师用一张图,给大家总结一如何借鸡生蛋,实现 Copy-On-Write思想的。
总体来说, MVCC Copy-On-Write思想, 包括三个组成部分:
事务要实现ACID,其中的原子性、一致性主要使用 undo-log 数据副本实现,undo-log 就是重做日志,一个事务一个 undo-log 日志副本。
多个事务的 undo-log 日志副本 (数据快照),组成了一个 副本链,如下下图:
MVCC 也就借鸡生蛋, 复用 这个 undo-log 副本链, 实现了自己 Copy-On-Write思想。
再看看 一个核心问题:MVCC与锁的关系?
还是一图胜千言,40岁老架构师用一张图,给大家总结一下MVCC和锁如何结合使用,提升事务并行能力的:
MVCC(Multi-Version Concurrency Control,多版本并发控制)和锁是数据库管理系统中两种不同的并发控制机制,它们在处理事务并发访问时起着不同的作用。
MVCC和锁之间的关系可以总结如下:
40岁老架构师尼恩提示: 很多时候,MVCC和锁可以结合使用,以实现更细粒度的并发控制,提高系统的性能和并发能力。
事务(Transaction)是数据库管理系统执行过程中的一个逻辑单位,它由一个有限的数据库操作序列构成。
这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。
事务的目的是确保数据的完整性和一致性,它通过一系列的操作,将数据库从一个一致性状态转换到另一个一致性状态。
事务通常具有以下四个特性,也被称为ACID属性:
原子性(Atomicity):事务作为一个整体执行,包含在其中的对数据库的操作要么全部执行,要么全部不执行。
一致性(Consistency):事务必须使数据库从一个一致性状态变换到另一个一致性状态。也就是说,一个事务的执行不能破坏数据库数据的完整性和一致性。
隔离性(Isolation):事务的执行不受其他事务的干扰,事务执行的中间结果对其他事务是不可见的。
持久性(Durability):一旦事务提交,则其结果就是永久性的,即使系统崩溃也不会丢失。
事务的这些特性确保了即使在高并发的环境中,数据库也能保持数据的完整性和一致性。在数据库系统中,事务是通过一系列的操作来完成的,包括数据的插入、更新、删除等。如果事务中的任何操作失败,或者因为某种原因被中断,那么整个事务都会回滚(Rollback),即撤销所有已经执行的操作,使数据库回到事务开始之前的状态。如果事务中的所有操作都成功完成,那么事务会提交(Commit),所做的更改会永久保存到数据库中。
什么是事务个隔离级别?事务隔离级别主要定义了事务在并发执行时的行为,特别是它们如何与其他事务交互以及它们如何看到数据库中的更改。
ANSI/ISO SQL标准定义了4中事务隔离级别:未提交读(read uncommitted),提交读(read committed),重复读(repeatable read),串行读(serializable)。
Oracle中默认的事务隔离级别是提交读 (read committed)。
对于MySQL的Innodb的默认事务隔离级别是重复读(repeated read)。
MySQL支持四种不同的事务隔离级别,每种级别都有其特定的行为和适用场景。以下是MySQL的四种事务隔离级别及其描述:
READ UNCOMMITTED(读取未提交)
READ COMMITTED(读取已提交)
REPEATABLE READ(可重复读取)
SERIALIZABLE(可串行化)
选择适当的事务隔离级别需要根据应用的需求和性能考虑进行权衡。在某些情况下,可能需要更高的隔离级别来确保数据的一致性,而在其他情况下,可能需要降低隔离级别以提高性能。同时,也需要注意不同隔离级别可能带来的并发问题,如脏读、不可重复读和幻读等。
脏读(Dirty Read):
一个事务读取到另一个尚未提交事务的修改。不可重复读(Non-repeatable Read):
在同一个事务内,多次读取同一数据返回的结果有所不同。幻读(Phantom Read):
一个事务在执行两次相同的查询时,因为另一个并发事务的插入或删除操作,导致两次查询返回的结果集不同。
一图胜千言,40岁老架构师用一张图,给大家总结一下 事务隔离级别、并发性、数据一致性的三角之间关系:
事务隔离级别和并发性和数据一致性密切相关。不同的隔离级别提供了不同的并发性和数据一致性保证。
参考阅读一下 尼恩的几篇相关文章:
MySQL
中仅在RC
读已提交级别、RR
可重复读级别才会使用MVCC
机制。
1:RU读未提交级别,不适用MVCC。
既然都允许存在脏读问题、允许一个事务读取另一个事务未提交的数据,直接进行当前读,那自然可以直接读最新版本的数据,因此无需MVCC
介入。
2:Serializable串行化级别不存在事务并发,不适用MVCC。
如果是Serializable串行化级别,因为会将所有的并发事务串行化处理,
Serializable串行化级别,不论事务是读操作,亦或是写操作,都会被排好队一个个执行,这都不存在所谓的多线程并发问题了,自然也无需MVCC介入。
MVCC
机制主要通过三个组件实现:
隐藏字段
Undo-log
日志ReadView
。在Innodb存储引擎中,每一行记录中都有隐藏字段
在有聚簇索引的情况下每一行记录中都会隐藏3个字段为DB_TRX_ID,DB_ROLL_PTR、deleted_bit,
除了上面的3个隐藏字段,没有聚簇索引还会有DB_ROW_ID这个字段。
40岁老架构师尼恩提是:隐藏字段的细节,稍后详细介绍。
在事务的ACID特性中,undo log(回滚日志)主要用于实现事务的原子性、隔离性、一致性的关键组件之一。它的主要作用包括:
事务的回滚操作:
当一个事务执行过程中发生错误或者被用户显式回滚时,数据库系统需要能够撤销该事务已经执行的操作,将数据库恢复到事务开始之前的状态。这就是回滚操作。
undo log记录了事务执行过程中所做的所有修改操作的逆操作,通过undo log可以快速回滚事务所做的修改,从而保证事务的原子性。
恢复和崩溃恢复:
当数据库系统发生崩溃或者异常关闭时,可能会导致部分事务未提交的修改操作丢失或者部分已提交的修改操作未持久化到磁盘。
通过undo log,数据库系统可以在恢复过程中, 将未提交的修改操作回滚,并将已提交但未持久化的修改操作重新应用到数据库中,从而保证数据库的一致性和完整性。
总的来说,undo log在数据库系统中扮演着非常重要的角色,它不仅用于实现事务的回滚操作和并发控制,还用于数据库系统的恢复和崩溃恢复。通过记录事务的修改操作和逆操作,undo log确保了数据库的原子性、隔离性和一致性,是数据库系统的关键组件之一。
尼恩在前面讲到, MVCC 实现了自己 Copy-On-Write思想提升并发能力的时候, 也需要数据的副本,这里既然undo-log 有了那么多副本,MVCC 就借鸡生蛋, 复用 这些数据副本。
所以,undo log 中的副本,可以用于实现多版本并发控制(MVCC),提升事务的并发性能。
那么多的数据副本,通过对比时间戳或者版本号,看到自己能看的版本?
undo log保存的是一个版本链,也就是使用DB_ROLL_PTR这个字段来连接的。
多个事务的 undo-log 日志副本 (数据快照),组成了一个 副本链,如下图:
那么,如果多个事务并行的读写操作,每一个事务应该使用那个版本呢?
简单来说,在MVCC中,每个事务可以有一个特定的时间戳或者版本号,而通过对比事务的时间点所能看到的数据版本的集合。
一般来说,时间戳或者版本号的对比规则包括以下几个方面:
通过遵循这些对比规则,数据库系统可以保证事务读取的数据是一致的、可靠的,并且与其他并发事务的操作相互独立。
一图胜千言。
40岁老架构师用一张图,给大家弄一个例子,展示一个下面的场景。
下面的图中,对于事务4来说,可以看到的数据版本,是事务1的已经提交的数据:
上图中,事务2,事务3,事务5的快照版本,事务4的是不可以看到的。
当然, 上面是通过时间比对来的,但是 mysql 的MVCC不是通过对比时间戳来实现的。
MVCC 使用 一个新的组件,read-view + 一组对比规则,来计算 可见版本。
read-view 有一些列的对比规则,这些规则用于确定一个事务在读取数据时,如何与数据库中的其他事务的版本号(这里其实就是事务ID)进行比较,以确定它所能看到的数据版本。
当 执行一个select语句时MVCC 会产生一致性视图read view
。那么这个read view 没有记录事务的开始时间,和截止时间 , 而是换成另一种方式去记录开始时间和截止时间,换成什么方式呢:
注意,上面是尼恩为大家总结和归纳的,比较清晰好记, mysql 的MVCC 版本的对比规则, 看上去非常、非常复杂。
下面是mysql 的MVCC 的read view 版本对比规则, 确实也是一个非常复杂的对比逻辑, 很多小伙伴傻傻看不懂, 并且背诵了半天还记不住,非常痛苦。
通过 上面的这个复杂的对比流程, read-view 终于确定一个事务在执行时所能看到的数据视图。
通常情况下,当你基于InnoDB
引擎建立一张表后,MySQL
除了会构建你显式声明的字段外,通常还会构建一些InnoDB
引擎的隐藏字段,
在InnoDB
引擎中,隐藏字段主要有DB_ROW_ID、DB_Deleted_Bit、DB_TRX_ID、DB_ROLL_PTR
这四个。
列名 | 是否必须 | 描述 |
---|---|---|
row_id | 否 | 隐藏主键,单调递增的行ID,不是必需的,占用6个字节。 |
deleted_bit | 是 | 删除标识,占用1个字节。 |
trx_id | 是 | 最近的更新事务Id,记录操作该行数据事务的事务ID,占用6个字节。 |
roll_pointer | 是 | 回滚指针,指向当前记录行的Undo-log日志中的旧版本数据,占用7个字节。 |
对于InnoDB
引擎的表而言,由于其表数据是按照 聚簇索引的格式存储,因此通常都会选择主键作为聚簇索引列,然后基于主键字段构建索引树,
但如若表中未定义主键,则会选择一个具备 唯一非空属性 的字段,作为聚簇索引的字段来构建树。
当两者都不存在时,InnoDB
就会隐式定义一个顺序递增的列ROW_ID
来作为聚簇索引列。
所以, 就算你的表中未定义主键、索引,其实默认也会存在一个聚簇索引,只不过这个索引在上层无法使用,仅提供给InnoDB
构建树结构存储表数据。
在MySQL中,对于InnoDB中一条delete
语句而言,当执行后并不会立马删除表的数据,而是将这条数据的Deleted_Bit
删除标识改为1/true
,而不是不会对数据库中的数据进行物理删除。
后续的查询SQL
检索数据时,如果检索到了这条数据,但看到隐藏字段Deleted_Bit=1
时,就知道该数据已经被其他事务delete
了,因此不会将这条数据纳入结果集。
Deleted_Bit
的优势:主要是能够有利于聚簇索引,比如当一个事务中删除一条数据后,后续又执行了回滚操作,假设此时是真正的删除了表数据,会发生如下两种情况:
①删除表数据时,有可能会破坏索引树原本的结构,导致 叶子节点合并的情况。
②事务回滚时,又需重新插入这条数据,再次插入时又会破坏前面的结构,导致 叶子节点分裂 的情况。
所以,当执行delete
语句时,只会改变将隐藏字段中的删除标识(Deleted_Bit
)改为1/true
,而不去执行物理删除(不去破坏索引树),如果后续事务出现回滚动作,直接将其标识再改回0/false
即可,这样就避免了索引树的结构调整。
谁来清理过期数据呢?
了防止“已删除”的数据占用过多的磁盘空间,同时确保清理数据时不会影响MVCC
的正常工作,Mysql使用 "Purger线程"完成“已删除”的数据的定期清理。
"Purger线程"用来定期检查数据库中的数据,并根据一些预定义的规则或条件来决定哪些数据应该被删除或清理。
Purger线程的主要职责包括:
Purger线程通常在后台运行,定期执行清理任务,以保持数据库的健康状态和良好的性能。
purger
线程自身也会维护一个ReadView
,如果某条数据的Deleted_Bit=true
,并且TRX_ID
对purge
线程的ReadView
可见,那么这条数据一定是可以被安全清除的(即不会影响MVCC
工作)。
TRX_ID
全称为transaction_id
,即是事务ID
的意思,
MySQL
对于每一个创建的事务,都会为其分配一个事务ID
,事务ID
同样遵循顺序递增的特性,即后来的事务ID
绝对会比之前的ID
要大,比如:
此时事务
T1
准备修改表字段的值,MySQL
会为其分配一个事务ID=1
,当事务T2
准备向表中插入一条数据时,又会为这个事务分配一个ID=2
…如果是SELECT语句,则分配的事务ID = 0;
表中的隐藏字段TRX_ID
,记录的就是最近一次改动当前这条数据的事务ID
,这个字段是实现MVCC
机制的核心之一。
ROLL_PTR
全称为rollback_pointer
,也就是回滚指针的意思,这个也是表中每条数据都会存在的一个隐藏字段。
当一个事务对一条数据做了改动后,都会将旧版本的数据放到Undo-log
日志中,而rollback_pointer
就是一个地址指针,指向Undo-log
日志中旧版本的数据。
当需要回滚事务时,就可以通过这个隐藏列,来找到改动之前的旧版本数据,而MVCC
机制也利用这点,实现了行数据的多版本。
Undo-log可以理解成回滚日志,它存储的是老版本数据。
在表记录修改之前,会先把原始数据拷贝到Undo-log里,如果事务回滚,即可以通过Undo-log来还原数据。
或者如果当前记录行不可见,可以顺着Undo-log链找到满足其可见性条件的记录行版本。
在insert/update/delete(本质也是做更新,只是更新一个特殊的删除位字段)操作时,都会产生Undo-log。
在InnoDB里,Undo-log分为如下两类:
insert Undo-log : 事务对insert新记录时产生的Undo-log, 只在事务回滚时需要, 并且在事务提交后就可以立即丢弃。
update Undo-log : 事务对记录进行delete和update操作时产生的Undo-log,不仅在事务回滚时需要,快照读也需要,只有当数据库所使用的快照中不涉及该日志记录,对应的回滚日志才会被删除。
Undo-log有什么用途呢?
1.事务回滚时,保证原子性和一致性。
2.如果当前记录行不可见,可以顺着undo log链找到满足其可见性条件的记录行版本(用于MVCC快照读)。
我们来看如下例子,理解一下Undo-log版本链。
如上述这段SQL
隶属于trx_id=1
的T1
事务,其中对同一条数据改动了两次,那Undo-log
日志中只会存储两条旧版本的数据,如下图:
从上图中可明显看出:
不同的旧版本数据,会以roll_ptr
回滚指针作为链接点,然后将所有的旧版本数据组成一个单向链表
。
请注意:最新的旧版本数据,都会插入到链表头中
,而不是追加到链表尾部。
细说一下执行上述
update
语句的详细过程:
1.对ID=1
这条要修改的行数据加上排他锁。2.将原本的旧数据拷贝到
Undo-log
的rollback Segment
区域。3.对表数据上的记录进行修改,修改完成后将隐藏字段中的
trx_id
改为当前事务ID
。4.将隐藏字段中的
roll_ptr
指向Undo-log
中对应的旧数据,并在提交事务后释放锁。
为什么Undo-log
日志要设计出版本链呢?
有如下两个好处:
一方面可以实现事务点回滚
;
另一方面则可以实现MVCC
机制。
与之前的删除标识类似,一条数据被delete
后并提交了,最终会从磁盘移除,而Undo-log
中记录的旧版本数据,同样会占用空间,
因此在事务提交后也会移除,移除的工作同样由purger
线程负责,purger
线程内部也会维护一个ReadView
,它会以此作为判断依据,来决定何时移除Undo
记录。
快照读,就是读取快照数据,即快照生成的那一刻的数据。
在不加锁的情况下,我们使常用的 普通的SELECT语句 就是快照读,如下:
SELECT * FROM USER WHERE …
当前读,就是读取最新的数据,要读取最新提交的数据版本。
我们在加锁SELECT语句,或者对数据进行增、删、改都会进行当前读。如下:
SELECT * FROM USER LOCK IN SHARE MODE;
SELECT * FROM USER FOR UPDATE;
INSERT INTO USER VALUES …
DELETE FROM USER WHERE …
UPDATE USER SET …
在MySQL中只有在RR和RC
这两个事务隔离级别下才会使用 快照读。
在RR中,快照会在事务中第一次SELECT语句执行时生成,只有在本事务中对数据进行更改 才会更新快照。
在RC中,每次SELECT都会重新生成一个快照,总是读取最新版本数据。
经过前面的分析,对于MVCC多版本并发控制,多版本是通过Undo-log日志
实现。
先来思考如下的问题:
如果T1
事务要查询id=1的一条行数据,此时这条行数据正在被T2
事务修改,那也就代表着这条数据可能存在多个旧版本数据,T1
事务在查询时,应该读这条数据的哪个版本呢?
此时就需要用到ReadView
,用它来做多版本的并发控制,根据查询的时机,来选择一个当前事务可见的旧版本数据读取。
当一个事务在尝试读取一条数据时,MVCC
基于当前MySQL
的运行状态生成的快照,也被称之为读视图,即ReadView
,在这个快照中记录着当前所有活跃事务的ID
(活跃事务是指还在执行的事务,即未结束(提交/回滚)的事务)。
ReadView是事务在进行快照读的时候生成的记录快照, 可以帮助我们解决可见性问题的。
当一个事务启动后,首次执行select
操作时,MVCC
就会生成一个数据库当前的ReadView
,
通常而言,一个事务与一个ReadView
属于一对一的关系(不同隔离级别下也会存在细微差异),ReadView
一般包含4个核心属性:
属性 | 描述 |
---|---|
creator_trx_id | 代表创建当前这个ReadView 的事务ID 。 |
trx_ids | 表示在生成当前ReadView 时,系统内活跃(未提交)的事务ID 列表,它的数据结构为一个List。(注意 :这里的trx_ids中的活跃事务,不包括当前事务自己和已提交的事务,这点非常重要) |
up_limit_id | 活跃的事务列表(trx_ids)中,最小的事务ID ,如果trx_ids为空,则up_limit_id 为 low_limit_id。 |
low_limit_id | 表示在生成当前ReadView 时,系统中要给下一个事务分配的ID值 。(注意 :它并不是目前系统中活跃事务的最大ID,因为MySQL的事务ID是按序递增的,因此当启动一个新的事务时,都会为其分配事务ID,而这个low_limit_id则是整个MySQL中要为下一个事务分配的ID值。) |
我们假设目前数据库中共有T1~T6
这6个事务,T1、T2、T4、T6
还在执行,T3
已经回滚,T5
已经提交,
此时当有一条查询语句执行时,就会利用MVCC
机制生成一个ReadView
,由于在MySQL中单纯由一条select
语句组成的事务并不会分配事务ID
,因此默认为0
,所以目前这个ReadView的信息如下:
访问某条记录的时候如何判断该记录是否可见,具体规则如下:
事务ID = creator_trx_id
,那么表示当前事务访问的是自己修改过的记录,那么该版本对当前事务可见;事务ID < up_limit_id
,那么表示生成该版本的事务在当前事务生成 ReadView 前已经提交,所以该版本可以被当前事务访问。事务ID > low_limit_id
值,那么表示生成该版本的事务在当前事务生成 ReadView 后才开启,所以该版本不可以被当前事务访问。事务ID在 up_limit_id和m_low_limit_id
之间,那就需要判断一下版本的事务ID是不是在 trx_ids 列表中,如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;上面这种图,网上有上万篇文章, 都是抄来抄去, 没有一篇文章做了总结和简化。
关于这个对比规则,由于逻辑复杂,导致尽管大家看了那些文章,甚至看了很多视频,还是不能理解透彻, 迷迷糊糊的,面试的时候 说不清楚,也很容易忘了。
尼恩团队看不下去,用咱们的雄厚技术实力(洪荒之力), 给大家来总结和简化。
具体如下:
此图,是全网的第一张彻底穿透式的解读 MVCC的对比规则的图。
通过此文,尼恩团队 第一次,帮助大家搞清楚复杂的 MVCC的底层原理。
此图很容易理解,很容易记忆。
大家可收藏起来, 面试之前复习一下,一定能吊打面试官。
不对,是吊死面试官。
尼恩团队用深厚的架构功力,非常喜欢也非常善于,把复杂的问题做清晰深入的穿透式、起底式的分析:
这个技术难题一旦掌握,大家内力猛涨。 所以,建议大家去看看尼恩的这些核心内容。
而且,尼恩团队进行一次真正的AI架构穿透,帮助大家穿透AI架构。
扯远了,言归正传。
在MySQL中只有在RR(可重复读)和RC(读已提交)
这两个事务隔离级别下有效,生成ReadView规则是不同的:
在RR中,
ReadView
会在事务中第一次SELECT
语句执行时生成,只有在本事务中对数据进行更改才会更新快照。在RC中,每次SELECT都会重新生成一个
ReadView
,总是读取最新版本数据。读已提交和可重复读唯一的区别在于:
1.在RC隔离级别下,是每个select都会创建最新的ReadView;
2.而在RR隔离级别下,则是当事务中的第一个select请求才创建ReadView。
经过前面的分析后已得知:
当一个事务尝试改动某条数据时,会将原本表中的旧数据放入Undo-log
日志中。
当一个事务尝试查询某条数据时,MVCC
会生成一个ReadView
快照。
其中Undo-log
主要实现数据的多版本,ReadView
则主要实现多版本的并发控制。
结合如下例子说明:
-- 事务T1:trx_id=1
UPDATE user_info SET name = "小夏" WHERE id = 1;
UPDATE user_info SET sex = "女" WHERE id = 1;
-- 事务T2:trx_id=2
SELECT * FROM user_info WHERE id = 1;
目前存在T1、T2
两个并发事务,T1
目前在修改ID=1
的这条数据,而T2
则准备查询这条数据,那么T2
在执行时具体过程如下:
1.当事务中出现select
语句时,会先根据MySQL
的当前情况生成一个ReadView
。
2.判断行数据中的隐藏列trx_id
与ReadView.creator_trx_id
是否相同:
ReadView
和修改行数据的事务是同一个,自然可以读取最新版数据。3.判断隐藏列trx_id
是否小于ReadView.up_limit_id
最小活跃事务ID
:
4.判断隐藏列trx_id
是否小于ReadView.low_limit_id
这个值:
ID
在up_limit_id、low_limit_id
之间,需要进一步判断。5.如果隐藏列trx_id
小于low_limit_id
,继续判断trx_id
是否在trx_ids
中:
在:表示改动行数据的事务目前依旧在执行,不能访问最新版数据。
不在:表示改动行数据的事务已经结束,可以访问最新版的数据。
然后经过上述一系列判断后,可以得知:目前查询数据的事务到底能不能访问最新版的数据。
如果能,就直接拿到表中的数据并返回,反之,不能则去Undo-log
日志中获取旧版本的数据返回。
MVCC
多版本并发控制,其中的多版本主要依赖Undo-log
日志来实现,而并发控制则通过表的隐藏字段
+ReadView
快照来实现,通过Undo-log
日志、隐藏字段
、ReadView
快照这3点,就实现了MVCC
机制。
MVCC 相关的面试题,是非常常见的面试题。也是核心面试题。
以上的内容,如果大家能对答如流,如数家珍,基本上 面试官会被你 震惊到、吸引到。最终,让面试官爱到 “不能自已、口水直流”。offer, 也就来了。
在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典》V174,在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。
另外,如果没有面试机会,可以找尼恩来帮扶、领路。尼恩已经指导了大量的就业困难的小伙伴上岸.
前段时间,帮助一个40岁+就业困难小伙伴拿到了一个年薪100W的offer,小伙伴实现了 逆天改命 。
……完整版尼恩技术圣经PDF集群,请找尼恩领取
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。