Skip to content

通过例子理解MySQL的隔离级别

我们先快速回顾一遍mysql当中的事务隔离级别。

事务隔离级别概述

MySQL的事务隔离级别有四种,分别是:

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

要想了解怎么做的,我们就要知道没有他们可能导致的问题,有:

  • 脏读
  • 不可重复读
  • 幻读

并发问题详解与实例

脏读

假设我们有一个accounts表:

iduser_namebalance
1zhangsan1000

在读未提交级别下:

事务A开始,执行:

sql
START TRANSACTION;
UPDATE accounts SET balance = balance - 200 WHERE id = 1;

这个时候balance变成800了,但是还没有提交。

这个时候事务B开始,继续更新位置,他会认为这里A已经进行修改了,也就是800-200 = 600:

sql
USE test_dirty_read;
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

START TRANSACTION;
SELECT * FROM accounts WHERE id = 1;
UPDATE accounts SET balance = balance - 200 WHERE id = 1;
COMMIT;

这个时候balance变成了600,然后事务A发生了回滚,也就是说在事务B中读取到的是不正确的值800,他没有确定要被更改,这就是脏读。

不可重复读

我们在读已提交的隔离级别下做测试。

先新建表:

idnameage
1张三25

在第一个会话当中:

sql
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT age FROM users WHERE id = 1;

在第二个会话当中修改age为26并提交:

sql
START TRANSACTION;
UPDATE users SET age = 26 WHERE id = 1;
COMMIT;

再次在会话A中进行查询:

sql
SELECT age FROM users WHERE id = 1;

结果是26,我们无法读取到事务开始时的25了,这就是不可重复读,寓意就是在同一个事务中多次读取同一行数据,结果可能会不一样。

幻读

不可重复读是对于单条记录而言的,而幻读是对于多条记录而言,对于某一个范围内的数据而言的。

我们先插入数据:

idnameprice
1电脑6000.00
2手机4000.00

在第一个会话当中:

sql
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

START TRANSACTION;

SELECT COUNT(*) FROM products WHERE price < 5000;

结果为1,因为只有手机。

在会话2中:

sql
START TRANSACTION;

INSERT INTO products (name, price) VALUES ('鼠标', 100.00);
COMMIT;

回到会话1中再查询这个范围的数据,会发现count的值为2。

这样我们就把所有的隔离级别可能出现的问题都介绍了一遍,也就是说明白了是什么。

隔离级别对比

隔离级别脏读不可重复读幻读并发性能主要实现机制(InnoDB)
读未提交允许允许允许最高无锁,直接读最新数据
读已提交避免允许允许较高每次读生成新 ReadView,写操作加行级排他锁
可重复读避免避免避免中等事务开始时生成 ReadView,结合间隙锁/临键锁防止幻读
串行化避免避免避免最低强制串行执行,读加共享锁,写加排他锁

注意,这些所谓的问题,都是针对于某一个事务而言的:

  • 脏读的意思是,对于这个事务,他读取到的数值可能是别人修改过的。
  • 不可重复读的意思是,对于这个事务,他在同一个事务中多次读取同一行数据,结果可能会不一样。
  • 幻读的意思是,对于这个事务,他在同一个事务中多次读取某个范围内的数据,结果可能会不一样。

对于数据库来说,没有这些问题意味着,从事务开始的时候数据是什么样子的,无论外部做任何修改,当前事务读取到的值都是这个开始时的值,无论是范围还是单行。

问题产生的根本原因

这里可能会有困惑,这么看脏读和不可重复读没什么区别啊。

  • 脏读是在对方事务未提交的情况下的。也就是说无论对方提交没提交,当前事务读取到的值都是不确定的。
  • 不可重复读是对方事务提交了之后,当前事务读取到的值可能会不一样。

接下来我们总结为什么会有这些问题:

  • 脏读(READ UNCOMMITTED):SELECT 毫不关心锁和提交状态,直接读最新数据。
  • 不可重复读(READ COMMITTED):SELECT 只读已提交的数据,但每次 SELECT 都会获取最新的已提交数据版本。
  • 幻读(READ COMMITTED):SELECT 只读已提交数据,但由于查询的是一个范围,当其他事务在这个范围内插入或删除数据并提交后,下次查询时结果集的行数会发生变化。

InnoDB的实现机制

MVCC与ReadView机制

为了解决脏读问题,提供了读已提交,他在每一次select的时候生成一个快照,也就是ReadView,这个ReadView包含了当前所有未提交的事务表,如果某行的事务为未提交状态,那么就不去读取这一行。这样别的未提交修改就不会影响到当前事务的读取。

为了解决不可重复读问题,提供了可重复读的隔离级别。在事务一开始就会生成一个ReadView,而不是在每次select的时候生成,这样就可以保证在这个事务中读取到的值都是一致的。也就是在这个事务开始时的快照。

间隙锁与临键锁机制

有一个问题就是幻读,innodb是怎么解决的呢?

InnoDB使用间隙锁和临键锁解决这个问题。

假设我们有以下数据:

idname
1电脑
5手机
10耳机
15键盘
20鼠标
25显示器

然后我们的查询是:

sql
SELECT ... WHERE id > 6 AND id < 12

会找到第一条大于6的记录10,继续往后直到找到第一条大于等于12的记录15,锁住查询到的10,锁住小于等于6的第一个索引,也就是5。锁住大于等于12的第一个索引也就是15,也就是锁住了(5,10]和[10,15)这两个范围。

临键锁并不会根据具体的范围来锁,而是锁住了查询到的记录和它前后相邻的索引值。

特殊情况:当前读vs快照读

那么InnoDB的可重复读真的解决了幻读吗?

MySQL官方对于幻读的定义是,在一个事务中进行多次查询,会查询到不一样的数据就算是幻读。

假设我们的数据是:

idnamestatus
1joe1
2jill1

会话1执行select会查询到这两个 ↓ 会话2更新插入(3 bob 1)并提交 ↓ 会话1再次执行select还是会查询到一开始查询的两个。

但是如果这个时候会话1又执行了:

sql
UPDATE users SET name='huashen'

最后就会显示:

idnamestatus
1huashen1
2huashen1
3huashen1

第三条数据被会话2插入了,在会话1中被查到了。

这是违背MySQL官方认定的不幻读的定义的。

为什么会出现这种情况?

这是因为在 UPDATE 的时候,它不是一个快照读,而是一个当前读(Current Read)。UPDATE 操作不会使用旧的 ReadView,而是会去读取最新的、已提交的数据,也就是说,它会看到被会话 2 插入的数据。UPDATE 会对这些数据加锁,并进行修改,因此 id=3 这条数据被它修改了。

在这个 UPDATE 操作之后,你再执行 SELECT 时,InnoDB 仍然使用最初的 ReadView。但是,根据 MVCC 的规则,一个事务可以读取自己修改过的数据。由于 UPDATE 已经将 id=3 的记录修改成了当前事务的版本,所以 SELECT 就可以看到这条数据了。

ReadView的本质

ReadView本质上是一个快照范围,他规定了只能读到某一个范围内的数据,然后通过这个规定,可以知道哪些数据是可以读取的,哪些是不能被读取的。可以理解为存了多个版本的数据,通过版本号来区分哪些数据是可以读取的,哪些是不能被读取的。

上述问题在于因为update操作,所以这个修改后的数据也加入到了当前的事务快照中,所以你就可以读到这个事务2修改的数据了。

操作流程总结: update操作是当前读 → 读到最新数据 → 更新数据并加入到当前事务当中 → 可以在下次select中读取。