MySQL事务、隔离级别及MVCC是什么

45次阅读
没有评论

共计 9666 个字符,预计需要花费 25 分钟才能阅读完成。

自动写代码机器人,免费开通

这篇文章主要介绍了 MySQL 事务、隔离级别及 MVCC 是什么,具有一定借鉴价值,需要的朋友可以参考下。希望大家阅读完这篇文章后大有收获。下面让丸趣 TV 小编带着大家一起了解一下。

mysql 教程栏目介绍 MySQL 相关的事务、隔离级别及 MVCC。

MySQL 系列的第四篇,主要内容是事务,包括事务 ACID 特性,隔离级别,脏读、不可重复读、幻读的理解以及多版本并发控制(MVCC)等内容。

事务(Transaction)能够保证一组不可分割的原子性操作集合要么都执行,要么都不执行。在 MySQL 常用的存储引擎中,InnoDB 是支持事务的,原生的 MyISAM 引擎则不支持事务。

在本文中,若未特殊说明,使用的数据表及数据如下所示:

CREATE TABLE `user` (`id` int(11) DEFAULT NULL, `name` varchar(12) DEFAULT NULL) ENGINE = InnoDB;insert into user values(1, 刺猬 复制代码 

1. ACID 四大特性

首先需要理解的是事务 ACID 四大特性,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability),这也是事务的四个基本要素。

为了详细解释 ACID 特性,在这里先设想一个场景:我向你转账 100 元。

假设这个操作可以分为以下几步(假设我和你的账户余额均为 100 元):

查询我的账户余额我的账户扣款 100 元 100 元开始转移查询你的账户余额你的账户到账 100 元 1.1 原子性(Atomicity)

事务的原子性是指:一个事务必须是不可再分割的最小工作单元,一个事务中的操作要么都成功,要么都失败,不可能存在只执行一个事务中部分操作的情况。

在上述的转账场景中,原子性就要求了这五个步骤要么都执行,要么都不执行,不可能存在我的账户扣款 100 元,而你的账户 100 元没有到账的情况。

1.2 一致性(Consistency)

事务的一致性是指:数据库总是从一个一致性状态转换到另一个一致性状态,一致性侧重的是数据的可见性,数据的中间状态对外是不可见的。

同时,事务的一致性要求符合开发人员定义的约束,如金额大于 0、身高大于 0 等。

在上述的转账场景中,一致性能够保证最终执行完整个转账操作后,我账户的扣款金额与你账户到账金额是一致的,同时如果我和你的账户余额不满足金额的约束(如小于 0),整个事务会回滚。

1.3 隔离性(Isolation)

事务的隔离性是指:在一次状态转换过程中不会受到其他状态转换的影响。

假设我和你都有 100 元,我发起两次转账,转账金额都是 50 元,下面使用伪代码来表示的操作步骤:

查询我的账户余额 read my 我的账户扣款 50 元 my=my-5050 元开始转移查询你的账户余额 read yours 你的账户到账 50 元 yours=yours+50

如果未保证隔离性就可能发生下面的情况:

时刻第一次转账第二次转账我的账户余额你的账户余额 1read my(100)
my=100yours=1002
read my(100)my=100yours=1003my=my-50=100-50=50
my=50yours=1004read yours(100)my=my-50=100-50=50my=50yours=1005yours=yours+50=100+50=150
my=50yours=1506
read yours(150)my=50yours=1507
yours=yours+50=150+50=200my=50yours=2007endendmy=50yours=200

两次转账后,最终的结果是我的账户余额为 50 元,你的账户余额为 200 元,这显然是不对的。

而如果在保证事务隔离性的情况下,就不会发生上面的情况,损失的只是一定程度上的一致性。

1.4 持久性(Durability)

事务的持久性是指:事务在提交以后,它所做的修改就会被永久保存到数据库。

在上述的转账场景中,持久性就保证了在转账成功之后,我的账户余额为 0,你的账户余额为 200。

2. 自动提交与隐式提交 2.1 自动提交

在 MySQL 中,我们可以通过 begin 或 start transaction 来开启事务,通过 commit 来关闭事务,如果 SQL 语句中没有这两个命令,默认情况下每一条 SQL 都是一个独立的事务,在执行完成后自动提交。

比如:

update user set name= 重塑 where id=1; 复制代码 

假设我只执行这一条更新语句,在我关闭 MySQL 客户端然后重新打开一个新的客户端后,可以看到 user 表中的 name 字段值全变成了「重塑」,这也印证了这条更新语句在执行后已被自动提交。

自动提交是 MySQL 的一个默认属性,可以通过 SHOW VARIABLES LIKE autocommit 语句来查看,当它的值为 ON 时,就代表开启事务的自动提交。

mysql SHOW VARIABLES LIKE autocommit 
+---------------+-------+| Variable_name | Value |
+---------------+-------+| autocommit | ON |
+---------------+-------+1 row in set (0.00 sec) 复制代码 

我们可以通过 SET autocommit = OFF 来关闭事务的自动提交。

2.2 隐式提交

然而,即便我们已经将 autocommit 变量的值改为 OFF 关闭事务自动提交了,在执行某些 SQL 语句的时候,MySQL 还是会将事务自动提交掉,这被称为隐式提交。

会触发隐式提交的 SQL 语句有:

DDL(Data definition language,数据定义语言),如 create, drop, alter, truncate 修改 MySQL 自带表数据的语句,如 create/drop user, grant, set password 在一个事务中,开启一个新的事务,会隐式提交上一个事务,如:时刻事务 A 事务 B1begin;
2update user set name= 重塑 where id=1;
3
select name from user where id=1;(N1)4begin;
5
select name from user where id=1;(N2)

在事务 B 中有两个查询语句 N1 和 N2,执行的结果是 N1= 刺猬,N2= 重塑,由此可以证明。

其他还有一些管理语句就不一一举例了,可自行百度。3. 隔离级别

事务的隔离级别规定了一个事务中所做的修改,在事务内和事务间的可见性。较低级别的隔离通常可以执行更高的并发,系统开销也更低。

在 SQL 标准中定义了四种事务的隔离级别,分别是读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)、可串行化(Serializable)。

为了详细解释这四种隔离级别及它们各自发生的现象,假设有两个事务即将执行,执行内容如下表:

时刻事务 A 事务 B1begin;
2
begin;3
update user set name= 重塑 where id=1;4select name from user where id=1;(N1)
5
commit;6select name from user where id=1;(N2)
7commit;
8select name from user where id=1;(N3)

在事务 A 和事务 B 执行的过程中,有三处查询 N1,N2,N3,在每个隔离级别下,它们值的情况是不同的,下面分别讨论。

3.1 读未提交(Read Uncommitted)

在读未提交的隔离级别下,事务中的修改,即便没有提交,对其他事务也都是可见的。

在上述场景中,若数据库的隔离级别为读未提交,由于事务 A 可以读取未提交事务 B 修改后的数据,即时刻 3 中事务 B 的修改对事务 A 可见,所以 N1= 重塑,N2= 重塑,N3= 重塑。

3.2 读已提交(Read Committed)

在读已提交的隔离级别下,事务中的修改只有在提交之后,才会对其他事务可见。

在上述场景中,若数据库的隔离级别为读已提交,由于事务 A 只能读取事务 B 提交后的数据,即时刻 3 中事务 B 的修改对事务 A 不可见,N2 处的查询在事务 B 提交之后,故对事务 A 可见。所以 N1= 刺猬,N2= 重塑,N3= 重塑。

3.3 可重复读(Repeatable Read)

可重复读是 MySQL 的默认事务隔离级别。在可重复读的隔离级别下,一个事务中多次查询相同的记录,结果总是一致的。

在上述场景中,若数据库的隔离级别为可重复读,由于查询 N1 和 N2 在一个事务中,所以它们的值都是「刺猬」,而 N3 是在事务 A 提交以后再进行的查询,对事务 B 的修改是可见的,所以 N3= 重塑。

3.4 可串行化(Serializable)

在可串行化的隔离级别下,事务都是串行执行的,读会加读锁,写会加写锁,事务不会并发执行,所以也就不会发生异常情况。

在上述场景中,若数据库的隔离级别为可串行化,首先开启事务 A,在开启事务 B 时被阻塞,直到事务 A 提交之后才会开启事务 B,所以 N1= 刺猬,N2= 刺猬。而 N3 处的查询会在事务 B 提交之后才执行(事务 B 先被阻塞,执行顺序在 N3 查询语句之前),所以 N3= 重塑。

4. 隔离级别导致的问题

在不同的事务隔离级别中,如果遇到事务并发执行,就会出现很多问题,如脏读(Dirty Read)、不可重复读(Non-Repeatable Read)、幻读(Phantom Read)等,下面就分别用不同的例子来详细说明这些问题。

4.1 脏读(Dirty Read)

脏读(Dirty Read)是指一个事务可以读取另一个未提交事务修改的数据。

看下面的案例,假设隔离级别为读未提交:

时刻事务 A 事务 B1begin;
2
begin;3
update user set name= 重塑 where id=1;4select name from user where id=1;(N1)
5
rollback;6select name from user where id=1;(N2)
7commit;

在读未提交的隔离级别下,N1 的值是「重塑」,由于事务 B 的回滚,N2 的值是「刺猬」。这里在 N1 处就发生了脏读,显然 N1 处的查询结果是一个脏数据,会对正常业务产生影响。

脏读会发生在读未提交的隔离级别中。

4.2 不可重复读(Non-Repeatable Read)

不可重复读(Non-Repeatable Read)是指,两次执行相同的查询可能会得到不一样的结果。

继续使用介绍隔离级别时的 AB 事务案例,同时假设隔离级别为读已提交:

时刻事务 A 事务 B1begin;
2
begin;3
update user set name= 重塑 where id=1;4select name from user where id=1;(N1)
5
commit;6select name from user where id=1;(N2)
7commit;
8select name from user where id=1;(N3)

在读已提交的隔离级别下,事务可以读取到其他事务提交的数据。在上述案例中结果是 N1= 刺猬,N2= 重塑,N3= 重塑,在事务 A 中,有两次相同的查询 N1 和 N2,但是这两次查询的结果并不相同,这就发生了不可重复读。

不可重复读会发生在读未提交、读已提交的隔离级别中。

4.3 幻读(Phantom Read)

幻读(Phantom Read)是指,一个事务在读取某个范围内记录时,另外一个事务在该范围内插入一条新记录,当之前的事务再次读取这个范围的记录时,会读到这条新记录。

看下面的案例,假设此时隔离级别为可重复读:

时刻事务 A 事务 B1begin;
2select name from user;(N1)
3
begin;4
insert into user values(2, 五条人 5
commit;6select name from user;(N2)
7select name from user for update;(N3)
8commit;

事务 A 有三次查询,在 N1 和 N2 之间,事务 B 执行了一条 insert 语句并提交,N3 处的查询使用的是 for update。

N1 处的结果很显然只有「刺猬」,N2 处的结果由于事务 A 开启在事务 B 之前,所以也是「刺猬」,而 N3 处的结果理论上在可重复读的隔离级别中也应该只有「刺猬」,但实际上 N2 的结果是「刺猬」和「五条人」,这就发生了幻读。

这就很奇怪了,不是说可重复读的隔离级别能够保证一个事务中多次查询相同的记录,结果总是一致的吗?这种结果并不满足可重复读的定义。

事实上,在可重复读的隔离级别下,如果使用的是当前读,那么就可能发生幻读现象。

当前读和快照读会在下文中介绍事务的实现原理及 MVCC 时讨论,这里先给一个结论。

幻读会发生在读未提交、读已提交、可重复读的隔离级别中。

这里需要额外注意的是:幻读和不可重复读都是说在一个事务中的同一个查询语句结果不同,但幻读更侧重于查询到其他事务新插入的数据(insert)或其他事务删除的数据(delete),而不可重复读的范围更广,只要结果不同就可以认为是不可重复读,但一般我们认为不可重复读更侧重于其他事务对数据的更新(update)。

4.4 小结

通过上面的描述,我们已经知道四种隔离级别的概念以及它们分别会遇到的问题,事务的隔离级别越高,隔离性就越强,所遇到的问题也就越少。但同时,隔离级别越高,并发能力就越弱。

下表是对隔离级别的概念不同隔离级别会发生的问题情况的小结:

隔离级别脏读不可重复读幻读概念读已提交√√√事务中的修改,即便没有提交,对其他事务也都是可见的读未提交
√√事务中的修改只有在提交之后,才会对其他事务可见可重复读

√一个事务中多次查询相同的记录,结果总是一致的可串行化

事务都是串行执行的,读会加读锁,写会加写锁 5. MVCC

MVCC(Multi-Version Concurrency Control)即多版本并发控制,这是 MySQL 为了提高数据库并发性能而实现的。它可以在并发读写数据库时,保证不同事务的读 - 写操作并发执行,同时也能解决脏读、不可重复读、幻读等事务隔离问题。

在前文讨论幻读的时候提到过当前读的概念,正是由于当前读,才会在可重复读的隔离级别下也会发生幻读的情况。

在解释可重复读隔离级别下发生幻读的原因之前,首先介绍 MVCC 的实现原理。

5.1 MVCC 的实现原理

首先我们需要知道,InnoDB 的数据页中每一行的数据是有隐藏字段的:

DB_ROW_ID: 隐式主键,若表结构中未定义主键,InnoDB 会自动生成该字段作为表的主键 DB_TRX_ID: 事务 ID,代表修改此行记录的最后一次事务 IDDB_ROLL_PTR: 回滚指针,指向此行记录的上一个版本(上一个事务 ID 对应的记录)

每一条修改语句都会相应地记录一条回滚语句(undo log),如果把每一条回滚语句视为一条数据表中的记录,那么通过事务 ID 和回滚指针就可以将对同一行的修改记录看作一个链表,链表上的每一个节点就是一个快照版本,这就是 MVCC 中多版本的意思。

举个例子,假设对 user 表中唯一的一行「刺猬」进行多次修改。

update user set name= 重塑 where id=1;update user set name= 木马 where id=1;update user set name= 达达 where id=1; 复制代码 

那么这条记录的版本链就是:

MySQL 事务、隔离级别及 MVCC 是什么

在这个版本链中,头结点就是当前记录的最新版本。DB_TRX_ID 事务 ID 字段是非常重要的属性,先 Mark 一下。

除此之外,在读已提交(RC,Read Committed)和可重复读(RR,Repeatable Read)的隔离级别中,事务在启动的时候会创建一个读视图(Read View),用它来记录当前系统的活跃事务信息,通过读视图来进行本事务之间的可见性判断。

在读视图中有两个重要的属性:

当前事务 ID:表示生成读视图的事务的事务 ID 事务 ID 列表:表示在生成读视图时,当前系统中活跃着的事务 ID 列表最小事务 ID:表示在生成读视图时,当前系统中活跃着的最小事务 ID 下一个事务 ID:表示在生成读视图时,系统应该分配给下一个事务的事务 ID

需要注意下一个事务 I 的值,并不是事务 ID 列表中的最大值 +1,而是当前系统中已存在过的事务的最大值 +1。例如当前数据库中活跃的事务有 (1,2),此时事务 2 提交,同时又开启了新事务,在生成的读视图中,下一个事务 ID 的值为 3。

我们通过将版本链与读视图两者结合起来,来进行并发事务间可见性的判断,判断规则如下(假设现在要判断事务 A 是否可以访问到事务 B 的修改记录):

若事务 B 的当前事务 ID 小于事务 A 的最小事务 ID 的值,代表事务 B 是在事务 A 生成读视图之前就已经提交了的,所以事务 B 对于事务 A 来说是可见的。若事务 B 的当前事务 ID 大于或等于事务 A 下一个事务 ID 的值,代表事务 B 是在事务 A 生成读视图之后才开启,所以事务 B 对于事务 A 来说是不可见的。若事务 B 的当前事务 ID 在事务 A 的最小事务 ID 和下一个事务 ID 之间(左闭右开,[最小事务 ID, 下一个事务 ID)),需要分两种情况讨论:若事务 B 的当前事务 ID 在事务 A 的事务 ID 列表中,代表创建事务 A 时事务 B 还是活跃的,未提交,所以事务 B 对于事务 A 来说是不可见的。若事务 B 的当前事务 ID 不在事务 A 的事务 ID 列表中,代表创建事务 A 时事务 B 已经提交,所以事务 B 对于事务 A 来说是可见的。

如果事务 B 对于事务 A 来说是不可见的,就需要顺着修改记录的版本链,从回滚指针开始往前遍历,直到找到第一个对于事务 A 来说是可见的事务 ID,或者遍历完版本链也未找到(表示这条记录对事务 A 不可见)。

这就是 MVCC 的实现原理。

5.2 读视图的创建时机

这里需要注意的是读视图的创建时机,在上面的论述中我们已经知道事务在启动时会创建一个读视图(Read View),而开启一个事务有两种方式,一是 begin/start transaction,二是 start transaction with consistent snapshot,通过这两种方式开启事务,创建读视图的时机也是不同的:

如果是以 begin/start transaction 方式开启事务,读视图会在执行第一个快照读语句时创建如果以 start transaction with consistent snapshot 方式开启事务,同时便会创建读视图 5.3 MVCC 的运行过程

为了详细说明 MVCC 的运行过程,下面举个例子,假设当前存在有两个事务(事务隔离级别为 MySQL 默认的可重复读):

这里需要注意的是事务的启动时机,在上面的论述中我们已经知道事务在启动时会创建一个读视图(Read View),而开启一个事务有两种方式,一是 begin/start transaction,二是 start transaction with consistent snapshot,通过这两种方式开启事务,创建读视图的时机也是不同的:

如果是以 begin/start transaction 方式开启事务,读视图会在执行第一个快照读语句时创建如果以 start transaction with consistent snapshot 方式开启事务,同时便会创建读视图时刻事务 A 事务 B1start transaction with consistent snapshot;
2
start transaction with consistent snapshot;3
update user set name= 重塑 where id=1;4select name from user where id=1;(N1)
5
commit;6select name from user where id=1;(N2)
7commit;

然后根据上面所描述的版本链以及两个事务开启时的读视图来分析 MVCC 的运行过程。

MySQL 事务、隔离级别及 MVCC 是什么

上图是两个事务开启时的读视图,而当事务 B 的更新语句执行之后,id= 1 行的版本链如下所示。

MySQL 事务、隔离级别及 MVCC 是什么

先来看 N1 处的查询语句,事务 B 的当前事务 ID=2,其值等于事务 A 的下一个事务 ID,所以按照上文中所论述的可见性判断,事务 B 对于事务 A 来说是不可见的,需要循着当前行的版本链网上检索。

于是循着版本链来到 DB_TRX_ID= 1 事务 ID= 1 的历史版本,恰巧等于事务 A 的事务 ID 值,也就是事务 A 开启时该行的版本,此版本对于事务 A 来说当然是可见的,所以读取到了 id= 1 行的 name= 刺猬,即最终 N1= 刺猬。

再来看 N2 处的查询语句,此时事务 B 已提交,版本链还是如上图所示,由于当前版本的事务 ID 等于事务 A 读视图中的下一个事务 ID,所以当前版本的记录对于事务 A 来说是不可见的,所以同样 N2= 刺猬。

这里需要注意的是,若例子中事务 A 的时刻 4 语句变更为对该行的更新语句,那么事务 A 便会等待事务 B 提交之后再执行更新语句,这是因为事务 B 未提交,即事务 B 对于 id= 1 行的写锁未释放,而事务 A 也要更新该行,必须是更新当前的最新版本(当前读)才可以,所以事务 A 就被阻塞了,必须等待事务 B 对该行的写锁释放,才会继续执行更新语句。

5.4 RC 与 RR 生成读视图的时机对比

上面所讨论的 MVCC 运行过程都是针对可重复读(RR, Repeatable Read)隔离级别的,如果是读已提交(RC, Read Committed)级别呢?

上文中已经讨论过读已提交隔离级别中关于不可重复读的情况了,这里就不再举例,直接给出结论就可以了。

可重复读(RR, Repeatable Read)隔离级别下生成读视图(Read View)的时机是开启事务的时候读已提交(RC, Read Committed)隔离级别下生成读视图(Read View)的时机是每一条语句执行前

对于上文中描述 MVCC 执行过程中的例子,如果隔离级别是读已提交(RC, Read Committed):

N1 处的查询语句,由于事务 B 还未提交,事务 A 可见的版本依旧是事务 ID= 1 的版本,所以 N1= 刺猬 N2 处的查询语句,事务 B 已提交,N2 处查询语句执行时也会生成读视图,其当前事务 ID=3,而在该记录的版本链中,当前版本的事务 ID DB_TRX_ID=2,在 N2 查询语句事务 ID 之前,是可见的,所以 N2= 重塑 5.5 当前读与快照读当前读:读取记录的最新版本快照读:读取记录时会根据一定规则读取事务可见版本的记录 5.6 可重复读发生幻读的原因

在理解了 MVCC 之后,我们再来看在可重复读隔离级别下发生幻读的原因。上文中说到正是由于当前读,才会在可重复读的隔离级别下发生幻读的情况,首先来回顾一下例子。

时刻事务 A 事务 B1begin;
2select name from user;(N1)
3
begin;4
insert into user values(2, 五条人 5
commit;6select name from user;(N2)
7select name from user for update;(N3)
8commit;

N1,N2 处的查询想必已经十分明确都是「刺猬」了。而在 N3 处所使用的查询语句是 for update,使用它进行查询就会对目标记录添加一把「行级锁」,行级锁的意义以后再说,现在只需要知道 for update 能够锁住目标记录就可以了。

加锁自然是防止别人修改,那么理所当然,锁住的当然也就是记录的最新版本了。所以,在使用 for update 进行查询的时候,会使用当前读,读到目标记录的最新版本,所以在 N3 处的查询语句就会把事务 B 中本对于事务 A 来说不可见的记录也查询出来,也就发生了幻读。

使用当前读的语句有:

select … for updateselect … lock in share mode(共享读锁)update …insert …delete …

感谢你能够认真阅读完这篇文章,希望丸趣 TV 小编分享 MySQL 事务、隔离级别及 MVCC 是什么内容对大家有帮助,同时也希望大家多多支持丸趣 TV,关注丸趣 TV 行业资讯频道,遇到问题就找丸趣 TV,详细的解决方法等着你来学习!

向 AI 问一下细节

丸趣 TV 网 – 提供最优质的资源集合!

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-12-18发表,共计9666字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)