高手必备10大难题:Mysql如何实现RR级隔离时,不会幻读?
以下为《尼恩面试宝典V15》的部分升级面试题,目录如下:
聊聊:什么是数据库事务?聊聊事务的特性?
事务(Transaction)是操作数据库中某个数据项的一个程序执行单元(unit)。
事务应该具有4个属性:
原子性、一致性、隔离性、持久性。
这四个属性通常称为ACID特性。
事务的四个特征:
1、Atomic原子性
事务必须是一个原子的操作序列单元,事务中包含的各项操作在一次执行过程中,要么全部执行成功,要么全部不执行,任何一项失败,整个事务回滚,只有全部都执行成功,整个事务才算成功。
2、Consistency一致性
事务的执行不能破坏数据库数据的完整性和一致性,事务在执行之前和之后,数据库都必须处于一致性状态。
3、Isolation隔离性
在并发环境中,并发的事务是相互隔离的,一个事务的执行不能被其他事务干扰。
即不同的事务并发操纵相同的数据时,每个事务都有各自完整的数据空间,即一个事务内部的操作及使用的数据对其他并发事务是隔离的,并发执行的各个事务之间不能相互干扰。
4、Durability持久性
持久性(durability):持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中对应数据的状态变更就应该是永久性的。
即使发生系统崩溃或机器宕机,只要数据库能够重新启动,那么一定能够将其恢复到事务成功结束时的状态。
比方说:一个人买东西的时候需要记录在账本上,即使老板忘记了那也有据可查。
聊聊:MySQL的事务ACID是如何实现的?
大多数场景下,我们的应用都只需要操作单一的数据库,这种情况下的事务称之为本地事务(Local Transaction)。本地事务的ACID特性是数据库直接提供支持。
了解过MySQL事务的同学,就会知道,为了达成本地事务,MySQL做了很多的工作,比如回滚日志,重做日志,MVCC,读写锁等。
MySQL数据库的事务实现原理
以MySQL 的InnoDB (InnoDB 是 MySQL 的一个存储引擎)为例,介绍一下单一数据库的事务实现原理。
InnoDB 是通过 日志和锁 来保证的事务的 ACID特性,具体如下:
(1)通过数据库锁的机制,保障事务的隔离性;
(2)通过 Redo Log(重做日志)来,保障事务的持久性;
(3)通过 Undo Log (撤销日志)来,保障事务的原子性;
(4)通过 Undo Log (撤销日志)来,保障事务的一致性;
Undo Log 如何保障事务的原子性呢?
具体的方式为:在操作任何数据之前,首先将数据备份到一个地方(这个存储数据备份的地方称为 Undo Log),然后进行数据的修改。如果出现了错误或者用户执行了 Rollback 语句,系统可以利用 Undo Log 中的备份将数据恢复到事务开始之前的状态。
Redo Log如何保障事务的持久性呢?
具体的方式为:Redo Log 记录的是新数据的备份(和 Undo Log 相反)。在事务提交前,只要将 Redo Log 持久化即可,不需要将数据持久化。当系统崩溃时,虽然数据没有持久化,但是 Redo Log 已经持久化。系统可以根据 Redo Log 的内容,将所有数据恢复到崩溃之前的状态。
聊聊:什么是脏读、幻读、不可重复读?
在多个事务并发操作时,数据库中会出现下面三种问题:脏读,幻读,不可重复读。
脏读(Dirty Read)
事务A读到了事务B还未提交的数据:
事务A读取的数据,事务B对该数据进行修改还未提交数据之前,事务A再次读取数据会读到事务B已经修改后的数据,如果此时事务B进行回滚或再次修改该数据然后提交,事务A读到的数据就是脏数据,这个情况被称为脏读(Dirty Read)。
幻读(Phantom Read)
事务A进行范围查询时,事务B中新增了满足该范围条件的记录,当事务A再次按该条件进行范围查询,会查到在事务B中提交的新的满足条件的记录(幻行 Phantom Row)。
不可重复读(Unrepeatable Read)
事务A在读取某些数据后,再次读取该数据,发现读出的该数据已经在事务B中发生了变更或删除。
幻读和不可重复度的区别:
幻读:在同一事务中,相同条件下,两次查询出来的 记录数 不一样; 不可重复读:在同一事务中,相同条件下,两次查询出来的 数据 不一样;
总之:
(1)脏读:A事务读到了B事务未提交的值,万一B事物回滚,则A产生脏读。 (2)不可重复读:同一事务A两个查询之间,被另外一个事务B修改(update)了数据的内容,产生内容的不一致。 (3)幻读:同一事务A两个查询之间,被另外一个事务B插入或删除了(insert、delete)记录,产生结果集的不一致。
聊聊:如何实现Transaction的隔离性?Mysql事务、Oracle事务的默认隔离级别?
通过数据库的锁机制,保障事务的隔离性;
并且,在数据库操作中,为了有效管理事务的隔离性,并且,有效保证高并发场景的数据正确性,提出的事务隔离级别。
我们的数据库锁的类型和应用,和不同的隔离级别,是有紧密关系。
SQL中的4个事务隔离级别:
隔离级别 | 脏读(Dirty Read) | 不可重复读(NonRepeatable Read) | 幻读(Phantom Read) |
---|---|---|---|
读未提交(Read uncommitted) | 可能 | 可能 | 可能 |
读已提交(Read committed) | 不可能 | 可能 | 可能 |
可重复读(Repeatable read) | 不可能 | 不可能 | 可能 |
可串行化(Serializable ) | 不可能 | 不可能 | 不可能 |
(1)读未提交
如果一个事务正在处理某一数据,并对其进行了更新, 但同时尚未完成事务,或者说事务没有提交, 与此同时,允许另一个事务也能够访问该数据。例如A将变量n从0累加到10才提交事务,此时B可能读到n变量从0到10之间的所有中间值。
允许脏读。在 读未提交 隔离级别下,允许 脏读 的情况发生。
脏读指的是读到了其他事务未提交的数据,
未提交意味着这些数据可能会回滚,也就是可能最终不会存到数据库中,也就是不存在的数据。
读到了并一定最终存在的数据,这就是脏读。
脏读最大的问题就是可能会读到不存在的数据。
比如在上图中,事务B的更新数据被事务A读取,但是事务B回滚了,更新数据全部还原,也就是说事务A刚刚读到的数据并没有存在于数据库中。
(2)读已提交
只允许读到已经提交的数据。即事务A在将n从0累加到10的过程中,B无法看到n的中间值,之中只能看到10。
在 读已提交 隔离级别下,禁止了 脏读,但是 允许不可重复读的情况发生
换成简单的表达:
就是不能读到中间值,但是允许在事务中间数据被修改, 也允许在事务期间增加和删除数据
事务A在将n从0累加到10的过程中,B无法看到n的中间值,之中只能看到10。同时, 有事务C进行从10到20的累加,此时B在同一个事务内再次读时,读到的是20。
不可重复读指的是在一个事务A内多次读取一批数据,最开始读到的数据和事务结束前的任意时刻读到的同一批数据出现不一致的情况。
事务 A 多次读取同一数据,但事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。
不可重复读 一词,有点反人类,不好记忆。
不可重复读一词是从 Nonrepeatable read 翻译过来的,感觉英文的,好记忆一点。
(3)可重复读
保证在事务处理过程中,多次读取同一个数据时,其值都和事务开始时刻时是一致的。
在可重复读隔离级别下,禁止了:脏读、不可重复读。但是,允许幻读。
换成简单的表达:
就是不能读到中间值,不允许在事务中间数据被修改, 但是,允许在事务期间增加和删除数据
在可重复读中,该sql第一次读取到数据后,就将这些数据加锁(悲观锁, 如行锁),其它事务无法修改这些数据,就可以实现可重复读了。
但这种方法却无法锁住insert的数据,所以当事务A先前读取了数据,或者修改了全部数据,事务B还是可以insert数据提交,
这时事务A就会发现莫名其妙多了一条之前没有的数据,这就是幻读,不能通过行锁来避免。
(4)串行化
最严格的事务,要求所有事务被串行执行,不能并发执行。
如果不对事务进行并发控制,我们看看数据库并发操作是会有那些异常情形
(1)一类丢失更新:两个事物读同一数据,一个修改字段1,一个修改字段2,后提交的恢复了先提交修改的字段。 (2)二类丢失更新:两个事物读同一数据,都修改同一字段,后提交的覆盖了先提交的修改。 (3)脏读:读到了未提交的值,万一该事物回滚,则产生脏读。 (4)不可重复读:两个查询之间,被另外一个事务修改(update)了数据的内容,产生内容的不一致。 (5)幻读:两个查询之间,被另外一个事务插入或删除了(insert、delete)记录,产生结果集的不一致。
数据一致性和高性能,是天生的矛盾
无论是尼恩的 葵花宝典视频、还是rocketmq视频、还是推送中台实操视频,无一例外,都揭示了一个硬道理:数据一致性和高性能,是天生的矛盾
在事务领域,也是如此:
场景一:性能最好的,一致性最差
读未提交 的级别,它是性能最好,也可以说它是最野蛮的方式,因为它压根儿就不加锁,所以根本谈不上什么隔离效果,可以理解为没有隔离。
场景二:一致性最好的,性能最差
串行化就一致性性最强。串行化相当于 处理一个人请求的时候,别的人都等着。
读的时候加共享锁,也就是其他事务可以并发读,但是不能写。写的时候加排它锁,其他事务不能并发写也不能并发读。
需要Serializable隔离级别 ,读用读锁,写用写锁,读锁和写锁互斥,这都是非常悲观的 悲观锁策略, 这么做可以有效的避免幻读、不可重复读、脏读等问题,但会极大的降低数据库的并发能力。
然后,就是
场景三:在 高性能和数据一致性中间,寻找平衡。
数据库的事务隔离越严格,并发副作用越小,但付出的代价越大;
读已提交和可重复读, 都是在寻找平衡
这两种隔离级别是比较复杂的,既要允许一定的并发,又想要解决数据一致性问题。
oracle默认事务隔离级别为读已提交(RC), 说明可以 不可重复读,(不可重复读 这个词用的反人类,)
换成简单的表达:
就是不能读到中间值,但是允许在事务中间数据被修改, 也允许在事务期间增加和删除数据
MySQL默认事务隔离级别为可重复读(RR),
换成简单的表达:
就是不能读到中间值,不允许在事务中间数据被修改, 但是,允许在事务期间增加和删除数据
ORACLE、MySQL、PostgreSQL等成熟的数据库中的 读已提交、可重复读 隔离级别,并没有使用Serializable隔离级别中的悲观锁,都是使用了以乐观锁为理论基础的MVCC(多版本并发控制)来实现。
但是,别着急,MySQL用自己的方式,解决了 事务期间增加和删除数据 的问题,也就是 解决了幻读问题。
聊聊:Mysql如何的控制事务的隔离级别?
MySQL事务隔离级别:https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html
通过修改MySQL系统参数来控制事务的隔离级别,
在MySQL8中该参数为 transaction_isolation ,在MySQL5中该参数为 tx_isolation :
MySQL8:
-- 查看系统隔离级别:
SELECT @@global.transaction_isolation;
-- 查看当前会话隔离级别
SELECT @@transaction_isolation;
-- 设置当前会话事务隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- 设置全局事务隔离级别
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
事务的四个隔离级别:
未提交读(READ UNCOMMITTED):所有事务都可以看到其他事务未提交的修改。一般很少使用; 读已提交(READ COMMITTED):Oracle默认隔离级别,事务之间只能看到彼此已提交的变更修改; 可重复读(REPEATABLE READ):MySQL默认隔离级别,同一事务中的多次查询会看到相同的数据行;可以解决不可重复读,但可能出现幻读; 可串行化(SERIALIZABLE):最高的隔离级别,事务串行的执行,前一个事务执行完,后面的事务会执行。读取每条数据都会加锁,会导致大量的超时和锁争用问题;
提交读(READ COMMITTED)隔离级别:
采用行锁(Record Lock), 不会出现脏读,但是会出现可重复读、"幻读"问题.
聊聊:Mysql如何实现 RR(REPEATABLE READ) 级别不产生幻读?
什么是幻读?就是 A事务两个查询之间,被另外一个事务B插入或删除了(insert、delete)记录,产生结果集的不一致。
MySQL Innodb存储引擎中,通过 间隙锁防止幻读的产生
MySQL默认的隔离级别是Repeatable Read,同时知道这种隔离级别是有幻读产生的,但是真的会出现幻读吗?
答案:在RR的隔离级别下,Innodb使用MVCC和 next-key locks(行锁和间隙锁的组合)解决幻读,
MVCC解决的是普通读(快照读)的幻读,next-key locks解决的是当前读情况下的幻读。
间隙锁 Gap Lock在InnoDB的唯一作用就是防止其他事务的插入操作,以此防止幻读的发生。
Innodb自动使用间隙锁的条件:
(1)必须在Repeatable Read级别下 (2)检索条件必须有索引(没有索引的话,mysql会全表扫描,那样会锁定整张表所有的记录,包括不存在的记录,此时其他事务不能修改不能删除不能添加)
间隙锁:
间隙锁在InnoDB的唯一作用就是防止其它事务的插入操作,以此来达到防止幻读的发生,
所以间隙锁不分什么共享锁与排它锁。
默认情况下,InnoDB工作在Repeatable Read隔离级别下,并且以Next-Key Lock的方式对数据行进行加锁,这样可以有效防止幻读的发生。
Next-Key Lock是行锁与间隙锁的组合:
当对数据进行条件,范围检索时,对其范围内也许并存在的值进行加锁!
当查询的索引含有唯一属性(唯一索引,主键索引)时,Innodb存储引擎会对next-key lock进行优化,将其降为record lock,即仅锁住索引本身,而不是范围!
若是普通辅助索引,则会使用传统的next-key lock进行范围锁定!
要禁止间隙锁的话,可以把隔离级别降为Read Committed,或者开启参数innodb_locks_unsafe_for_binlog。
快照读和当前读
在MVCC并发控制中,读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)。
快照读,读取的是记录的可见版本 (有可能是历史版本),不用加锁。 当前读,读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。
在一个支持MVCC并发控制的系统中,哪些读操作是快照读?哪些操作又是当前读呢?
以MySQL InnoDB为例:
快照读:
简单的select操作,属于快照读,不加锁。(当然,也有例外,下面会分析)
select * from table where ?;
当前读:
特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。
select * from table where ? lock in share mode;
select * from table where ? for update;
insert into table values (…);
update table set ? where ?;
delete from table where ?;
聊聊:什么是间隙锁?
MySQL InnoDB支持三种行锁定方式:
记录锁(Record Lock): 锁直接加在索引记录上面,锁住的是key。
间隙锁(Gap Lock): 锁定索引记录间隙,确保索引记录的间隙不变。
间隙锁是针对事务隔离级别为可重复读或以上级别而已的。
临键锁 Next-Key Lock :行锁和间隙锁组合起来就叫Next-Key Lock。
记录锁(Record Locks)
记录锁其实很好理解,对表中的记录加锁,叫做记录锁,简称行锁。比如
SELECT * FROM `test` WHERE `id`=1 FOR UPDATE;
它会在 id=1 的记录上加上记录锁,以阻止其他事务插入,更新,删除 id=1 这一行。
需要注意的是:
id 列必须为唯一索引列或主键列,否则上述语句加的锁就会变成临键锁(有关临键锁下面会讲)。 同时查询语句必须为精准匹配(=),不能为 >、<、like等,否则也会退化成临键锁。
其他实现
在通过 主键索引 与 唯一索引 对数据行进行 UPDATE 操作时,也会对该行数据加记录锁:
-- id 列为主键列或唯一索引列
UPDATE SET age = 50 WHERE id = 1;
记录锁是锁住记录,锁住索引记录,而不是真正的数据记录.
如果要锁的列没有索引,进行全表记录加锁
记录锁也是排它(X)锁,所以会阻塞其他事务对其插入、更新、删除。
间隙锁(Gap Locks)
间隙锁 是 Innodb 在 RR(可重复读) 隔离级别 下为了解决 幻读问题 时引入的锁机制。
间隙锁是innodb中行锁的一种。
请务必牢记:使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据。
举例来说,假如emp表中只有101条记录,其empid的值分别是1,2,...,100,101,
下面的SQL:
SELECT * FROM emp WHERE empid > 100 FOR UPDATE
当我们用条件检索数据,并请求共享或排他锁时,InnoDB不仅会对符合条件的empid值为101的记录加锁,也会对empid大于101(这些记录并不存在)的“间隙”加锁。
这个时候如果你插入empid等于102的数据的,如果那边事物还没有提交,那你就会处于等待状态,无法插入数据。
临键锁(Next-Key Locks)
Next-key锁是记录锁和间隙锁的组合,它指的是加在某条记录以及这条记录前面间隙上的锁。
也可以理解为一种特殊的间隙锁。
通过临建锁可以解决 幻读 的问题。
每个数据行上的非唯一索引列上都会存在一把临键锁,当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据。
需要强调的一点是,InnoDB 中行级锁是基于索引实现的。
临键锁只与 非唯一索引列 有关,在 唯一索引列(包括主键列)上不存在临键锁。
比如一个age 索引包含值,10,11,13和20。那么,间隙锁的范围如下
(negative infinity, 10]
(10, 11]
(11, 13]
(13, 20]
(20, positive infinity)
Next-Key Locks的范围是左开右闭。默认右边的记录也会加锁。
在事务 A 中执行如下命令:
-- 根据非唯一索引列 UPDATE 某条记录
UPDATE table SET name = Vladimir WHERE age = 11;
-- 或根据非唯一索引列 锁住某条记录
SELECT * FROM table WHERE age = 11 FOR UPDATE;
不管执行了上述 SQL 中的哪一句,之后如果在事务 B 中执行以下命令,则该命令会被阻塞:
INSERT INTO table VALUES(100, 12, 'tianqi');
很明显,事务 A 在对 age 为 11的列进行 UPDATE 操作的同时,也获取了 (11, 13] 这个区间内的临键锁。
行级锁实际上是索引记录锁。
索引记录上的临键锁(Next-Key Locks)也会影响该索引记录之前的“间隔”。
也就是说,临键锁(Next-Key Locks)是索引标准记录锁(Record Locks)加上位于索引记录右边的间隙上的间隙锁(Gap Locks)。
InnoDB的默认加锁方式是next-key 锁。
默认情况下,InnoDB工作在可重复读(Repeatable Read)隔离级别下,并且会以Next-Key Lock的方式对数据行进行加锁,这样可以有效防止幻读的发生。
Next-Key Lock是行锁和间隙锁的组合,当InnoDB扫描索引记录的时候,会首先对索引记录加上行锁(Record Lock),再对索引记录两边的间隙加上间隙锁(Gap Lock)。
加上间隙锁之后,其他事务就不能在这个间隙修改或者插入记录。
在可重复读(Repeatable Read)隔离级别下, Gap Lock在InnoDB的唯一作用就是防止其他事务的插入操作,以此防止幻读的发生。
Mysql的间隙锁工作在Repeatable Read隔离级别下面,可以防止幻读,
Innodb 自动使用间隙锁的条件:(1)必须在Repeatable Read级别下 (2)检索条件必须有索引(没有索引的话,mysql会全表扫描,那样会锁定整张表所有的记录,包括不存在的记录,此时其他事务不能修改、不能删除、不能添加)
聊聊:什么是MVCC多版本并发控制协议?
MVCC的设计思想:copyonwrite思想
在并发读写数据库时,读操作可能会不一致的数据(脏读)。
为了避免这种情况,需要实现数据库的并发访问控制,最简单的方式就是加锁访问。
由于,加锁访问,不光阻塞了写,也阻塞了读,会将读写操作串行化,当然,不会出现不一致的状态。
但是,读操作会被写操作阻塞,大幅降低读性能。
copyonwrite思想
在java concurrent包中,有copyonwrite系列的类,专门用于读多写少场景。
copyonwrite的思想是:
在进行写操作时,将数据copy一份,不会影响原有数据,然后进行修改,修改完成后原子替换掉旧的数据,而读操作只会读取原有数据。
copyonwrite的优势是:
通过这种方式实现写操作不会阻塞读操作,从而优化读效率。
copyonwrite的缺点是:
写操作之间是要互斥的,并且每次写操作都会有一次copy,所以,copyonwrite,更多只适合读操作远多于写操作场景。
MVCC的原理
MVCC的原理与copyonwrite类似,全称是Multi-Version Concurrent Control,即多版本并发控制。
在MVCC协议下,每个读操作, 会看到一个一致性的snapshot 快照,并且可以实现非阻塞的读。
读这个snapshot 快照,不用加锁。
除了snapshot 快照版本之外, MVCC允许数据具有多个版本,版本编号可以是时间戳,或者是全局递增的事务ID,在同一个时间点,不同的事务看到的数据是不同的。
下面是一个简单的例子, 以事务ID为版本号:
------------------------------------------------------> 时间轴
|-------Read(T1)-----|
|-----------Update(T2)-----------|
如上图,假设有两个并发操作Read(T1)和Update(T2):
T1和T2是事务ID,T1小于T2, (T1) 操作中包含的数据a = 1,
Read(T1)和Update(T2)的操作如下:
Read:read a =1(T1)
Update:a = 2 (T2)
Read:read (读操作)的版本T1表示要读取数据的版本,
在时间轴上,Read 早于Update,由于Update在Read 开始之后,所以Update提交的数据,所以对于Read 是不可见的。
所以,Read只会读取T1版本的数据,即a = 1。读不到 a =2
Update 写操作才会更新数据的版本,读操作不会。
关于MVCC数据的一致性
而对于读操作而言,只能读到在自己之前开始的,所有已经提交的写操作,正在执行中的写操作对其是不可见的。
由于在update操作提交之前,不能影响已有数据的一致性,所以不会改变旧的数据,
另外,update操作会被拆分成insert + delete。
update操作需要标记删除旧的数据,insert新的数据。只有update提交之后,才会影响后续的读操作。
mysql的innodb引擎是如何实现MVCC的
什么是 MVCC 多版本并发控制(Multi-Version Concurrent Control)呢 ?
其实就是 innodb会为每一行添加两个字段,注意是在每一行记录的后面增加两个隐藏列:
创建版本号 删除版本号,
分别表示该行创建的版本和删除的版本,填入的是事务版本号(事务的编号),
事务版本号随着事务的创建不断递增。
下面是一个简单的例子:
1、在插入操作时 :记录的创建版本号就是事务版本号(事务id )。
插入一条记录, 事务版本号(事务id )假设是1 ,那么记录如下:
id | name | create version | delete version |
---|---|---|---|
1 | test | 1 |
也就是说,创建版本号就是事务版本号(事务id )。
2、在更新操作的时候,采用的是先标记旧的那行记录为已删除,就是说,删除版本号是事务版本号,然后插入一行新的记录的方式。
比如,针对上面那行记录,事务Id为2 要把name字段更新
update table set name= 'new_value' where id=1;
id | name | create version | delete version |
---|---|---|---|
1 | test | 1 | 2 |
1 | new_value | 2 |
3、删除操作的时候,就把事务版本号作为删除版本号。比如
delete from table where id=1;
id | name | create version | delete version |
---|---|---|---|
1 | new_value | 2 | 3 |
4、查询操作:
select from table where id=1;
从上面的描述可以看到,在查询时要符合以下两个条件的记录才能被事务查询出来:
1)该行的创建版本号小于等于当前版本号(也就是,行的创建版本本号小于或等于事务版本号),
这样可以确保事务读取的行:
只么是在事务开始前已经存在的, 要么是事务自身插入或者修改过的。
2)该行的删除版本号大于当前版本或者为空。
这可以确保事务读取到的行,在事务开始之前未被删除。
通过前面的 3个事务, 目前的版本记录,具体如下:
id | name | create version | delete version |
---|---|---|---|
1 | test | 1 | 2 |
1 | new_value | 2 | 3 |
假设执行 这个查询的 select from table where id=1; 事务编号为 4
那么 该行的创建版本号 1/2 小于等于当前版本号 4 的记录,有两条。
该行的删除版本号 2/3大于当前版本或者为空,的, 0条。
所以, select from table where id=1; 查不到数据。
在repeated read的隔离级别下,MVCC具体的实现:
select:
满足以下两个条件innodb会返回该行数据:
该行的创建版本号小于等于当前版本号,保证改行在当前版本之前已经被插入。 该行的删除版本号大于当前版本或者为空。删除版本号大于当前版本意味着当前版本并未执行该行的删除操作,是之后才删除的。
insert:
将新插入的行的创建版本号设置为当前系统的版本号。说明该行在当前版本被插入。
delete:
将要删除的行的删除版本号设置为当前系统的版本号。说明该行在当前版本被删除。
update:
不执行原地update,而是转换成insert + delete。将旧行的删除版本号设置为当前版本号,并将新行insert同时设置创建版本号为当前版本号。 将旧行的删除版本号设置为当前版本号,并将新行insert同时设置创建版本号为当前版本号。
其他规则
执行insert、delete和update都要将系统版本号递增。执行select的版本号为系统版本号。 由于旧数据并不真正的删除,所以必须对这些数据进行清理,innodb会开启一个后台线程执行清理工作,具体的规则是将删除版本号小于当前系统版本的行删除,这个过程叫做purge。
总之:通过MVCC 就保证了各个事务互不影响。
数据库并发场景有三种,分别为:
1、读读:不存在任何问题,也不需要并发控制
2、读写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读、幻读、不可重复读
3、写写:有线程安全问题,可能存在更新丢失问题
MVCC是一种用来解决读写冲突的无锁并发控制,目标是高并发
MVCC为事务分配单项增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照,所以MVCC可以为数据库解决以下问题:
1、在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
2、解决脏读、幻读、不可重复读等事务隔离问题,但是不能解决更新丢失问题
从这里也可以体会到一种提高系统性能的思路,就是:通过版本号+副本的方式,来减少锁的争用。
MVCC的实现,通过保存数据在某个时间点的快照来实现的。
这意味着一个事务无论运行多长时间,在同一个事务里能够看到数据一致的视图。
根据事务开始的时间不同,同时也意味着在同一个时刻,不同事务看到的相同表里的数据可能是不同的。
MVCC的基本特征:
每行数据都存在一个版本,每次数据更新时,都更新该版本。 修改时Copy出当前版本随意修改,各个事务之间无干扰。 保存时比较版本号,如果成功(commit),则覆盖原记录;失败则放弃copy(rollback)
MVCC并不是MySql独有的,Oracle,PostgreSQL等都在使用。
聊聊:MVCC工作的事务隔离级别是啥?
MVCC只是工作在两种事务隔离级别底下:
(2) Read Committed (3) Repeatable Read;
其他两种:
(1) READ UNCOMMITTED, 可行读取到最新的没有提交的中间数据,而那些中间数据,不符合当前数据的事务版本要求
(3)Serializable 则会对所有的行加锁。
(1) 、(3) 这两种事务隔离级别,都不需要MVCC。
聊聊:Mysql中Undo Log 机制与MVCC的关系?
Undo Log 机制
Undo log的字面意思是撤销操作的日志,指的是使mysql中的数据回到某个状态。
在事务开启中(Undo Log是InnerDB独有的),mysql会将待修改的记录保存到Undo Log中。
如果数据库崩溃或者事务需要回滚时,mysql可以通过利用Undo log日志,将数据库中的事务回滚到之前的状态。
mysql新增、修改、删除数据时,在事务开启前,就会将信息写入Undo Log中,事务提交时,并不会立刻删除Undo Log,InnoDB存储引擎会将事务对应的Undo Log放入待删除列表中,之后会通过后台的purge thread对待删除的列表进行删除操作处理。
注意的是Undo log是一种逻辑日志,记录的是一种逻辑过程。比如:
mysql执行delete操作,Undo log就会记录一个insert操作; mysql执行一个insert操作,Undo Log就会记录一个delete操作; mysql执行update操作,Undo Log记录一个相反的update操作。
Undo Log以段的方式来管理记录日志信息,在InnoDB存储引擎的数据文件中,包含了一种rollback segment的回滚段,内部包含了1024个undo log segment 。
Undo Log实现了事务的原子性和多版本并发控制(MVCC):
Undo Log实现了事务的原子性
Mysql出现了错误、或者手动执行了事务的回滚,Undo Log会将数据库中的数据恢复到之前的状态。
Undo Log实现了事务的多版本并发控制(MVCC)
Mysql 基于 Undo log实现多版本并发控制
事务A未提交之前,undo log保存了未提交之前的版本,事务B读取的是之前的版本信息和旧数据的副本,
这个旧数据的副本,是从 undo log 的Buffer中获取的。
undo log的版本链模式
我们每条数据其实都有两个隐藏字段,一个是trx_id,一个是roll_pointer,
trx_id就是最近一次更新这条数据的事务id, roll_pointer就是指向更新了这个事务之前生成的undo log。
多个事务串行执行的时候,每个人修改了一行数据,都会更新隐藏字段txr_id和roll_pointer,
同时之前多个数据快照对应的undo log,会通过roll_pinter指针串联起来,形成一个重要的版本链!
最终版本链,链首存储的是最新的旧记录,链尾存储的是最旧的旧记录。
undo log不会无限膨胀,会存在一个后台清除线程,purge线程,发现当前记录不需要回滚且不需要参与MVCC的时候就会吧数据清理掉。
MySQL采用从新到旧(Newest To Oldest)的版本链。
如下图, V1被一个事务更新为V2,V2被另一个事务更新为V3,Δ1存储V1到V2的更新,Δ2存储V2到V3的更新。
此时,如果一个事条定位到B+Tree叶子节点的记录V3,则通过V3+Δ2可以还原出V2,通过V3+Δ2+Δ1可以还原出V1。
B+Tree叶结点上,始终存储的是最新的数据(可能是还未提交的数据)。
而旧版本数据,通过UNDO记录(做DELTA)存储在回滚段(Rollback Segment)里。
每一条记录都会维护一个ROW HEADER元信息,存储有创建这条记录的事务ID,一个指向UNDO记录的指针。
通过最新记录和UNDO信息,可以还原出旧版本的记录。
ReadView机制
这个ReadView呢,简单来说,每执行一个事务的时候,就给这个事务生成一个ReadView,
ReadView是某一个时间点,事务执行状态的一个快照,可以用来判断事务的可见性。ReadView的基本结构如下:
ReadView {
creator_trx_id
low_limit_id
up_limit_id
ids
...
}
creator_trx_id 创建这个ReadView的事务ID
low_limit_id 所有事务ID大于或等于low_limit_id对当前事务都不可见
up_limit_id 所有事务ID严格小于up_limit_id的事务对当前事务可见
ids 未提交的事务ID列表
ReadView 比较关键的东西有4个
一个是trx_ids ,这个就是说此时有哪些事务在MySQL里执行还没提交的, 保存这些事务id 的集合 一个是up_limit_id ,就是trx_ids 里最小的值; 一个是low_limit_id ,这是说mysql下一个要生成的事务id,就是最大事务id; 一个是creator_trx_id ,就是你这个事务的id
简单的说, 这些 记录,类似于乐观锁中的版本号的作用。
有了这个ReadView,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见。
如果被访问版本的trx_id属性值与ReadView中的 creator_trx_id 值相同,意味着当前事务在访问 它自己修改过的记录,所以该版本可以被当前事务访问。 如果被访问版本的trx_id属性值小于ReadView中的 up_limit_id 值,表明生成该版本的事务,在当前 事务生成ReadView前已经提交,所以该版本可以被当前事务访问。 如果被访问版本的trx_id属性值,大于或等于ReadView中的 low_limit_id 值,表明生成该版本的事 务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
如果被访问版本的trx_id属性值在ReadView的 up_limit_id 和 low_limit_id 之间,那就需要判 断一下trx_id属性值是不是在 trx_ids 列表中。
如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问。 如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
硬核文章推荐
一文搞懂:Java高手必备之 Mpsc 无锁队列 (史上最全) 一文搞懂:微服务核心组件 Nacos(史上最全) 一文搞懂:微服务核心组件 sentinel(史上最全) 一文秒懂:多级时间轮,最顶尖的Java调度算法 面试官:请说说 HashMap 的时间复杂度是多少? 一文穿透:缓存之王 Caffeine 架构、源码、原理(5W长文) 高性能组件:环形队列、 条带环形队列 Striped-RingBuffer 架构分析 一文穿透:队列之王 Disruptor 原理、架构、源码 如何优雅的使用 单例模式 ?来看看缓存之王 Caffeine 、链路之王 Skywalking 是如何做的吧! 吊打面试官,聊聊: 强引用、软引用、弱引用、虚引用?重点是 各自的 使用场景? 吊打面试官,聊聊:Java中String对象的大小? 细思极恐:Java官方JVM 为啥要叫做 HotSpot JVM?背后的水,不知道有多深!!! 彻底明白逃逸分析:Java对象不一定在堆上分配
硬核电子书
本文收录于:《尼恩Java 面试宝典》V15版
长按二维码,点击“识别图中二维码”即可查看老架构师尼恩个人微信,发暗号 “领电子书” 给尼恩,获取最新PDF。
最新的《尼恩Java面试宝典》
极致经典,不断升级,目前最新为V15
尼恩Java高并发三部曲
《Java高并发核心编程-卷1(加强版)》,不断升级
《Java高并发核心编程-卷2(加强版)》,不断升级
《Java高并发核心编程-卷3(加强版)》,不断升级
尼恩架构笔记100篇+,不断添加