vlambda博客
学习文章列表

MySQL 事务操作与事务隔离级别原理分析

干货福利,不错过


 事务是数据处理的最小操作单元,是一组不可再分割的操作集合,这个操作单元里的一系列操作要么都成功,要么都失败。
 在数据库事务操作过程中必须要遵循事务的4个特性:原子性( Atomicity) 、一致性( Consistency )、隔离性( Isolation) 、持久性( Durability) ,简称: ACID

本文所述的事务都是基于 MySQL 的 InnoDB 引擎。



隐式事务&显式事务


MySQL 默认采用自动提交(AUTOCOMMIT)模式。也就是说,如果不是显式地开始一个事务,则每个 SQL 操作都会被当做一个事务执行提交操作,这种事务被称为隐式事务。当然我们也可以通过设置 AUTOCOMMIT 参数来启用或禁用自动提交模式:

-- 查看 AUTOCOMMIT 参数配置
SHOW VARIABLES LIKE 'AUTOCOMMIT';
-- 设置 AUTOCOMMIT 参数为禁用状态,1或ON表示启用,0或OFF表示禁用(该修改操作只对当前会话session有效)
SET AUTOCOMMIT = 0;

当 AUTOCOMMIT = 0 时,所有的SQL操作都在一个事务中,直到显式地执行 COMMIT 提交或 ROLLBACK 回滚。

此外,如果我们想通过多个SQL语句作为一个事务执行时,还可以通过 BEGIN 开启一个事务,然后通过执行 COMMIT 提交或 ROLLBACK 回滚一个事务。这种通过手动方式完成的事务操作被称为显式事务。

BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT; -- 当然也可以使用 ROLLBACK 回滚整个事务,事务执行失败



事务的四个特性


一个完整的数据库事务操作必须要具备 ACID 这4个特性:

原子性(Atomicity)

原子性是指事务是一个不可分割的工作单位,事务中的操作要么都成功(commit),要么都失败(rollback)。

一致性(Consistency

事务前后数据的完整性必须保持一致。数据一致性是基础,也是最终目的,其他三个特性(原子性、隔离性和持久性)都是为了保证数据的一致性。

隔离性(Isolation)

事务的隔离性是指多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。

持久性(Durability)

持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。

事务的原子性与持久性侧重于研究事务本身,而事务的隔离性研究的是不同事务之间的相互影响。如果不考虑事务的隔离性问题,可能会导致脏读、不可重复读和幻读的问题,从而导致在并发情况下出现数据一致性的问题。



脏读、不可重复读、幻读问题


脏读、不可重复读、幻读这三种问题的出现与数据库并发事务有密切的关系。为了解决这些问题,SQL 标准定义了四种隔离级别,通过隔离级别解决了这些问题。在研究隔离级别前我们需要了解一下脏读、不可重复读、幻读的概念。

脏读

事务A读取了事务B中没有提交的数据,若事务B中的更新操作回滚,那么事务A中读取的数据是不存在的,这种现象就是脏读。注意:只有事务隔离级别为 读未提交 时才会出现脏读问题。

不可重复读

在同一个事务中,多次读取相同的数据,但是读取的结果不一样,称之为不可重复读。注意:当事务隔离级别为 读未提交 和 读已提交 时都可能会出现不可重复读问题。

幻读

在同一个事务中,第一次查询某条记录,发现没有,但是再次读取同一条记录,它就神奇地出现了,称之为幻读。注意:当事务隔离级别为 读未提交 、读已提交、可重复读 时都可能会出现幻读问题。

脏读、不可重复读、幻读用事务操作表示如下:



事务的隔离级别


为了保证数据一致性,SQL 标准定义了四种隔离级别,这四种隔离级别分别如下:

读未提交(READ UNCOMMITTED)

在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别是最低的隔离级别,很少用于实际应用中。隔离级别为 读未提交 时,可能会存在 脏读、不可重复读、幻读 的问题。

读已提交(READ COMMITTED)

它是大多数数据库系统的默认隔离级别(但不是 MySQL 默认的)。它满足了隔离的简单定义:一个事务只能读到已提交事务所做的改变。隔离级别为 读已提交 时,可以防止 脏读 问题,但会出现 不可重复读、幻读 的问题。

可重复读(REPEATABLE READ)

它可以确保在同一个事务内的查询结果是一致的。隔离级别为 可重复读 时,可以防止 脏读 和 不可重复读 的问题,但可能发生 幻读 问题。

注意,MySQL 的 InnoDB 存储引擎,在 可重复读 隔离级别中解决了幻读问题,但是通过 UPDATE 更新不存在的记录时,竟然能成功。后面会进行示例说明。

串行化(SERIALIZABLE)

串行化是最严格的隔离级别,同样性能也是最差的。隔离级别为 串行化 时,所有事务按照次序依次执行,因此,脏读、不可重复读、幻读 都不会出现。

从上往下,隔离强度逐渐增强,性能逐渐变差,采用哪种隔离级别要根据系统需求权衡决定。总的来说,隔离级别和脏读、不可重复读以及幻读的对应关系如下:

MySQL 事务操作与事务隔离级别原理分析

单纯的理论概念,很可能会让你晕头转向。为了更好地理解事务隔离级别,下面我们通过几个实际的 SQL 操作来进行一下演示。

查看&设置事务隔离级别

我们可以通过以下语句来查看并设置当前数据库的隔离级别。

查看事务隔离级别

-- MySQL 5.6及以下版本 | 查看数据库版本:select version(); 
SELECT @@GLOBAL.tx_isolation as '全局-隔离级别', @@tx_isolation as '会话-隔离级别';
-- MySQL 5.6以上版本
SELECT @@GLOBAL.transaction_isolation AS '全局-隔离级别', @@transaction_isolation AS '会话-隔离级别';

设置事务隔离级别

SET {作用域} TRANSACTION ISOLATION LEVEL {事务隔离级别};
-- 作用域:SESSION | GLOBAL
-- 事务隔离级别:READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE
注意:作用域 GLOBAL 是全局有效,而 SESSION 只针对当前会话窗口生效,所以在测试时只修改当前 SESSION 的隔离级别就可以。另外,如果你使用的是 Navicat 的话,不同的查询窗口会连接不同的 SESSION 会话;如果你使用的是 SQLyog,那么不同查询窗口使用的是同一个 SESSION 会话,所以需要你再开启一个新的连接。
隔离级别实操演示

在开始之前我们先创建一张数据表,表结构即数据内容如下:

CREATE TABLE `db_user` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(20) DEFAULT NULL COMMENT '姓名',
  `account` INT DEFAULT NULL COMMENT '账户余额',
  PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
INSERT INTO `db_user` (`id`, `name`, `account`) VALUES('1','李四','1000');

读未提交

在该事务隔离级别下,一个事务会读到另一个事务更新后但未提交的数据,如果另一个事务回滚,那么当前事务读到的数据就是脏数据,这也就是脏读问题。我们来看看下面这个例子:

MySQL 事务操作与事务隔离级别原理分析

当事务A在 T3 时,它更新了 李四 的记录,但并未提交,而事务B在 T4 时,读取到了事务A还未提交的数据。但是,事务A在 T5 时进行了事务回滚操作,那么此时事务B再次读取数据发现和上一次读到的数据不一致,也就是产生了脏读。

读未提交 隔离级别没办法解决脏数据问题,更别说不可重复读和幻读的问题了。

读已提交

在该事务隔离级别下,一个事务可能会遇到不可重复读的问题。我们来看看下面这个例子:

当事务B在 T3 时,查询到的结果是 1000,随后,由于事务A在 T4 更新了这条记录并完成提交,所以,事务B在 T6 时查询到的结果是 2000。因此,在 读已提交 隔离级别下,事务不可重复读同一条记录,因为很可能读到的结构不一致。

读已提交 隔离级别解决了脏读的问题,但是无法做到可重复读,也没办法解决幻读。

可重复读

在标准 SQL 定义中的可重复读隔离级别下,一个事务可能会遇到幻读问题。而 MySQL 的 InnoDB 存储引擎解决了幻读问题。但是在更新操作中却可以操作成功。具体我们看看下面这个例子:

事务B在 T3 时,读到的记录为空,说明此时 张三 的记录还不存在。随后,事务A在 T4 插入了一条 张三 的记录并提交。然后,事务B在 T6 时查询记录仍然为空,由此可见,InnoDB 存储引擎在 可重复读 隔离级别下,不存在幻读问题。但是,事务B在 T7 时,试图更新这条不存在的记录时,竟然成功了,并且,事务B在 T8 时,再次读取该记录,发现记录存在了。

可重复读 隔离级别解决了脏读问题,并且做到了可重复读,同时在 InnoDB 存储引擎中也一定程度的避免了幻读问题的出现。

串行化

在该事务隔离级别下,所有的事务操作都按照次序依次执行。因此,脏读、不可重复读、幻读这些问题都不会出现。所以,串行化是最严格的隔离级别,但这也会大大降低系统的性能。如果没有特别重要的情景,一般都不会使用 Serializable 隔离级别。

脏读、不可重复读、幻读问题的解决原理

要解决脏读、不可重复读、幻读这些问题,其实有两种可选的解决方案:

方式一:读操作利用 MVCC 机制,写操作进行加锁

所谓的 MVCC 我们在后面再进行详细描述,简单来说就是通过生成数据快照,然后利用数据快照找到符合条件的记录版本,在生成数据快照之前未提交的事务或者之后才开启的事务所做的更改是看不到的。

而 “” 操作肯定针对的是最新版本的记录,读记录的历史版本和变更记录的最新版本本身并不冲突,也就是采用 MVCC 时,“读-写” 操作并不冲突。

在 “读已提交” 隔离级别下,数据快照会在每一次进行普通 SELECT 操作前生成,数据快照的存在本身就保证了事务不可以读取到未提交的事务所做的更改,也就是避免了脏读现象。

在 “可重复读” 隔离级别下,数据快照只在第一次进行普通 SELECT 操作前生成,之后的查询操作都重复使用这个快照,这样也就避免了不可重复读和幻读的问题。

方式二:读、写操作都采用加锁的方式

如果我们的一些业务场景不允许读取记录的旧版本,而是每次都必须读取记录的最新版本。也就是说读取记录的时候也需要对其进行 “加锁” 操作,这也就意味着 “” 操作和 “” 操作也像 “写-写” 操作那样排队执行(即,串行化)。

很明显,采用 MVCC 方式的话,“读-写” 操作彼此并不冲突,性能更高,采用 “加锁” 方式的话,“读-写” 操作彼此需要排队执行,影响性能。一般情况下我们更倾向于采用 MVCC 来解决 “读-写” 操作并发执行的问题。



MVCC(多版本并发控制)


MVCC(Multi-Version Concurrency Control),即多版本并发控制。数据库系统基于提升并发性能的考虑,它们一般都会实现多版本并发控制。

MVCC实现原理介绍

我们可以认为 MVCC 是行级锁的一个变种,它是通过保存数据在某个时间点的快照来实现的。也就是说,不管事务执行多久,在该事务中看到的数据都是一致的,并且根据事务开始时间的不同,每个事务对同一张表,同一时刻看到的数据可能会是不一样的。

MVCC 没有统一的实现标准,不同存储引擎对 MVCC 的实现方式可能不同。在 InnoDB 存储引擎中,MVCC 是通过在每行记录后面保存两个隐藏列来实现的。这两个列,一个用来记录数据行的创建时间,另一个用来记录数据行的过期时间(删除时间),但是在实际操作中,存储的并不是时间,而是系统版本号。

每开始一个新的事务,系统版本号都会自动递增。事务开始时的系统版本号会作为事务的版本号,用来和查询到的每一行记录的版本号进行比较。下面我们看看在可重复读(REPEATABLE READ)隔离级别下,MVCC具体是如何操作的:

SELECT

InnoDB 会检查每行数据记录是否符合以下两个条件:

  1. InnoDB 只查找版本早于当前事务版本的数据行(也就是,行的系统版本号 <= 当前事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。

  2. 行的删除版本要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除。

INSERT

InnoDB 为新插入的每一行保存当前系统版本号作为行版本号。

DELETE

InnoDB 为删除的每一行保存当前系统版本号作为行删除标识。

UPDATE

InnoDB 会插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识。

MVCC与实务隔离级别

MVCC 只在 “读已提交” 和 “可重复读” 两个隔离级别下工作,其他两个隔离级别都和MVCC不兼容。

在“读已提交” 隔离级别时,数据快照会在每一次进行普通 SELECT 操作前生成;而 “可重复读” 隔离级别时,数据快照只在第一次进行普通 SELECT 操作前生成,之后的查询操作都重复使用这个快照。

END



请戳 “阅读原文”,一起来充电!