文章目录
- 一、前言回顾
- 1.1 事务四大特性ACID
- 1.2 并发事务问题
- 1.3 事务隔离级别
- 二、MVCC
- 2.1 为什么使用MVCC
- 2.2 基本概念——当前读、快照读、MVCC
- 2.2.1 当前读
- 2.2.2 快照读
- 2.2.3 MVCC
- 2.3 隐藏字段—— TRX_ID、ROLL_PTR
- 2.4 undo log
- 2.4.1 介绍
- 2.4.2 版本链
- 2.5 Read View读视图
- 2.6 原理分析
- 2.6.1 RC隔离级别
- 2.6.2 RR隔离级别
- 三、MySQL如何解决幻读
- 3.1 快照读如何解决幻读
- 3.2 当前读如何解决幻读
- 3.2.1 记录锁 Record Lock
- 3.2.2 间隙锁 Gap Lock
- 3.2.3 Next-Key锁
- 四、总结
还记得MySQL事务四大特性、并发事务问题、事务隔离级别吗?幻读又是什么呢?如果忘记可以到这里重新温习: MySQL基础:SQL分类DDL、DML、DQL、DCL;函数、约束、多表查询、事务、并发事务四大问题、事务隔离级别——脏写、脏读、不可重复读、幻读
一、前言回顾
1.1 事务四大特性ACID
- 原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。
- 一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。
- 隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。 在事务开始和完成时,中间过程对其它事务是不可见的。
- 持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的
1.2 并发事务问题
并发事务问题:脏写、脏读、不可重复读、幻读
问题 | 描述 |
---|---|
脏写(dirty write) | 两个事务同时更新一行数据,事务A回滚把事务B的值覆盖了,实质就是两个未提交的事务互相影响 |
脏读(dirty ready) | 一个事务读到另外一个事务还没有提交的数据。 |
不可重复读(non-repeatable read) | 一个事务先后读取同一条记录,但两次读取的数据不同,称之为不可重复读。(其他事务已提交)【针对同一行记录】 |
幻读(phantom read) | 一个事务按照条件查询数据时,没有对应的数据行,但是在插入数据时,又发现这行数据已经存在,好像出现了“幻影”【针对数据行数】 |
- 脏写(dirty write):两个事务未提交的情况下,同时更新一行数据。事务A回滚,把事务B修改的值覆盖了,实质就是两个未提交的事务修改同一个值、互相影响。
- 脏读(dirty read):指的是读到了其他事务未提交的数据。事务A修改一条数据的值,还未提交,事务B就读到了A修改的值;结果A回滚了,事务B之前读的就是一个过期值,即事务读到了修改之后没有提交的值
- 不可重复读(non-repeatable read):指的是在一个事务内多次读取同一条数据 得到不一样的值。这个过程中可能其他事务会修改数据,并且修改之后事务都提交了。它和脏读不一样,脏读是指读取到了其他事务未提交的数据,而不可重复读表示读到了其他事务修改并提交后的值。
- 幻读(phantom read):一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。被其他事务插入或者删除的数据影响,一个事务内同样条件的数据记录变多或者变少了
具体细节可参考 MySQL基础:SQL分类DDL、DML、DQL、DCL;函数、约束、多表查询、事务、并发事务四大问题、事务隔离级别——脏写、脏读、不可重复读、幻读
上面四个问题都是因为业务系统会多线程并发执行,每个线程可能都会开启一个事务,每个事务都会执行增删改查操作。
然后数据库会并发执行多个事务,多个事务可能会并发地对缓存页里的同一批数据进行增删改查操作,可能就会导致脏写、脏读、不可重复读、幻读这些问题。
因此这些问题的本质,就是数据库的多事务并发问题。为了解决多事务并发问题,数据库才设计了事务隔离机制、MVCC多版本隔离机制、锁机制,用一整套机制来解决多事务并发问题。
1.3 事务隔离级别
为了解决并发事务所引发的问题,在数据库中引入了事务隔离级别,且不同级别的隔离可以规避不同严重程度的事务问题。主要有以下几种:
- 读未提交(READ UNCOMMITTED),指一个事务还没提交,它做的修改就能被其他事务看到。
- 读提交(READ COMMITTED),一个事务做的修改,只有提交之后,其他事务才能看到。
- 可重复读(REPEATABLE READ),在整个事务过程中看到的数据,自始至终都是一致的。
- 串行化(SERIALIZABLE),每个读写操作都会加锁,多个事务要访问同一条记录时,必须要进行排队,优先级低的事务必须等优先级高的事务完成以后才能进行。
从1到4,隔离级别依次变高,当然,性能也依次变差。那么这些隔离级别究竟都能防止哪些问题呢
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
Read uncommitted | √会出现 | √ | √ |
Read committed | ×不会出现 | √ | √ |
Repeatable Read(MySQL默认) | × | × | √ |
Serializable 隔离级别最高、性能最差 | × | × | × |
注:事务隔离级别 事务隔离级别越高,数据越安全,但是性能越低。一般采用数据库的默认级别。
MySQL InnoDB引擎默认的隔离级别是可重复读(RR)。
二、MVCC
2.1 为什么使用MVCC
在数据库并发场景中,只有读-读之间的操作才可以并发执行,读-写,写-读,写-写操作都要阻塞,这样就会导致 MySQL 的并发性能极差。
采用了 MVCC 机制后,只有写写之间相互阻塞,其他三种操作都可以并行,这样便提高了 MySQL 的并发性能。即 MVCC 具体解决了以下问题:
- 并发读-写时:可以做到读操作不阻塞写操作,同时写操作也不会阻塞读操作。
- 解决 脏读、幻读、不可重复读 等事务隔离问题,但不能解决上面的写-写(需要加锁)问题。
2.2 基本概念——当前读、快照读、MVCC
MVCC分为两种模式,一种是当前读(读取最新的数据),如 select … for update/lock in share mode、insert、update、delete;另一种是快照读(历史某个版本的数据,不一定是当前时刻最新的数据),如普通的select。具体介绍如下。
2.2.1 当前读
读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如:select … lock in share mode(共享锁),select … for update、update、insert、delete(排他锁)都是一种当前读。
测试:
在测试中我们可以看到,即使是在默认的RR隔离级别下,事务A中依然可以读取到事务B最新提交的内容,因为在查询语句后面加上了 lock in share mode 共享锁,此时是当前读操作。当然,当我们加排他锁的时候,也是当前读操作。
2.2.2 快照读
表示不加锁的非阻塞读,像 不加锁的普通select 都属于快照读,快照读的实现基于MVCC,它读取的是记录数据的可见版本,有可能是历史某个版本的数据,不一定是当前时刻最新的数据。
- Read Committed:每次select,都生成一个快照读。
- Repeatable Read:开启事务后第一个select语句才是快照读的地方。
- Serializable:快照读会退化为当前读(快照读在MySQL的串行隔离级别下会上升为当前读,即使是select操作也会加锁)。
测试见上图第4步,我们看到即使事务B提交了数据,事务A中也查询不到。 原因就是因为普通的select是快照读,而在当前默认的RR隔离级别下,开启事务后第一个select语句才是快照读的地方,后面执行相同的select语句都是从快照中获取数据,可能不是当前的最新数据,这样也就保证了可重复读。
2.2.3 MVCC
全称 Multi-Version Concurrency Control,多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突,即在同一时刻同一条记录在系统中可以存在多个版本。快照读为MySQL实现MVCC提供了一个非阻塞读功能。
在MySQL InnoDB中,MVCC的实现主要是为了提高数据库并发性能,它能很好地处理MySQL的读写冲突,做到尽量不加锁,大大降低系统的开销。
MVCC的具体实现,还需要依赖于数据库记录中的三个隐式字段、undo log日志、Read View。
接下来,我们再来介绍一下InnoDB引擎的表中涉及到的隐藏字段 、undolog 以及 readview,从而来介绍一下MVCC的原理。
2.3 隐藏字段—— TRX_ID、ROLL_PTR
当我们创建了上面的这张表,我们在查看表结构的时候,就可以显式的看到这四个字段。 实际上除了这四个字段以外,InnoDB还会自动的给我们添加三个隐藏字段及其含义分别是:
隐藏字段 | 含义 |
---|---|
DB_TRX_ID | 最近修改事务ID,记录插入这条记录或最后一次修改该记录的事务ID。 |
DB_ROLL_PTR | 回滚指针,指向这条记录的上一个版本,用于配合undo log,指向上一个版本。 |
DB_ROW_ID | 隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段。 |
而上述的前两个字段是肯定会添加的, 是否添加最后一个字段DB_ROW_ID,得看当前表有没有主键,如果有主键,则不会添加该隐藏字段。
2.4 undo log
2.4.1 介绍
undo 日志,又叫undo log,也称回滚日志,它是InnoDB存储引擎在insert、update、delete的时候产生的便于数据回滚的日志。在数据更新之前,MySQL就需要先把更新前的数据记录到 undo log 日志中,当事务回滚时,可以利用 undo log 来进行回滚。作用包含两个——提供回滚、MVCC(多版本并发控制)。undo log主要分为两种:
- insert undo log:当insert的时候,产生的undo log日志只在回滚时需要,在事务提交后,可被立即删除(因为这种log只是对本事务可见,其他事务不可见,所以当事务提交后,这种类型的undo log就会被系统直接删除回收,也就是该undo log占用的undo页面链表被释放)。
- update undo log:update、delete的时候,产生的undo log日志不仅在事务回滚时需要,在快照读时也需要(也就是MVCC),所以不能在事务提交后马上删除,只在提交后放入undo log的链表,等待purge线程进行最后的删除。
比如现在Tom的账户余额有100,现在有一个事务需要把Tom的账户余额更新为300,大致的流程如下图:
2.4.2 版本链
不同事务或相同事务对同一条记录进行修改,会导致该记录的undolog生成一条记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的旧记录。
然后,有四个并发事务同时在访问这张表。
2.5 Read View读视图
Read View(读视图)是 快照读 SQL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务(未提交的)id。
Read View就是事务进行快照读操作的时候产生的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统数据以及当前活跃事务的ID(就是启动了还没提交的事务)
ReadView中包含了四个核心字段:
字段 | 含义 |
---|---|
m_ids | 当前活跃的事务ID集合,活跃事务则代表是已启动但未提交的事务 |
min_trx_id | 最小活跃事务ID |
max_trx_id | 预分配事务ID,当前最大事务ID+1(因为事务ID是自增的) |
creator_trx_id | ReadView创建者的事务ID |
而在readview中就规定了版本链数据的访问规则:
trx_id 代表当前undo log版本链对应事务ID。
条件 | 是否可以访问 | 说明 |
---|---|---|
trx_id == creator_trx_id | 可以访问该版本 | 成立,说明数据是当前这个事务更改的 |
trx_id < min_trx_id | 可以访问该版本 | 成立,说明数据已经提交了 |
trx_id > max_trx_id | 不可以访问该版本 | 成立,说明该事务是在ReadView生成后才开启 |
min_trx_id <= trx_id <= max_trx_id | 如果trx_id不在m_ids中, 是可以访问该版本的 | 成立,说明数据已经提交 |
不同的隔离级别,生成ReadView的时机不同:
-
对于使用
READ UNCOMMITTED
隔离级别的事务来说,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了 -
对于使用
SERIALIZABLE
隔离级别的事务来说,InnoDB 使用加锁的方式来访问记录,不存在并发问题。 -
READ COMMITTED :在事务中每一次执行快照读时生成ReadView。
-
REPEATABLE READ:仅在事务中第一次执行快照读时生成ReadView,一直到提交前都复用该ReadView。在可重复读隔离级别下,Read View是在事务开始(begin)之后且执行第一条sql时创建,创建Read View的同时也就生成了一个新的事务id(直到commit结束),事务会依赖该 Read View保证查询结果保持不变直到该事务结束。
2.6 原理分析
MVCC的实现原理就是通过 InnoDB表的隐藏字段、UndoLog 版本链、ReadView来实现的。 而MVCC + 锁,则实现了事务的隔离性。 而一致性则是由redolog 与 undolog保证。
2.6.1 RC隔离级别
RC隔离级别下,在事务中每一次执行快照读时生成ReadView。
我们就来分析事务5中,两次快照读读取数据,是如何获取数据的?
2.6.2 RR隔离级别
RR隔离级别下,仅在事务中第一次执行快照读时生成ReadView,后续复用该ReadView。 而RR 是可重复读,在一个事务中,执行两次相同的select语句,查询到的结果是一样的。
那MySQL是如何做到可重复读的呢? 我们简单分析一下就知道了
所以呢,MVCC的实现原理就是通过 InnoDB表的隐藏字段、UndoLog 版本链、ReadView来实现的。 而MVCC + 锁,则实现了事务的隔离性。 而一致性则是由redolog 与 undolog保证。
三、MySQL如何解决幻读
在MySQL数据库内,默认的存储引擎是InnoDB,且事务的隔离级别是可重复读(Repeatable Read,RR)。通过上面分析我们知道,RR隔离级别下MVCC可以解决不可重复读的问题。
可重复读隔离级别没有解决幻读问题。隔离级别是RR(可重复读)下的MySQL怎么避免幻读问题呢——MySQL通过一种next-key lock的锁机制一定程度上避免了幻读问题,具体原理如下。
2.1章节已经介绍了当前读、快照读,现在我们来看看 当前读、快照读的情况下各自如何解决幻读问题?
3.1 快照读如何解决幻读
读取的并非最新数据,我们通过在事务开始生成一个快照,后面一直使用这个快照(事务会依赖该 Read View保证查询结果保持不变直到该事务结束,其他事务增加与删除数据,对于当前事务来说是不可见的),就能解决幻读,不需要额外的操作。
即在 RR 隔离级别下,MVCC 解决了在快照读情况下的幻读。
(而在实际场景中,我们可能需要读取实时的数据,比如在银行业务等特殊场景下,必须是需要读取到实时的数据,此时就不能快照读,只能当前读)
3.2 当前读如何解决幻读
由于每次都是读当前,会导致一直生成新的快照。当有行数据插入或则删除时并且在查询范围之内,就会造成幻读的现象。为了解决当前读情况下出现幻读的问题,MySQL InnoDB 引擎引入了next-key lock,其等同于 记录锁+间隙锁 的组合。当执行当前读时,在锁定读取到的记录的同时,也会锁定它们的间隙,防止其它事务在查询范围内插入数据。只要我不让你插入,就不会发生幻读。
next-key锁 包含两部分:
- 记录锁(行锁,Record Lock)。锁定单个行记录的锁,防止其他事务对此行进行update和delete。加在索引上
- 间隙锁(Gap Lock)。锁定索引记录间隙(不含该记录),确保索引记录间隙不变,防止其他事务在这个间隙进行insert,产生幻读。加在索引之间
3.2.1 记录锁 Record Lock
Record Lock 锁住的永远是索引,不包括记录本身,即使该表上没有任何索引,那么InnoDB引擎会创建一个隐藏的聚集主键索引,那么锁住的就是这个隐藏的聚集主键索引。
记录锁是有 S 锁(共享锁)和 X 锁(排他锁)之分的,当一个事务获取了一条记录的 S 型记录锁后,其他事务也可以继续获取该记录的 S 型记录锁,但不可以继续获取 X 型记录锁;当一个事务获取了一条记录的 X 型记录锁后,其他事务既不可以继续获取该记录的 S 型记录锁,也不可以继续获取 X 型记录锁。
3.2.2 间隙锁 Gap Lock
间隙锁,对索引前后的间隙上锁,不对索引本身上锁。确保索引记录间隙不变,防止其他事务在这个间隙进行insert,产生幻读,在RR隔离级别下都支持。前开后开区间。
既然涉及到索引,那么索引对间隙锁会产生什么影响?
- 对主键或唯一索引,如果当前读时,where 条件全部精确命中(=或in),这种场景本身就不会出现幻读,所以只会加行记录锁,也就是说间隙锁会退化为行锁(记录锁)。
- 非唯一索引列,如果 where 条件部分命中(>、<、like等)或者全未命中,则会加附近间隙锁。例如,某表数据如下,非唯一索引
2,6,9,9,11,15
。如下语句要操作非唯一索引列 9 的数据,间隙锁将会锁定的列是(6,11],该区间内无法插入数据。 - 对于没有索引的列,当前读操作时,会加全表间隙锁,生产环境要注意。
注意:间隙锁唯一目的是防止其他事务插入间隙。间隙锁可以共存,一个事务采用的间隙锁不会阻止另一个事务在同一间隙上采用间隙锁。
3.2.3 Next-Key锁
next-key lock 是索引记录上的记录锁和索引记录之前的间隙上的间隙锁的组合,包括记录本身,每个 next-key lock 是前开后闭区间(同样说明锁住的范围更大,影响并发度),也就是说间隙锁只是锁的间隙,没有锁住记录行,next-key lock 就是间隙锁基础上锁住右边界行。
默认情况下,InnoDB以 REPEATABLE READ 隔离级别运行。在这种情况下,InnoDB 使用 Next-Key Lock 锁进行搜索和索引扫描,以防止幻读的发生。
以下表为例
假设,bank_balance表中只存在余额balance>0且主键id 为4和6的记录,那么当一个事务使用select * from where balance>0 for update查询时,其他事务就无法插入 id = 5的记录,就像是事务A把(4,6)这个范围锁住了,这就是间隙锁。
如果再把id=6的记录也同时一起锁了,合起来变成一个左开右闭区间(4, 6],那么整个区间锁也叫next-key lock。
当事务A执行select * from bank_balance where balance > 0 for update这条锁定读语句后,就会把整个表所有记录锁上(因为balance字段无索引),并根据主键id和表记录形成多个next-key lock; 此时如果事务B执行insert语句将会被阻塞,直到事务A提交了 事务B才会执行。这就避免了上述所说的幻读问题。
四、总结
1)事务四大特性ACID:原子性、一致性、隔离性、持久性
2)并发事务问题:脏写、脏读、不可重复读、幻读
3)事务隔离级别:读未提交(READ UNCOMMITTED)、读提交(READ COMMITTED)、可重复读(REPEATABLE READ)、串行化(SERIALIZABLE)
4)MVCC 在可重复读(RR)隔离级别下 解决了以下问题:
- 并发读-写时:可以做到读操作不阻塞写操作,同时写操作也不会阻塞读操作。
- 解决 脏读、幻读、不可重复读 等事务隔离问题,但不能解决写-写(需要加锁)问题。
5)当前读与快照读:MVCC分为两种模式,一种是当前读(读取最新的数据),如 select … for update/lock in share mode、insert、update、delete;另一种是快照读(历史某个版本的数据,不一定是当前时刻最新的数据),不加锁的普通select 都属于快照读
6)MVCC原理:MVCC的具体实现,需要依赖于数据库记录中的三个隐式字段(TRX_ID、ROLL_PTR)、undo log日志、Read View
7)Read View读视图:Read View 是 快照读 SQL执行时MVCC提取数据的依据,记录并维护系统数据以及当前活跃事务的ID(未提交的)。Read View也规定了版本链数据的访问规则
8)不同的隔离级别,生成ReadView的时机不同:
- READ COMMITTED :在事务中每一次执行快照读(不加锁的普通select)时生成ReadView。
- REPEATABLE READ:仅在事务中第一次执行快照读时生成ReadView,一直到提交前都复用该ReadView,事务会依赖该 Read View保证查询结果保持不变直到该事务结束。
9)RR隔离级别如何解决幻读问题:快照读依靠MVCC控制,当前读通过 next-key lock 解决(MVCC 解决了快照读情况下的幻读,next-key lock 解决当前读情况下的幻读)。
10)间隙锁和行锁合称 Next-Key Lock,每个 Next-Key Lock 是前开后闭区间。当执行当前读时,在锁定读取到的记录的同时,也会锁定它们的间隙,防止其它事务在查询范围内插入数据。只要我不让你插入,就不会发生幻读。
参考 黑马程序员MySQL相关视频笔记、美团面试官:可重复读隔离级别实现原理是什么?(一文搞懂MVCC机制)、MySQL 如何解决幻读(MVCC 原理分析)