一文读懂 MySQL 事务隔离机制

MySQL 事务隔离机制

  • 隔离性与隔离级别
  • 四个案例看懂 MySQL 事务隔离级别
    • 查看隔离级别
    • READ_UNCOMMITTED
      • 脏读
      • 不可重复读
      • 幻读
    • READ_COMMITTED
    • REPEATABLE_READ
    • SERIALIZABLE
  • 事务隔离的实现
  • 总结

提到事务,大家肯定不陌生。最经典的例子就是银行转账。

比如,A 账户给 B 账户转账 100。在这种交易的过程中,有几个问题值得思考:

  • 如何同时保证上述交易中,A 账户总金额减少 100,B账户总金额增加 100?
  • A 账户如果同时在和 C 账户交易(T2),如何让这两笔交易互不影响?
  • 如何在支持大量交易的同时,保证数据的合法性(没有钱凭空产生或消失) ?
  • 如果交易完成时数据库突然崩溃,如何保证交易数据成功保存在数据库中?

要保证交易正常可靠地进行,数据库就得解决上面的四个问题,这也就是事务诞生的背景,它能解决上面的四个问题。

简单来说,事务就是要保证一组数据库操作,要么全部成功,要么全部失败。在MySQL 中,事务支持是在引擎层实现的。

我们知道,MySQL 是一个支持多引擎的系统,但并不是所有的引擎都支持事务。

比如 MySQL 原生的 MyISAM 引擎就不支持事务,这也是 MyISAM 被 InnoDB 取代的重要原因之一。

下面,配合实例,我们分析 InnoDB 存储引擎在事务支持方面对于隔离性的实现。

隔离性与隔离级别

我们知道事务的四个特性:ACID(AtomicityConsistencyIsolationDurability,即原子性、一致性、隔离性、持久性)。

当数据库上有多个事务同时执行的时候,就可能会出现问题:

  • 脏读(dirty read)
  • 不可重复读(non-repeatable read)
  • 幻读(phantom read)

为了解决这些问题,就有了隔离级别的概念。

在谈隔离级别之前,我们首先要知道,隔离的越严实,效率就会越低。因此很多时候,我们都要在二者之间寻找一个平衡点。

SQL标准的事务隔离级别包括

  • 读未提交read uncommitted):一个事务还没提交时,它做的变更就能被别的事务看到;
  • 读提交read committed):一个事务提交以后,它做的变更才会被其它事务看到;
  • 可重复读repeatable read):一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其它事务也是不可见的;
  • 串行化serializable):对于同一行记录,「写」会加「写锁」,「读」会加「读锁」。当出现读写锁冲突的时候,后访问的事务必须等待前一个事务执行完成,才会继续执行。

在 MySQL 数据库中,默认的事务隔离级别是 RR。

四个案例看懂 MySQL 事务隔离级别

查看隔离级别

MySQL8 之前的查询命令是:

SELECT @@GLOBAL.tx_isolation, @@tx_isolation;

MySQL8 开始查询命令是:

SELECT @@GLOBAL.transaction_isolation, @@transaction_isolation;

一文读懂 MySQL 事务隔离机制_第1张图片
根据上图,可以看到,默认的隔离级别为 REPEATABLE-READ,「全局隔离级别」和「当前会话隔离级别」是相同的。

我们可以通过如下命令修改隔离级别(建议在修改时,仅修改当前 session 隔离级别即可,不用修改全局的隔离级别):

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

上面这条 SQL 表示,仅把当前 session 的数据库隔离级别设置为 READ UNCOMMITTED,设置成功后,再次查询隔离级别,发现当前 session 的隔离级别已经变了,如图:

一文读懂 MySQL 事务隔离机制_第2张图片

注意,这里只是修改了当前 session 的隔离级别,换一个 session 之后,隔离级别又会恢复到默认的隔离级别,如果使用的是 Navicat 的话,不同的查询窗口就对应了不同的 session

READ_UNCOMMITTED

READ UNCOMMITTED 是最低隔离级别,这种隔离级别中存在脏读、不可重复读以及幻读问题

我们通过这个隔离级别,搞懂这三个问题到底是怎么回事。

建表语句:

CREATE TABLE account (
	id INT NOT NULL AUTO_INCREMENT,
	name VARCHAR (50) NOT NULL,
	balance BIGINT NOT NULL,
	PRIMARY KEY (id),
	UNIQUE KEY idx_name (name)
) ENGINE = INNODB

预设两条数据,如下: zhangsanzhaowu 两个用户,两个人的账户各有 1000 块。

在这里插入图片描述

下面通过模拟这两个用户之间的一个转账操作,借此分析这三个问题到底是怎么回事。

脏读

一个事务读到另外一个事务还没有提交的数据,称之为脏读

具体操作如下:

  1. 首先打开两个 SQL 操作窗口,假设分别为 A 和 B,在 A 窗口中输入如下几条 SQL (输入完成后不用执行)
START TRANSACTION;
UPDATE account set balance=balance+100 where name='zhangsan';
UPDATE account set balance=balance-100 where name='zhaowu';
COMMIT;
  1. 在 B 窗口执行如下 SQL,修改默认的事务隔离级别为 READ UNCOMMITTED,如下:
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
  1. 接下来在 B 窗口中输入如下 SQL,输入完成后,首先执行第一行开启事务(注意只需要执行一行即可):
START TRANSACTION;
SELECT * FROM ACCOUNT;
COMMIT;
  1. 接下来执行 A 窗口中的前两条 SQL,即开启事务,给 zhangsan 这个账户添加 100 元。
  2. 进入到 B 窗口,执行 B 窗口的第二条查询 SQL(SELECT * FROM ACCOUNT;),结果如下:

在这里插入图片描述

可以看到,A 窗口中的事务,虽然还未提交,但是 B 窗口中已经可以查询到数据的相关变化了。

这就是脏读问题。

不可重复读

不可重复读是指一个事务先后读取同一条记录,但两次读取的数据不同,称之为不可重复读。

具体操作步骤如下(操作之前先将两个账户的钱都恢复为1000):

  1. 首先打开两个查询窗口 A 和 B ,并且将 B 的数据库事务隔离级别设置为 READ UNCOMMITTED
  2. 在 B 窗口中输入如下 SQL,然后只执行前两条 SQL 开启事务并查询 zhangsan 的账户:
START TRANSACTION;
SELECT * FROM ACCOUNT WHERE NAME='zhangsan';
COMMIT;

在这里插入图片描述

  1. 在 A 窗口中执行如下 SQL,给 zhangsan 这个账户添加 100 块钱,如下:
START TRANSACTION;
update account set balance=balance+100 where name='zhangsan';
COMMIT;
  1. 再次回到 B 窗口,执行 B 窗口的第二条 SQL 查看 zhangsan 的账户,结果如下:

在这里插入图片描述

zhangsan 的账户已经发生了变化,即前后两次查看 zhangsan 账户,结果不一致,这就是不可重复读

不可重复读和脏读的区别在于脏读是看到了其它事务未提交的数据,而不可重复读是看到了其它事务已经提交的数据(由于当前 SQL 也是在事务中,因此有可能并不想看到其它事务已经提交的数据)。

幻读

幻读和不可重复读非常像。幻读指的,一个事务里面,后一个请求看到的比之前相同请求看到的,多了记录出来。 幻读仅专指「新插入的行」。

我来举一个简单例子。

在 A 窗口中输入如下 SQL:

START TRANSACTION;
insert into account(name,balance) values('wangliu',1000);
COMMIT;

然后在 B 窗口输入如下 SQL:

START TRANSACTION;
SELECT * from account;
delete from account where name='wangliu';
COMMIT;

执行步骤如下:

  1. 首先打开两个查询窗口 A 和 B ,并且将 B 的数据库事务隔离级别设置为 READ UNCOMMITTED
  2. 执行 B 窗口的前两行,开启一个事务,同时查询数据库中的数据,此时查询到的数据只有 zhangsanzhaowu
  3. 执行 A 窗口的前两行,向数据库中添加一个名为 wangliu 的用户,注意不用提交事务。
  4. 执行 B 窗口的第二行,由于脏读问题,此时可以查询到 wangliu 这个用户。
  5. 执行 B 窗口的第三行,去删除 name 为 wangliu 的记录,这个时候删除就会出问题,虽然在 B 窗口中可以查询到 wangliu,但是这条记录还没有提交,是因为脏读的原因才看到了,所以是没法删除的。此时就产生了幻觉,明明有个 wangliu,却无法删除。

这就是幻读

看了上面的案例,大家应该明白了脏读不可重复读以及幻读各自的含义了。

READ_COMMITTED

READ UNCOMMITTED 相比,READ COMMITTED 主要解决了脏读的问题,对于不可重复读和幻读则未解决

将事务的隔离级别改为 READ COMMITTED 之后,重复上面关于脏读案例的测试,发现已经不存在脏读问题了;重复上面关于不可重复读案例的测试,发现不可重复读问题依然存在。

上面那个案例不适用于幻读的测试,我们换一个幻读的测试案例。

继续两个窗口 A 和 B,将 B 窗口的隔离级别改为 READ COMMITTED,然后在 A 窗口输入如下测试 SQL:

START TRANSACTION;
insert into account(name,balance) values('wangliu',1000);
COMMIT;

在 B 窗口输入如下测试 SQL:

START TRANSACTION;
SELECT * from account;
insert into account(name,balance) values('wangliu',1000);
COMMIT;

执行步骤如下:

  1. 首先执行 B 窗口的前两行 SQL,开启事务并查询数据,此时查到的只有 zhangsan 和 zhaowu 两个用户。
  2. 执行 A 窗口的前两行 SQL,插入一条记录,但是并不提交事务。
  3. 执行 B 窗口的第二行 SQL,由于现在已经没有了脏读问题,所以此时查不到 A 窗口中添加的数据。
  4. 执行 B 窗口的第三行 SQL,由于 name 字段唯一,因此这里会无法插入。此时就产生幻觉了,明明没有 wangliu 这个用户,却无法插入 wangliu。

REPEATABLE_READ

READ COMMITTED 相比,REPEATABLE READ 进一步解决了不可重复读的问题,但是幻读则未解决。

REPEATABLE READ 中关于幻读的测试和上一小节基本一致,不同的是第二步中执行完插入 SQL 后记得提交事务。

由于 REPEATABLE READ 已经解决了不可重复读,因此第二步即使提交了事务,第三步也查不到已经提交的数据,第四步继续插入就会出错

SERIALIZABLE

SERIALIZABLE 提供了事务之间最大限度的隔离,在这种隔离级别中,事务一个接一个顺序的执行,不会发生脏读、不可重复读以及幻读问题,最安全。

如果设置当前事务隔离级别为 SERIALIZABLE,那么此时开启其它事务时,就会发生阻塞,必须等当前事务提交了,其它事务才能开启成功,因此前面的脏读、不可重复读以及幻读问题这里都不会发生。

事务隔离的实现

在 InnoDB 中事务隔离性是由「锁」来实现的

首先说 READ UNCOMMITTED,它是性能最好,也可以说它是最野蛮的方式,但是它压根儿就不加锁,所以根本谈不上什么隔离效果,可以理解为没有隔离。

再来说 SERIALIZABLE。读的时候加共享锁,也就是其它事务可以并发读,但是不能写。写的时候加排它锁,其它事务不能并发写也不能并发读。

最后说 READ COMMITTEDREPEATABLE READ。这两种隔离级别是比较复杂的,既要允许一定的并发,又想要兼顾的解决问题。

为了实现可重复读,MySQL 采用了 MVCC (多版本并发控制) 的方式。

有关锁和MVCC的分析,我们在后面文章进行详细说明。

这里我们只需要知道,MVCC 只在 READ COMMITTEDREPEATABLE READ 这两个隔离级别下工作。

最后,「幻读」InnoDB 通过引入间隙锁的方式解决。

总结

​本文我们分析了并发访问引发的三个问题:

  • 脏读:脏读指的是读到了其它事务未提交的数据,未提交意味着这些数据可能会回滚,也就是可能最终不会存到数据库中,也就是不存在的数据。读到了不一定最终存在的数据,这就是脏读。
  • 不可重复读:它对比可重复读,不可重复读指的是在同一事务内,不同的时刻读到的同一批数据可能是不一样的,可能会受到其它事务的影响,比如其它事务改了这批数据并提交了。通常针对数据更新(UPDATE)操作
  • 幻读:幻读则是针对数据插入(INSERT)操作来说的

针对这三个问题,SQL 标准定义了四种隔离级别:

  • 读未提交(READ UNCOMMITTED)
  • 读提交 (READ COMMITTED)
  • 可重复读 (REPEATABLE READ)
  • 串行化 (SERIALIZABLE)

从上往下,隔离强度逐渐增强,性能逐渐变差。采用哪种隔离级别要根据系统需求权衡决定,其中,可重复读是 MySQL 的默认级别。

事务隔离其实就是为了解决上面提到的脏读、不可重复读、幻读这几个问题,下面展示了 4 种隔离级别对这三个问题的解决程度。

一文读懂 MySQL 事务隔离机制_第3张图片
最后我们简单介绍了下事务隔离的实现。

事务隔离性主要是由「锁」来实现的,为了解决可重复读,MySQL 采用了 MVCC (多版本并发控制) 的方式。

好了,有关事务隔离机制的就先介绍到这了,我们下篇见。

你可能感兴趣的:(数据库,mysql,隔离机制)