尼恩说在前面
在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,遇到很多很重要的面试题:
1.请解释什么是MVCC,它在数据库中的作用是什么?
2.在MySQL中,MVCC是如何实现的?请简述其工作原理。
3.MVCC是如何解决读-写和写-写冲突的?
4.在并发环境中,当多个事务同时读取同一行数据时,MVCC是如何保证每个事务看到的数据版本是一致的?
5.MVCC如何帮助提高数据库的并发性能?
最近有小伙伴在面试京东,又遇到了相关的面试题。小伙伴懵了,因为没有遇到过,所以支支吾吾的说了几句,面试官不满意,面试挂了。
所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V171版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,回复:领电子书
本文目录
● 尼恩说在前面
● 本文目录
● 什么是MVCC
● MVCC的根本目标:提升并发能力
● 提升并发的思想:COW思想
● Java中的CopyOnWriteArrayList
● 为啥MVCC要使用COW思想 呢?借鸡生蛋
● MVCC与锁的关系
● MySQL事务隔离级别与MVCC
● 什么是事务
● 事务的ACID特性
● 4种 事务隔离级别
● 隔离级别、并发性、数据一致性的三角之间关系
● 并发事务的四种场景和MVCC的作用
● 读●读场景:
● 写●写场景
● 读●写、写●读场景
● 各并发事务场景的解决方案
● MVCC的实现原理分析
● 组件1: InnoDB表的隐藏字段
● 隐藏的主键:row_id
● 隐藏的删除标识:deleted_bit
● 隐藏的最近更新事务ID:trx_id (DB_TRX_ID)
● 隐藏的回滚指针:roll_ptr
● 组件2:InnoDB引擎的Undo-log日志
● Undo-log有什么用途呢?
● Undo-log日志 版本链
● 为什么Undo-log
日志要设计出版本链呢?
● 组件3:MVCC核心ReadView
● 什么是ReadView呢?
● ReadView的核心属性
● ReadView的读取规则
● ReadView的生成规则
● MVCC实现原理
● MVCC实现原理 总结
● 注意:RC/RR 适用MVCC
● MVCC 演示过程
● 快照读和当前读
● 面试官追问:MySQL 如何通过MVCC实现 读已提交 隔离级别的?
● 1. MVCC的基本概念
● 2. Read View的作用
● 3. 数据版本的可见性
● 5. 读已提交 隔离级别总结
● 面试官再追问:MySQL 又是如何通过MVCC实现 可重复读(REPEATABLE READ) 隔 离级别的?
● 1. 一致性视图(Read View)的创建
● 2. 解决脏读和不可重复读
● 3. 快照读与当前读
● 5. 如何避免幻读
● 问题 总结
● 面试官再追问:READ COMMITTED和REPEATABLE READ的区别是什么?
● 1. 核心区别:Read View的创建时机
● 2. 数据一致性表现
● 3. 幻读问题
● 4. 性能和并发性
● 5. 答案总结
● 面试官再追问:READ COMMITTED的缺点是什么?
● 1. 数据一致性较弱
● 2. 对业务逻辑的潜在影响
● 5. 适用场景的局限性
● 面试官再追问:MySQL默认的隔离级别为什么是REPEATABLE READ?REPEATABLE READ 的缺点是什么
● 1. MySQL默认隔离级别为 REPEATABLE READ 的原因
● 2.REPEATABLE READ
的缺点
● 3 答案总结
● 面试官再追问:大厂的MySQL,为啥一般调整为的READ COMMITTED 隔离级别?
● 1. 提升并发性能
● 2. 减少死锁
● 3. 适应高并发场景
● 4. 兼容性和性能优化
● 5. 互联网业务 一般允许幻读
● 6 答案总结
● 面试官再追问:可重复读 什么时候使用了邻键锁 / 间隙锁(Gap Lock)和Next●Key Lock?
● 1. 范围查询时 使用邻键锁 :
● 2. 插入操作引发时 使用邻键锁 :
● 3. 唯一索引冲突检测时 使用邻键锁 :
● 面试官再追问:如何选择隔离级别?
● 1. 数据一致性要求
● 2. 读写 操作要求
● 3. 锁开销和 性能表现
● 4. 数据一致性问题
● 4. 默认隔离级别与调整
● 5. 实际业务参考案例
● 答案总结
什么是MVCC
MVCC
机制的全称为Multi-Version Concurrency Control
,即多版本并发控制。
MVCC主要是为了提升数据库并发性能而设计的,其中采用更好的方式处理了读-写并发冲突,做到即使有读写冲突时,可以实现并发执行,从而提升并发能力,确保了任何时刻的读操作都是非阻塞的。
在众多的MySQL开源存储引擎中,几乎只有InnoDB实现了MVCC机制,其他的存储引擎如:MyISAM、memory等存储引擎中并未实现MVCC。
MVCC(Multi-Version Concurrency Control,多版本并发控制)一种并发控制机制,用于解决并发事务访问数据库时可能出现的一些问题,如脏读、不可重复读和幻读。
在MVCC机制中,数据库中的每个数据行都可以存在多个版本,并且每个事务看到的数据版本可能不同。具体来说,MVCC机制通过以下方式实现并发控制:
(1) 版本控制:
每当对数据库中的数据行进行更新操作时,不是直接覆盖原始数据,而是创建一个新的数据版本,并将新版本的数据与事务的时间戳相关联。
(2) 快照读取:
在MVCC中,读取操作不会阻塞写入操作,也不会阻塞其他读取操作。事务可以读取数据库中的数据快照,即某个时间点之前的数据版本,而不会受到其他事务的影响。
(3) 可见性判断:
在执行读取操作时,事务只能看到在其开始之前已经提交的数据版本,而看不到其他事务正在修改的数据。这样可以避免脏读和不可重复读问题。
(4) 回滚操作:
当事务回滚时,不会对数据库中的数据进行物理删除或修改,而是标记事务所涉及的数据版本为无效,使得其他事务无法看到该版本。
总的来说,MVCC机制通过维护多个数据版本,实现了事务的隔离性和并发性,保证了数据库的一致性和可靠性。它是许多现代数据库系统(如MySQL、PostgreSQL等)中常用的并发控制技术。
MVCC的根本目标:提升并发能力
在并发读写数据库时,读操作可能会不一致的数据(脏读)。
为了避免这种情况,需要实现数据库的并发访问控制,最简单的方式就是加锁访问。
由于,加锁会将读写操作串行化,所以不会出现不一致的状态。
但是,读操作会被写操作阻塞,大幅降低读性能。
提升并发的思想:COW思想
Copy-On-Write(COW,写时复制)是一种常见的并发编程思想。
Copy-On-Write基本思想是,当多个线程需要对共享数据进行修改时,不直接在原始数据上进行操作,而是先将原始数据复制一份(即写时复制),然后在副本上进行Write。
Copy-On-Write 通过操作副本,可以避免多个线程之间的数据冲突,提高了并发性能。
Copy-On-Write的实现步骤如下:
(1) 读取数据:多个线程同时读取共享数据时,它们可以直接访问原始数据,而不需要复制。因为读取操作不会修改数据,所以可以安全地共享原始数据。
(2) 写入数据:当某个线程需要修改共享数据时,首先会将原始数据进行复制(即写时复制),然后在副本上进行修改。这样做的好处是,其他线程仍然可以继续读取原始数据,不受写入线程的影响。
(3) 更新引用:写入线程完成修改后,会更新共享数据的引用,使得其他线程后续访问时可以获取到最新的数据副本。
Copy-On-Write的优点包括:
- 线程安全:通过复制数据副本并在副本上进行修改,避免了多线程并发修改原始数据时的数据冲突问题,从而提高了线程安全性。
- 减少锁竞争:由于读取操作不需要加锁,所以可以减少锁竞争,提高了并发性能。
- 节省内存:只有在有写入操作时才会进行数据复制,而读取操作可以共享原始数据,因此可以节省内存空间。
然而,Copy-On-Write也有一些缺点,主要是由于数据复制和更新引用所带来的额外开销,可能会导致内存和性能方面的消耗增加。因此,适用场景需要根据具体情况进行评估和选择。
COW思想写操作之间是要互斥的,并且每次写操作都会有一次copy,所以只适合读大于写的情况。所以,COW思想 专门用于优化读的次数远大于写次数的场景。比如,Java的 并发容器CopyOnWriteArrayList。
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();
}
}
为啥MVCC要使用Copy-On-Write思想呢?借鸡生蛋
总之 MVCC Copy-On-Write思想, 包括三个组成部分:
为啥MVCC要使用Copy-On-Write思想呢?
主要是MVCC 是结合事务使用的,事务为何实现隔离性,每一个事务都需要有自己的操作版本,引入了一个数据副本维护机制 undo-log ,也就是重做日志,
所以, MVCC 也就复用 这个 undo-log 机制, 实现了自己 Copy-On-Read 思想。
MVCC与锁的关系
一图胜千言,40岁老架构师用一张图,给大家总结一下MVCC和锁如何结合使用,提升事务并行能力的:
MVCC(Multi-Version Concurrency Control,多版本并发控制)和锁是数据库管理系统中两种不同的并发控制机制,它们在处理事务并发访问时起着不同的作用。
(1) MVCC:
- MVCC通过维护数据的多个版本来实现并发控制,允许事务并发访问数据库而不会发生阻塞。
- 在MVCC中,读取操作不会阻塞写入操作,也不会阻塞其他读取操作。每个事务可以看到一个一致性的数据快照,而不受其他事务的影响。
- MVCC主要用于读取操作的并发控制,可以有效地避免脏读、不可重复读和幻读等并发问题。
(2) 锁:
锁是一种悲观并发控制机制,通过在事务访问数据时对数据进行加锁,以防止其他事务对该数据进行修改或读取。
在使用锁进行并发控制时,可能会出现阻塞和死锁等问题,特别是在高并发的情况下,锁的粒度过大或者锁的竞争过于激烈时,性能可能会受到影响。
MVCC和锁之间的关系可以总结如下:
- MVCC是一种 乐观的并发控制机制,通过多副本的版本控制来实现并发访问,而不需要对数据进行加锁。
- 锁是一种 悲观的并发控制机制,通过对数据进行加锁来确保事务的隔离性和一致性。
40岁老架构师尼恩提示: 很多时候,MVCC和锁可以结合使用,以实现更细粒度的并发控制,提高系统的性能和并发能力。
MySQL事务隔离级别与MVCC
什么是事务
事务(Transaction)是数据库管理系统执行过程中的一个逻辑单位,它由一个有限的数据库操作序列构成。
这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。
事务的目的是确保数据的完整性和一致性,它通过一系列的操作,将数据库从一个一致性状态转换到另一个一致性状态。
事务的ACID特性
事务通常具有以下四个特性,也被称为ACID属性:
(1) 原子性(Atomicity):事务作为一个整体执行,包含在其中的对数据库的操作要么全部执行,要么全部不执行。
(2) 一致性(Consistency):事务必须使数据库从一个一致性状态变换到另一个一致性状态。也就是说,一个事务的执行不能破坏数据库数据的完整性和一致性。
(3) 隔离性(Isolation):事务的执行不受其他事务的干扰,事务执行的中间结果对其他事务是不可见的。
(4) 持久性(Durability):一旦事务提交,则其结果就是永久性的,即使系统崩溃也不会丢失。
事务的这些特性确保了即使在高并发的环境中,数据库也能保持数据的完整性和一致性。在数据库系统中,事务是通过一系列的操作来完成的,包括数据的插入、更新、删除等。如果事务中的任何操作失败,或者因为某种原因被中断,那么整个事务都会回滚(Rollback),即撤销所有已经执行的操作,使数据库回到事务开始之前的状态。如果事务中的所有操作都成功完成,那么事务会提交(Commit),所做的更改会永久保存到数据库中。
4种 事务隔离级别
什么是事务个隔离级别?事务隔离级别主要定义了事务在并发执行时的行为,特别是它们如何与其他事务交互以及它们如何看到数据库中的更改。
ANSI/ISO SQL标准定义了4中事务隔离级别:未提交读(read uncommitted),提交读(read committed),重复读(repeatable read),串行读(serializable)。
Oracle中默认的事务隔离级别是提交读 (read committed)。
对于MySQL的Innodb的默认事务隔离级别是重复读(repeated read)。
MySQL支持四种不同的事务隔离级别,每种级别都有其特定的行为和适用场景。以下是MySQL的四种事务隔离级别及其描述:
(1) READ UNCOMMITTED(读取未提交)
- 允许读取尚未提交的数据变更。
- 这是最低的隔离级别,它可能导致脏读、不可重复读和幻读。
- 在这个级别,一个事务可以读取到另一个尚未提交事务的修改,这可能导致数据的不一致性。
(2) READ COMMITTED(读取已提交)
- 只允许读取并发事务已经提交的数据。
- 这个级别可以防止脏读,但仍可能导致不可重复读和幻读。
- 在这个级别,每个事务只能看到它开始时的数据状态以及它提交时其他事务所做的提交。
(3) REPEATABLE READ(可重复读取)
- 这是MySQL的默认隔离级别。
- 它确保在同一事务中多次读取同一数据时,看到的是相同的数据版本,即使其他事务在此期间修改了这些数据。
- 尽管可以避免脏读和不可重复读,但在这个级别下仍可能出现幻读(即在一个事务中,两次相同的查询可能会返回不同的结果集,因为其他事务在此期间插入了新的记录)。
(4) SERIALIZABLE(可串行化)
- 这是最高的隔离级别。
- 它通过强制事务串行执行来避免脏读、不可重复读和幻读。
在这个级别,每个事务在执行时都会完全锁定它所访问的数据,从而确保数据的一致性。但这也可能导致性能下降,因为并发事务必须等待其他事务完成才能执行。
选择适当的事务隔离级别需要根据应用的需求和性能考虑进行权衡。在某些情况下,可能需要更高的隔离级别来确保数据的一致性,而在其他情况下,可能需要降低隔离级别以提高性能。同时,也需要注意不同隔离级别可能带来的并发问题,如脏读、不可重复读和幻读等。
脏读(Dirty Read):
一个事务读取到另一个尚未提交事务的修改。不可重复读(Non-repeatable Read):
在同一个事务内,多次读取同一数据返回的结果有所不同。幻读(Phantom Read):
一个事务在执行两次相同的查询时,因为另一个并发事务的插入或删除操作,导致两次查询返回的结果集不同。
隔离级别、并发性、数据一致性的三角之间关系
一图胜千言,40岁老架构师用一张图,给大家总结一下 事务隔离级别、并发性、数据一致性的三角之间关系:
事务隔离级别和并发性和数据一致性密切相关。不同的隔离级别提供了不同的并发性和数据一致性保证。
(1) 并发性:
- 并发性指的是数据库系统同时处理多个事务的能力。隔离级别越低,允许的并发操作越多,系统的并发性能越高。
- 但是,过高的并发操作可能会导致事务之间的相互干扰,产生一些并发问题,如脏读、不可重复读和幻读。
(2) 数据一致性: - 数据一致性指的是事务执行后,数据库中的数据是否保持一致性。隔离级别越高,数据一致性越好,但对并发操作的限制也越严格。
- 高隔离级别可以防止一些并发问题的产生,如脏读、不可重复读和幻读,但会降低系统的并发性能。
参考阅读一下 尼恩的几篇相关文章:
并发事务的四种场景和MVCC的作用
并发事务中又会分为四种情况,分别是
读-读
、写-写
、读-写
、写-读
,
这四种情况分别对应并发事务执行时的四种场景,为了后续分析MVCC
机制时方便理解,因此先将这几种情况说明。
读-读场景:
读-读场景即是指多个事务/线程在并发读取一个相同的数据,比如事务T1
正在读取ID=16
的行记录,事务T2
也在读取这条记录,两个事务之间是并发执行的。
MySQL
执行查询语句,绝对不会对引起数据的任何变化,因此对于这种情况而言,不需要做任何操作,因为不改变数据就不会引起任何并发问题。
写-写场景
写-写场景也比较简单,也就是指多个事务之间一起对同一数据进行写操作,
比如事务T1
对ID=16
的行记录做修改操作,事务T2
则对这条数据做删除操作,事务T1
提交事务后想查询看一下,结果连这条数据都不见了,这也是所谓的脏写问题,也被称为更新覆盖问题,
对于这个问题在所有数据库、所有隔离级别中都是零容忍的存在,最低的隔离级别也要解决这个问题。
读-写、写-读场景
读-写、写-读实际上从宏观角度来看,可以理解成同一种类型的操作,但从微观角度而言则是两种不同的情况,
读-写是指一个事务先开始读,然后另一个事务则过来执行写操作,
写-读则相反,主要是读、写发生的前后顺序的区别。
并发事务中同时存在读、写两类操作时,这是最容易出问题的场景,脏读、不可重复读、幻读都出自于这种场景中,当有一个事务在做写操作时,读的事务中就有可能出现这一系列问题,因此数据库才会引入各种机制解决。
各并发事务场景的解决方案
对于写-写、读-写、写-读这三类场景,都是可以利用 加锁 的方案确保线程安全,但加锁会导致部分事务串行化,因此效率会下降,而MVCC
机制的诞生则解决了这个问题。
因此MySQL
就基于读-写并存的场景,推出了MVCC
机制,在线程安全问题和加锁串行化之间做了一定取舍,让两者之间达到了很好的平衡,即防止了脏读、不可重复读及幻读问题的出现,
又无需对并发读-写事务加锁处理。
MVCC的实现原理分析
MVCC的实现原理是依靠表记录中的3个隐含字段、undo log日志、Read View来实现的。
MVCC
机制主要通过三个组件实现:
隐藏字段
Undo-log
日志ReadView
。
组件1: InnoDB表的隐藏字段
通常情况下,当你基于InnoDB
引擎建立一张表后,MySQL
除了会构建你显式声明的字段外,通常还会构建一些InnoDB
引擎的隐藏字段,
在InnoDB
引擎中,隐藏字段主要有DB_ROW_ID、DB_Deleted_Bit、DB_TRX_ID、DB_ROLL_PTR
这四个。
列名 | 是否必须 | 描述 |
---|---|---|
row_id | 否 | 隐藏主键,单调递增的行ID,不是必需的,占用6个字节。 |
DB_deleted_bit | 是 | 删除标识,占用1个字节。 |
DB_trx_id | 是 | 最近的更新事务Id,记录操作该行数据事务的事务ID,占用6个字节。 |
DB_roll_pointer | 是 | 回滚指针,指向当前记录行的Undo-log日志中的旧版本数据,占用7个字节。 |
对应到表隐式定义的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日志,指向上一个旧版本
隐藏的主键:row_id
对于InnoDB
引擎的表而言,由于其表数据是按照聚簇索引的格式存储,因此通常都会选择主键作为聚簇索引列,然后基于主键字段构建索引树,但如若表中未定义主键,则会选择一个具备唯一非空属性的字段,作为聚簇索引的字段来构建树。
当两者都不存在时,
InnoDB
就会隐式定义一个顺序递增的列ROW_ID
来作为聚簇索引列。所以就算你的表中未定义主键、索引,其实默认也会存在一个聚簇索引,只不过这个索引在上层无法使用,仅提供给
InnoDB
构建树结构存储表数据。
隐藏的删除标识:deleted_bit
在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
工作)。
隐藏的最近更新事务ID:trx_id (DB_TRX_ID)
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
ROLL_PTR
全称为rollback_pointer
,也就是回滚指针的意思,这个也是表中每条数据都会存在的一个隐藏字段。
当一个事务对一条数据做了改动后,都会将旧版本的数据放到Undo-log
日志中,而rollback_pointer
就是一个地址指针,指向Undo-log
日志中旧版本的数据。
当需要回滚事务时,就可以通过这个隐藏列,来找到改动之前的旧版本数据,而MVCC
机制也利用这点,实现了行数据的多版本。
组件2:InnoDB引擎的Undo-log日志
Undo-log可以理解成回滚日志,它存储的是老版本数据。
在表记录修改之前,会先把原始数据拷贝到Undo-logg里,如果事务回滚,即可以通过Undo-log来还原数据。
或者如果当前记录行不可见,可以顺着Undo-log链找到满足其可见性条件的记录行版本。
在insert/update/delete(本质也是做更新,只是更新一个特殊的删除位字段)操作时,都会产生Undo-log。
在InnoDB里,Undo-log分为如下两类:
(1) insert Undo-log : 事务对insert新记录时产生的Undo-log, 只在事务回滚时需要, 并且在事务提交后就可以立即丢弃。
(2) update Undo-log : 事务对记录进行delete和update操作时产生的Undo-log,不仅在事务回滚时需要,快照读也需要,只有当数据库所使用的快照中不涉及该日志记录,对应的回滚日志才会被删除。
Undo-log有什么用途呢?
1.事务回滚时,保证原子性和一致性。
2.如果当前记录行不可见,可以顺着undo log链找到满足其可见性条件的记录行版本(用于MVCC快照读)。
Undo-log日志 版本链
所有版本的数据都只会存一份,然后通过回滚指针连接起来,之后就是通过一定的规则找到具体是哪个版本上的数据就行了。
’假设现在有一张account表,其中有id和name两个字段,那么版本链的示意图如下:
而具体版本链的比对规则如下,首先从版本链中拿出最上面第一个版本的事务ID开始逐个往下进行比对:
(其中min_id指向ReadView中未提交事务数组中的最小事务ID,而max_id指向ReadView中的已经创建的最大事务ID)
如果落在绿色区间(DB_TRX_ID < min_id):这个版本比min_id还小(事务ID是从小往大顺序生成的),说明这个版本在SELECT之前就已经提交了,所以这个数据是可见的。或者*(这里是短路或,前面条件不满足才会判断后面这个条件)*这个版本的事务本身就是当前SELECT语句所在事务的话,也是一样可见的**;
如果落在红色区间(DB_TRX_ID > max_id):表示这个版本是由将来启动的事务来生成的,当前还未开始,那么是不可见的;
如果落在黄色区间(min_id <= DB_TRX_ID <= max_id):这个时候就需要再判断两种情况:
-
- 如果这个版本的事务ID在ReadView的未提交事务数组中,表示这个版本是由还未提交的事务生成的,那么就是不可见的;
- 如果这个版本的事务ID不在ReadView的未提交事务数组中,表示这个版本是已经提交了的事务生成的,那么是可见的。
如果在上述的判断中发现当前版本是不可见的,那么就继续从版本链中通过回滚指针拿取下一个版本来进行上述的判断。
从上图中可明显看出:
不同的旧版本数据,会以roll_ptr
回滚指针作为链接点,然后将所有的旧版本数据组成一个单向链表
。
请注意:最新的旧版本数据,都会插入到链表头中
,而不是追加到链表尾部。
为什么Undo-log
日志要设计出版本链呢?
有如下两个好处:
(1) 一方面可以实现事务点回滚
;
(2) 另一方面则可以实现MVCC
机制。
与删除标识类似,一条数据被delete
后并提交了,最终会从磁盘移除,而Undo-log
中记录的旧版本数据,同样会占用空间,因此在事务提交后也会移除,移除的工作同样由purger
线程负责,purger
线程内部也会维护一个ReadView
,它会以此作为判断依据,来决定何时移除Undo
记录。
组件3:MVCC核心ReadView
先来思考如下的问题:
如果T1
事务要查询id=1的一条行数据,此时这条行数据正在被T2
事务修改,那也就代表着这条数据可能存在多个旧版本数据,T1
事务在查询时,应该读这条数据的哪个版本呢?
此时就需要用到ReadView
,用它来做多版本的并发控制,根据查询的时机,来选择一个当前事务可见的旧版本数据读取。
什么是ReadView呢?
当一个事务在尝试读取一条数据时,
MVCC
基于当前MySQL
的运行状态生成的快照,也被称之为读视图,即ReadView
,在这个快照中记录着当前所有活跃事务的ID
(活跃事务是指还在执行的事务,即未结束(提交/回滚)的事务)。ReadView是事务在进行快照读的时候生成的记录快照, 可以帮助我们解决可见性问题的。
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的信息如下:
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 时生成该版本的事务还是活跃的,该版本不可以被访问; - 如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。
ReadView的生成规则
在MySQL中只有在RR(可重复读)和RC(读已提交)
这两个事务隔离级别下有效,生成ReadView规则是不同的:
在RR中,
ReadView
会在事务中第一次SELECT
语句执行时生成,只有在本事务中对数据进行更改才会更新快照。在RC中,每次SELECT都会重新生成一个
ReadView
,总是读取最新版本数据。读已提交和可重复读唯一的区别在于:
1.在RC隔离级别下,是每个select都会创建最新的ReadView;
2.而在RR隔离级别下,则是当事务中的第一个select请求才创建ReadView。
MVCC实现原理
一行数据记录对事务是否可见,取决于数据记录的trx_id与这几个信息的比对结果,有以下几种可能:
1、如果小于min_trx_id,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的;
2、如果大于max_trx_id,表示这个版本是由将来启动的事务生成的,是肯定不可见的;
3、如果在min_trx_id 和 max_trx_id之间,那就包括两种情况
(1)若 row trx_id在数组中,表示这个版本是由还没提交的事务生成的,不可见;
(2)若 row trx_id不在数组中,表示这个版本是已经提交了的事务生成的,可见。
如下图所示:
经过前面的分析后已得知:
当一个事务尝试改动某条数据时,会将原本表中的旧数据放入
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实现原理 总结
MVCC
多版本并发控制,其中的多版本主要依赖Undo-log
日志来实现,而并发控制则通过表的隐藏字段
+ReadView
快照来实现,通过Undo-log
日志、隐藏字段
、ReadView
快照这3点,就实现了MVCC
机制。
注意:RC/RR 适用MVCC
MySQL
中仅在RC
读已提交级别、RR
可重复读级别才会使用MVCC
机制。
1:RU读未提交级别,不适用MVCC。
既然都允许存在脏读问题、允许一个事务读取另一个事务未提交的数据,直接进行当前读,那自然可以直接读最新版本的数据,因此无需MVCC
介入。
2:Serializable串行化级别不存在事务并发,不适用MVCC。
如果是Serializable串行化级别,因为会将所有的并发事务串行化处理,
Serializable串行化级别,不论事务是读操作,亦或是写操作,都会被排好队一个个执行,这都不存在所谓的多线程并发问题了,自然也无需MVCC介入。
MVCC 演示过程
下面通过一个示例来具体演示MVCC的执行过程(假设是在可重复读事务级别下),当前account表中已经有了一条初始数据(id=1,name=monkey):
Transaction 100 | Transaction 200 | Transaction 300 | 无事务ID | 无事务ID | |
---|---|---|---|---|---|
1 | begin; | begin; | begin; | begin; | begin; |
2 | UPDATE test SET a='1' WHERE id = 1; | ||||
3 | UPDATE test SET a='2' WHERE id = 2; | ||||
4 | UPDATE account SET name = 'monkey301' WHERE id = 1; | ||||
5 | commit; | ||||
6 | SELECT name FROM account WHERE id = 1; | ||||
7 | UPDATE account SET name = 'monkey101' WHERE id = 1; | ||||
8 | UPDATE account SET name = 'monkey102' WHERE id = 1; | ||||
9 | SELECT name FROM account WHERE id = 1; | ||||
10 | commit; | UPDATE account SET name = 'monkey201' WHERE id = 1; | |||
11 | UPDATE account SET name = 'monkey202' WHERE id = 1; | ||||
12 | SELECT name FROM account WHERE id = 1; | SELECT name FROM account WHERE id = 1; | |||
13 | commit; |
从左往右分别是五个事务,从上到下是时刻点。
其中在第2和3时刻点中事务100和事务200(这里两个事务之间相差100只是为了更加方便去看,正常来说下个事务的ID是以+1的方式来创建的)分别执行了一条UPDATE语句,这两条语句并无实际作用,只是为了生成事务ID的,所以在下面的MVCC执行过程中就不分析这两条语句所带来的影响了,我们只研究account表。
而其中最后两个事务, 是注明没有事务ID的。为啥呢?
因为事务ID并不是开启事务的时候就会生成,而是要执行一条更新操作(增删改)的语句后才会生成(这也是事务100和事务200要先执行一条更新语句的意义)。
最后两个事务中可以看到就是执行了一些SELECT语句而已,所以它们并没有事务ID。
首先来看一下初始状态时的版本链和ReadView(ReadView此时还未生成):
其中事务1在account表中创建了一条初始数据。之后 咱们开始来看:
在第1时刻点,五个事务分别开启了事务(如上所说,这个时候还没有生成事务ID)。
在第2时刻点,第一个事务执行了一条UPDATE语句,生成了事务ID为100。
在第3时刻点,第二个事务执行了一条UPDATE语句,生成了事务ID为200。
在第4时刻点,第三个事务执行了一条UPDATE语句,将account表中id为1的name改为了monkey301。同时生成了事务ID为300。
在第5时刻点,事务300也就是上面的事务执行了commit操作。
UPDATE account SET name = 'monkey301' WHERE id = 1;
commit;
在第6时刻点,第四个事务执行了一条SELECT语句,想要查询一下当前id为1的数据(如上所说,该事务没有生成事务ID)。此时的版本链和ReadView如下:
因为在第5时刻点,事务300已经commit了,所以ReadView的未提交事务数组中不包含它。
此时根据上面所说的比对规则,拿版本链中的第一个版本的事务ID为300进行比对,首先当前这条SELECT语句没有在事务300中进行查询,然后发现是落在黄色区间,而且事务300也没有在ReadView的未提交事务数组中,所以是可见的。
即此时在第6时刻点,第四个事务所查找到的结果是monkey301。
在第7时刻点,事务100执行了一条UPDATE语句,将account表中id为1的name改为了monkey101。
在第8时刻点,事务100又执行了一条UPDATE语句,将account表中id为1的name改为了monkey102。
在第9时刻点,第四个事务执行了一条SELECT语句,想要查询一下当前id为1的数据。此时的版本链和ReadView如下:
注意,因为当前是在可重复读的事务级别下,所以此时的ReadView沿用了在第6时刻点生成的ReadView(如果是在读取已提交的事务级别下,此时就会重新生成一份ReadView了)。
然后根据上面所说的比对规则,拿版本链中的第一个版本的事务ID为100进行比对,首先当前这条SELECT语句没有在事务100中进行查询,然后发现是落在黄色区间,而且事务100是在ReadView的未提交事务数组中,所以是不可见的。
此时通过回滚指针拿取下一个版本,发现事务ID仍然为100,经过分析后还是不可见的。
此时又拿取下一个版本:事务ID为300进行比对,首先当前这条SELECT语句没有在事务300中进行查询,然后发现是落在黄色区间,但是事务300没有在ReadView的未提交事务数组中,所以是可见的。
即此时在第9时刻点,第四个事务所查找到的结果仍然是monkey301(这也就是可重复读的含义)。
在第10时刻点,事务100commit提交事务了。同时事务200执行了一条UPDATE语句,将account表中id为1的name改为了monkey201。
在第11时刻点,事务200又执行了一条UPDATE语句,将account表中id为1的name改为了monkey202。
在第12时刻点,第四个事务执行了一条SELECT语句,想要查询一下当前id为1的数据。此时的版本链和ReadView如下:
跟第9时刻点一样,在可重复读的事务级别下,ReadView沿用了在第6时刻点生成的ReadView。
然后根据上面所说的比对规则,拿版本链中的第一个版本的事务ID为200进行比对,首先当前这条SELECT语句没有在事务200中进行查询,然后发现是落在黄色区间,而且事务200是在ReadView的未提交事务数组中,所以是不可见的。
此时通过回滚指针拿取下一个版本,发现事务ID仍然为200,经过分析后还是不可见的。
此时又拿取下一个版本:事务ID为100进行比对,首先当前这条SELECT语句没有在事务100中进行查询,然后发现是落在黄色区间内,同时在ReadView的未提交数组中,所以依然是不可见的。
此时又拿取下一个版本,发现事务ID仍然为100,经过分析后还是不可见的。
此时再拿取下一个版本:事务ID为300进行比对,首先当前这条SELECT语句没有在事务300中进行查询,然后发现是落在黄色区间,但是事务300没有在ReadView的未提交事务数组中,所以是可见的。
即此时在第12时刻点,第四个事务所查找到的结果仍然是monkey301。
同时在第12时刻点,第五个事务执行了一条SELECT语句,想要查询一下当前id为1的数据。此时的版本链和ReadView如下:
注意,此时第五个事务因为是该事务内的第一条SELECT语句,所以会重新生成在当前情况下的ReadView,即上图中所示的内容。
可以看到,和第四个事务生成的ReadView并不一样,因为在之前的第10时刻点,事务100已经提交事务了。
然后根据上面所说的比对规则,拿版本链中的第一个版本的事务ID为200进行比对,首先当前这条SELECT语句没有在事务200中进行查询,然后发现是落在黄色区间,而且事务200是在ReadView的未提交事务数组中,所以是不可见的。
此时通过回滚指针拿取下一个版本,发现事务ID仍然为200,经过分析后还是不可见的。
此时又拿取下一个版本:事务ID为100进行比对,发现是在绿色区间,所以是可见的。
即此时在第12时刻点,第五个事务所查找到的结果是monkey102(可以看到,即使是同一条SELECT语句,在不同的事务中,查询出来的结果也可能是不同的,究其原因就是因为ReadView的不同)。
在第13时刻点,事务200执行了commit操作,整段分析过程结束。
以上演示的就是MVCC的具体执行过程,在多个事务下,版本链和ReadView是如何配合进行查找的。
上面还遗漏了一种情况没有进行说明,就是如果是DELETE语句的话,也会在版本链上将最新的数据插入一份,然后将事务ID赋值为当前进行删除操作的事务ID。
但是同时会在该条记录的信息头(record header)里面的deleted_flag标记位置为true,以此来表示当前记录已经被删除。所以如果经过版本比对后发现找到的版本上的deleted_flag标记位为true的话,那么也不会返回,而是继续寻找下一个。
另外,如果当前事务执行rollback回滚的话,会把版本链中属于该事务的所有版本都删除掉
快照读和当前读
快照读,就是读取快照数据,即快照生成的那一刻的数据。我们使常用的普通的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都会重新生成一个快照,总是读取最新版本数据。
面试官追问:MySQL 如何通过MVCC实现 读已提交 隔离级别的?
在MySQL中, READ COMMITTED (读已提交)隔离级别通过多版本并发控制(MVCC)实现,其核心机制如下:
1. MVCC的基本概念
MVCC通过为每行数据维护多个版本来实现并发控制。每个版本通过隐藏字段(如DB_TRX_ID
和DB_ROLL_PTR
)记录事务ID和版本链。这些隐藏字段帮助MySQL判断数据版本的可见性。
2. Read View的作用
在 READ COMMITTED 隔离级别下,每次执行SELECT查询时,都会生成一个新的Read View
。 Read View 记录了当前系统中活跃事务的ID列表,用于判断数据版本的可见性。
3. 数据版本的可见性
当事务执行查询时,MySQL会根据 Read View 中的信息,沿着版本链查找数据的可见版本:
- 如果数据版本的事务ID小于 Read View 中的最小活跃事务ID,说明该版本在查询事务开始之前已经提交,因此是可见的。
- 如果数据版本的事务ID在 Read View 的活跃事务列表中,说明该版本尚未提交,不可见。
5. 读已提交 隔离级别总结
通过Read View
和版本链,MySQL在 READ COMMITTED 隔离级别下确保每次查询看到的都是已提交的数据版本,同时避免了脏读问题。
MVCC 机制允许读操作和写操作并发执行,提高了系统的并发性能。
面试官再追问:MySQL 又是如何通过MVCC实现 可重复读(REPEATABLE READ) 隔离级别的?
在REPEATABLE READ(可重复读)隔离级别下,MySQL通过MVCC(多版本并发控制)机制实现了一致性读取,具体实现方式如下:
1. 一致性视图(Read View)的创建
在REPEATABLE READ隔离级别下,事务在第一次执行读操作时会生成一个一致性视图(Read View),并且在整个事务期间复用这个视图。这意味着事务在后续的查询中看到的都是事务开始时的数据版本,而不是其他事务的更改。
2. 解决脏读和不可重复读
脏读:由于事务只在第一次读取时生成Read View,并且后续查询都基于这个视图,因此不会读取到未提交的事务数据。
不可重复读:因为事务始终使用第一次生成的Read View,即使其他事务修改了数据,当前事务看到的仍然是事务开始时的数据版本。
3. 快照读与当前读
快照读:普通SELECT语句属于快照读,通过Read View从Undo Log中找到事务开始时的数据版本。
当前读:SELECT ... FOR UPDATE、INSERT、UPDATE、DELETE等操作属于当前读,会读取最新的数据版本,并且会对数据加锁。
5. 如何避免幻读
在REPEATABLE READ隔离级别下,MySQL通过以下方式避免幻读:
快照读:通过MVCC机制,事务在整个生命周期内看到的都是第一次生成的快照,即使其他事务插入了新记录,也不会出现在当前事务的快照中。
当前读:对于SELECT ... FOR UPDATE等操作,MySQL会使用Next-Key Lock(记录锁+间隙锁)来锁定数据范围,防止其他事务插入新记录。
问题 总结
在REPEATABLE READ隔离级别下,MySQL通过以下机制实现一致性读取:
- 在事务第一次读取时生成一个Read View,并在整个事务期间复用该视图。
- 使用MVCC机制,通过Undo Log版本链找到事务开始时的数据版本。
- 对于范围查询,通过Next-Key Lock锁定记录和间隙,防止幻读。
这种机制, 确保了事务在执行过程中看到的数据版本始终一致,从而解决了脏读、不可重复读和幻读问题。
面试官再追问:READ COMMITTED和REPEATABLE READ的区别是什么?
READ COMMITTED(读已提交)和REPEATABLE READ(可重复读)是MySQL中两种常见的事务隔离级别,它们在实现机制和行为上有显著区别。
1. 核心区别:Read View的创建时机
这是READ COMMITTED和REPEATABLE READ隔离级别最根本的区别。
READ COMMITED创建Read View的时机:
- 每次查询创建新的Read View:在READ COMMITED隔离级别下,每次执行读操作(如SELECT)时,都会生成一个新的Read View。
- 每次查询看到最新的已提交版本:这意味着每次查询的结果可能不同,因为新的Read View会反映最新的已提交数据。
REPEATABLE READ 创建Read View的时机:
- 事务开始时创建一次Read View:在REPEATABLE READ隔离级别下,事务在第一次执行读操作时创建一个Read View,并在整个事务生命周期内复用这个Read View。
- 整个事务期间看到的数据版本一致:无论何时执行查询,事务看到的都是事务开始时的数据版本,保证了可重复读。
2. 数据一致性表现
READ COMMITED:
不保证可重复读:由于每次查询都创建新的Read View,事务可能看到不同的数据版本。例如,第一次查询后,其他事务提交了更新,第二次查询会看到新的已提交版本。
避免脏读:虽然每次查询看到的是最新的已提交版本,但不会看到未提交的数据,因此避免了脏读。
REPEATABLE READ:
保证可重复读:事务在第一次创建Read View后,后续查询始终看到相同的数据版本,即使其他事务提交了更新。
避免不可重复读和幻读:通过固定Read View和使用Next-Key Lock(记录锁+间隙锁),REPEATABLE READ不仅避免了不可重复读,还解决了幻读问题。
3. 幻读问题
READ COMMITED:
可能出现幻读:由于每次查询都基于最新的已提交版本,如果其他事务插入了新记录,当前事务可能会看到“幻影”数据。
不使用间隙锁:READ COMMITED隔离级别下,MySQL不会使用间隙锁来防止幻读,因此幻读是可能发生的。
REPEATABLE READ:
- 避免幻读:通过Next-Key Lock(记录锁+间隙锁),REPEATABLE READ锁定了记录所在的范围,防止其他事务插入新记录,从而避免幻读。
4. 性能和并发性
READ COMMITED:
- 更高的并发性:由于每次查询都基于最新的已提交版本,READ COMMITED隔离级别下的并发性更高,适合对实时性要求较高的场景。
较少的锁开销:不使用间隙锁,减少了锁的开销。
REPEATABLE READ:
较低的并发性:为了保证可重复读和避免幻读,REPEATABLE READ隔离级别下使用了更多的锁(如Next-Key Lock),可能会降低并发性。
更严格的隔离:适合对数据一致性要求较高的场景,例如统计报表或复杂的事务操作。
5. 答案总结
READ COMMITED:每次查询创建新的Read View,看到最新的已提交版本,不保证可重复读,可能出现幻读,适合高并发场景。
REPEATABLE READ:事务开始时创建一次Read View,保证可重复读,避免幻读,适合对数据一致性要求较高的场景。
面试官再追问:READ COMMITTED的缺点是什么?
READ COMMITTED
隔离级别的缺点主要体现在 数据一致性较弱
具体来说: READ COMMITTED 意味着在同一事务中,多次读取同一数据, 可能会得到不同的结果。
1. 数据一致性较弱
不可重复读 问题:在READ COMMITTED
隔离级别下,事务每次读取数据时都会生成一个新的Read View
,因此可能会看到不同的数据版本。这意味着在同一事务中,多次读取同一数据可能会得到不同的结果。
幻读问题:由于READ COMMITTED
不使用间隙锁来防止新数据的插入,因此可能会出现幻读现象,即事务在读取数据时发现其他事务插入的新数据。
2. 对业务逻辑的潜在影响
业务逻辑复杂性:某些业务场景(如财务系统、转账系统)要求事务内的数据保持一致性,而READ COMMITTED
无法保证这一点,可能会导致业务逻辑的复杂性增加。
实时性与一致性的权衡:虽然READ COMMITTED
能够提供较高的并发性能和实时性,但这种实时性是以牺牲数据一致性为代价的。
5. 适用场景的局限性
不适合对一致性要求高的场景:READ COMMITTED
更适合读多写少的场景,而对于对数据一致性要求较高的场景(如金融交易系统),REPEATABLE READ
或更高的隔离级别可能更为合适。
总的来说,READ COMMITTED
隔离级别在提供高并发性能的同时,牺牲了一定的数据一致性,因此需要根据具体业务需求权衡使用。
面试官再追问:MySQL默认的隔离级别为什么是REPEATABLE READ?REPEATABLE READ 的缺点是什么
MySQL默认使用REPEATABLE READ
隔离级别,因为它提供了更强的数据一致性保证,适合大多数需要严格事务控制的场景。
1. MySQL默认隔离级别为 REPEATABLE READ 的原因
MySQL默认选择REPEATABLE READ
作为事务隔离级别,主要有以下原因:
(1) 数据强一致性保障:REPEATABLE READ
保证了在同一个事务中,多次读取相同数据时结果一致,有效避免了不可重复读的问题。
(2) 性能与隔离性的平衡:相比 SERIALIZABLE , REPEATABLE READ 提供了更好的并发性能,同时满足了大多数应用对数据一致性的需求。
(3) 实现相对简单: REPEATABLE READ 通过锁定查询的数据行,而不是锁表,实现起来相对简单。
(4) 适用于广泛场景:对于大多数业务场景,REPEATABLE READ
提供了足够的隔离级别,既能保证数据一致性,又不会过度影响性能。
2. REPEATABLE READ
的缺点
尽管REPEATABLE READ
提供了较好的数据一致性和性能平衡,但它也有一些缺点:
(1) 幻读问题:REPEATABLE READ
通过版本控制无法完全解决幻读问题,通过结合使用 邻键锁 Next-Key Lock(记录锁+间隙锁)。幻读是指在同一个事务中,由于其他事务插入或删除数据,导致查询结果前后不一致。
(2) 并发性能受限:由于REPEATABLE READ
需要维护长生命周期的快照和undo日志,可能导致内存开销较大,尤其是在长事务中。
(3) 锁争用增加:在锁定读操作中,REPEATABLE READ
使用 邻键锁 Next-Key Lock(记录锁+间隙锁)来防止幻读,这可能引起更多的锁争用,从而降低并发性能。
(4) 快照过期或undo日志过大:在长事务中,快照可能过期,或者undo日志不断增长,对系统性能和资源造成影响。
3 答案总结
MySQL选择REPEATABLE READ
作为默认隔离级别,是因为保障了数据的强一致性,而且性能尚可,适用于大多数业务场景。
面试官再追问:大厂的MySQL,为啥一般调整为的READ COMMITTED 隔离级别?
大厂通常将MySQL的隔离级别调整为READ COMMITTED
,而不是MySQL默认的REPEATABLE READ
,主要有以下原因:
1. 提升并发性能
- 在
READ COMMITTED
隔离级别下,读操作不会加锁,且写锁仅在事务修改数据时存在,提交后立即释放。这使得读操作几乎不会阻塞写操作,大大提高了系统的并发性能。 - 相比之下,
REPEATABLE READ
隔离级别为了解决幻读,会使用邻键锁 Next-Key Lock(记录锁+间隙锁),锁的粒度更大,可能导致锁竞争和死锁。
2. 减少死锁
REPEATABLE READ
隔离级别下,事务可能会锁定未被查询到的行或间隙,增加了死锁的可能性。- 而
READ COMMITTED
隔离级别下,仅对实际修改的行加锁,减少了锁的范围和持有时间,从而降低了死锁的概率。
3. 适应高并发场景
- 互联网业务 性能 要求 更高:互联网业务通常对实时性和并发性要求较高,而对数据一致性的容忍度相对较高。
READ COMMITTED
隔离级别在这些场景下表现更优,因为它允许事务读取最新的已提交数据。 - 互联网业务 允许弱一致性: 在
READ COMMITTED
隔离级别下,即使出现不可重复读,也不会对互联网业务 逻辑产生重大影响。
4. 兼容性和性能优化
READ COMMITTED
隔离级别下, 减少更新操作的冲突和锁等待。- READ COMMITTED`隔离级别下,不需要使用间隙锁,进一步减少了锁的开销。
5. 互联网业务 一般允许幻读
- 在
READ COMMITTED
隔离级别下,虽然可能出现幻读,但在大多数互联网业务场景中,幻读的影响是可以接受的。如果业务对幻读非常敏感,可以通过其他机制(如应用层逻辑)来解决。
6 答案总结
大厂选择READ COMMITTED
隔离级别,主要是为了在高并发场景下提升性能、减少死锁,并适应业务对实时性的需求。这种调整虽然牺牲了一定的数据一致性(如不可重复读和幻读),但在实际业务中通常是可以接受的。
面试官再追问:可重复读 什么时候使用了邻键锁 / 间隙锁(Gap Lock)和Next-Key Lock?
可重复读(Repeatable Read)隔离级别下间隙锁(Gap Lock)和 Next - Key Lock 的使用时机
1. 范围查询时 使用邻键锁 :
当在可重复读隔离级别下, 执行带有范围条件的查询语句时: 如
SELECT... FROM table WHERE column BETWEEN value1 AND value2 FOR UPDATE
或
SELECT... FROM table WHERE column > value1 AND column < value2 FOR UPDATE
会使用间隙锁和 Next - Key Lock。
例如,在一个按年龄字段索引的用户表中,执行
SELECT * FROM users WHERE age BETWEEN 20 AND 30 FOR UPDATE,
如果表中存在年龄为 20、25、30 的用户记录,那么会锁定年龄在 (20, 25]、(25, 30] 以及 20 和 30 对应的记录行,防止其他事务在这个年龄范围内插入新记录,避免幻读问题。
2. 插入操作引发时 使用邻键锁 :
如果在一个事务中,先进行范围查询,然后准备插入一条新记录,而这条新记录的值处于之前查询的范围区间内,也会触发间隙锁和 Next - Key Lock。
假设事务 A 先执行SELECT * FROM products WHERE price > 100 AND price < 200 FOR UPDATE,之后事务 A 准备插入一条price为 150 的新商品记录,此时会锁定 (100, 200) 这个区间,防止其他事务在该区间插入数据,确保事务 A 再次查询该范围时,数据的一致性。
3. 唯一索引冲突检测时 使用邻键锁 :
当插入或更新数据时,如果涉及到唯一索引,并且在可重复读隔离级别下,为了防止其他事务在同一时刻插入相同唯一值的数据,会使用 Next - Key Lock。
例如,在一个用户表中,email字段设置为唯一索引,当一个事务执行下面的sql ,
INSERT INTO users (name, email) VALUES ('张三', '[zhangsan@example.com](mailto:zhangsan@example.com)')
会锁定email索引上对应值的 Next - Key 范围,确保在事务完成前,其他事务不能插入相同email的记录,保证唯一索引的约束性和数据一致性。
面试官再追问:如何选择隔离级别?
选择合适的事务隔离级别需要根据具体的业务需求、性能要求以及对数据一致性的容忍度来决定。
在实际应用中,可以根据不同业务模块的需求,灵活调整隔离级别,以达到性能和一致性的最佳平衡。
1. 数据一致性要求
- 如果业务对数据一致性要求极高(如金融交易、账务系统),需要避免不可重复读和幻读,建议选择
REPEATABLE READ
或SERIALIZABLE
。 - 如果业务对实时性要求较高,且可以容忍一定程度的数据不一致(如社交媒体、日志系统),可以选择
READ COMMITTED
。 - 如果业务对数据一致性要求最低,且需要尽可能高的性能(如日志记录、监控系统),可以选择
READ UNCOMMITTED
。
2. 读写 操作要求
- 读多写少:如果系统主要是读操作,且写操作较少,
READ COMMITTED
是一个不错的选择,因为它提供了较好的并发性能。 - 写密集型:如果系统写操作较多,
READ COMMITTED
或REPEATABLE READ
都可以考虑,但需要权衡锁的开销和并发性能。 - 复杂查询和报表:对于需要多次读取同一数据集的复杂查询或报表系统,
REPEATABLE READ
可以保证数据的一致性。
3. 锁开销和 性能表现
READ COMMITTED
提供了较高的并发性能,因为它减少了锁的使用和锁的持有时间。READ COMMITTED
不使用间隙锁,减少了锁的开销。REPEATABLE READ
由于使用了间隙锁(Gap Lock)和Next-Key Lock,可能会导致较多的锁竞争,从而降低并发性能, 间隙锁和Next-Key Lock,锁的开销较大。 但REPEATABLE READ 可以避免幻读。SERIALIZABLE
提供了最高的数据一致性,但通过锁表的方式实现,性能开销最大,适合并发较低的场景。
4. 数据一致性问题
不可重复读:
- 如果业务不能容忍不可重复读(如账务系统),应选择
REPEATABLE READ
或SERIALIZABLE
。 - 如果业务可以容忍不可重复读(如新闻网站),可以选择
READ COMMITTED
。
幻读:
- 如果业务对幻读非常敏感(如报表系统),应选择
REPEATABLE READ
或SERIALIZABLE
。 - 如果幻读对业务影响不大(如日志系统),可以选择
READ COMMITTED
。
4. 默认隔离级别与调整
MySQL默认隔离级别:
- MySQL默认使用
REPEATABLE READ
,因为它在数据一致性和性能之间提供了较好的平衡,适合大多数场景。 - 如果业务对性能要求较高,可以考虑调整为
READ COMMITTED
。
调整隔离级别:
可以在会话级别或全局级别调整隔离级别:
-- 设置会话级别的隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL [隔离级别];
-- 设置全局隔离级别
SET GLOBAL TRANSACTION ISOLATION LEVEL [隔离级别];
5. 实际业务参考案例
电商系统:
- 订单处理:需要高一致性,可选择
REPEATABLE READ
。 - 商品浏览:对实时性要求高,可选择
READ COMMITTED
。
金融系统:
- 账务处理:需要严格的一致性,建议使用
REPEATABLE READ
或SERIALIZABLE
。 - 报表查询:需要可重复读,建议使用
REPEATABLE READ
。
答案总结
选择隔离级别需要综合考虑业务需求、性能要求和数据一致性问题。以下是一些通用建议:
- 如果需要高并发和较好的性能,选择
READ COMMITTED
。 - 如果需要严格的数据一致性,选择
REPEATABLE READ
。 - 如果对数据一致性要求极高且并发较低,选择
SERIALIZABLE
。 - 如果对数据一致性要求最低、性能要求极致的高,甚至可以 选择
READ UNCOMMITTED
。
尼恩架构团队的塔尖 sql 面试题
- sql查询语句的执行流程:
美团面试:Mysql 有几级缓存? 每一级缓存,具体是什么?
- 索引
阿里面试:为什么要索引?什么是MySQL索引?底层结构是什么?
滴滴面试:单表可以存200亿数据吗?单表真的只能存2000W,为什么?
- 索引下推 ?
- 索引失效
美团面试:mysql 索引失效?怎么解决?(重点知识,建议收藏,读10遍+)
- MVCC
- binlog、redolog、undo log
美团面试:binlog、redolog、undo log底层原理是啥?分别实现ACID哪个特性?(尼恩图解,史上最全)
- mysql 事务
阿里面试:MYSQL 事务ACID,底层原理是什么? 具体是如何实现的?
京东面试:RR隔离mysql如何实现?什么情况RR不能解决幻读?
- 分布式事务
分布式事务圣经:从入门到精通,架构师尼恩最新、最全详解 (50+图文4万字全面总结 )
- mysql 调优