通俗地解释脏读、不可重复读、幻读

1.数据库 事务隔离级别分为四种(级别递减):

Serializable (串行化)

顾名思义,可串行化的,也即并发事务串行执行。很显然,该级别可以避免前面讲到的所有问题:“脏读”、“不可重复读”和“幻读”。代价是处理事务的吞吐量低,严重浪费数据库的性能,因此要慎用此事务隔离级别。

最严格的级别,事务串行执行,资源消耗最大;

下面演示Serializable如何解决这些问题:

1. 小明连接数据库去查询自己本学期的成绩,他设置session(当前连接)的事务隔离级别为Serializable:

1
2
3
4
5
6
7
8
9
10
xiaoming> set session transaction isolation level serializable;
Query OK, 0 rows affected (0.00 sec)

xiaoming> select @@tx_isolation;
+----------------+
| @@tx_isolation |
+----------------+
| SERIALIZABLE |
+----------------+
1 row in set (0.00 sec)

2. 小明开始查询成绩,由于还没有录入,因此没有成绩:

1
2
3
4
5
xiaoming> begin;
Query OK, 0 rows affected (0.00 sec)

xiaoming select * from scores where name = 'xiaoming';
Empty set (0.00 sec)

3. 这时小明的班主任王老师也连接数据库来录入成绩,可是他会卡在插入第一条成绩信息这里, 如下所示,insert语句迟迟不会返回:

1
2
3
4
mr.wang> begin;
Query OK, 0 rows affected (0.00 sec)

mr.wang> insert into scores(name, score) values ('xiaoming', 69);

4. 小明结束本次查询:

1
2
xiaoming> commit;
Query OK, 0 rows affected (0.00 sec)

5. 这时王老师插入第一条成绩才完成:

1
2
mr.wang> insert into scores(name, score) values ('xiaoming', 69);
Query OK, 1 row affected (3.42 sec)

6. 如果小明久久不结束查询,还会导致王老师录入成绩超时:

1
2
xiaoming> insert into scores(name, score) values ('xiaoming', 69);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

从上面的例子我们可以看出,如果一个session设置隔离级别为Serializable时,其执行事务时会阻塞其他并发事务,从上面的错误信息中我们也可以看出应该是通过某种锁来实现的。既然是这样,那么“脏读”、“不可重复读”和“幻读”自然是不可能发生了。

REPEATABLE READ(重复读)

也即在一个事务范围内相同的查询会返回相同的数据,保证了一个事务不会修改已经由另一个事务读取但未提交(回滚)的数据。避免了“脏读取”和“不可重复读取”的情况,但不能避免“幻读”,但是带来了更多的性能损失。
延续上面的栗子:

1. 小明很开心自己考了69分,于是他连接到数据库查询自己的成绩来炫耀给小伙伴,由于Repeatable Read是默认的事务隔离级别,因此这次他不需要进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
xiaoming> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set (0.00 sec)

xiaoming> begin;
Query OK, 0 rows affected (0.00 sec)

xiaoming> select * from scores where name = 'xiaoming';
+----+----------+-------+
| id | name | score |
+----+----------+-------+
| 1 | xiaoming | 69 |
+----+----------+-------+
1 row in set (0.00 sec)

2. 不幸的是,小明的班主任王老师复查试卷后,发现小明的成绩多加了10分,于是他连接到数据库来修改小明的成绩:

1
2
3
4
5
6
7
8
9
mr.wang> begin;
Query OK, 0 rows affected (0.00 sec)

mr.wang> update scores set score = 59 where name = 'xiaoming';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0

mr.wang> commit;
Query OK, 0 rows affected (0.00 sec)

3. 接着小明觉得还不尽兴,于是又查一次,还是69分,可怜的是他不知道自己其实是不及格的:

1
2
3
4
5
6
7
xiaoming> select * from scores where name = 'xiaoming';
+----+----------+-------+
| id | name | score |
+----+----------+-------+
| 1 | xiaoming | 69 |
+----+----------+-------+
1 row in set (0.00 sec)

可见Repeatable Read的确可以解决“不可重复读”的问题,小明在一次事务中2次查询的成绩都是一样的,即使2次查询中王老师修改了成绩。注意我们演示的场景中,王老师是针对一条已有的记录进行了Update, 如果王老师是新增即Insert小明的成绩,那么小明的2次查询的结果还是不一样的,如下所示:

1.首先小明第一次查询, 没有成绩

1
2
xiaoming> select * from scores where name = 'xiaoming';
Empty set (0.00 sec)

2. 然后王老师录入成绩

1
2
3
4
5
6
7
8
mr.wang> begin;
Query OK, 0 rows affected (0.00 sec)

mr.wang> insert into scores(name,score) values ("xiaoming", 59);
Query OK, 1 row affected (0.00 sec)

mr.wang> commit;
Query OK, 0 rows affected (0.00 sec)

3. 最后小明再次查询成绩,这次有了:

1
2
3
4
5
6
7
xiaoming> select * from scores where name = 'xiaoming';
+----+----------+-------+
| id | name | score |
+----+----------+-------+
| 1 | xiaoming | 59 |
+----+----------+-------+
1 row in set (0.00 sec)

通过上述例子,我们可以看出Repeatable Read也是存在以下问题的:

a. 幻读,也即在一次事务范围内多次进行查询,如果其他并发事务中途插入了新的记录,那么之后的查询会读取到这些“幻影”行。

另外,我们也需要注意,不可重复读对应的是修改即Update,幻读对应的是插入即Insert。

READ COMMITTED (提交读)

顾名思义,就是读已提交,一个事务只能看到其他并发的已提交事务所作的修改,是大多数主流数据库的默认事务等级,保证了一个事务不会读到另一个并行事务已修改但未提交的数据,避免了“脏读取”,但不能避免“幻读”和“不可重复读取”。该级别适用于大多数系统。

下面通过例子来演示Read Committed解决“脏读”:

1. 小明连接数据库去查询自己本学期的成绩,他设置session(当前连接)的事务隔离级别为Read Committed:

1
2
3
4
5
6
7
8
9
10
xiaoming> set session transaction isolation level read committed;
Query OK, 0 rows affected (0.00 sec)

xiaoming> select @@tx_isolation;
+----------------+
| @@tx_isolation |
+----------------+
| READ-COMMITTED |
+----------------+
1 row in set (0.00 sec)

2. 就在这个时候,小明的班主任王老师也连接了数据库去登记学生本学期的成绩:

1
2
3
4
5
mr.wang> begin;  
Query OK, 0 rows affected (0.00 sec)

mr.wang> insert into scores(name,score) values ("xiaoming", 59);
Query OK, 1 row affected (0.00 sec)

3. 当王老师还没有提交事务时,小明刚好开始查询自己的成绩,结果他没查到成绩,因为王老师还没提交:

1
2
3
4
5
xiaoming> begin;
Query OK, 0 rows affected (0.00 sec)

xiaoming> select * from scores where name = 'xiaoming';
Empty set (0.00 sec)

4. 小明查成绩之后,王老师发现自己登错了成绩,其实小明考了69分,于是他回滚了当前事务, 并重新录入了小明的正确成绩:

1
2
3
4
5
6
7
8
9
10
11
mr.wang> rollback;
Query OK, 0 rows affected (0.00 sec)

mr.wang> begin;
Query OK, 0 rows affected (0.00 sec)

mr.wang> insert into scores(name,score) values ("xiaoming", 69);
Query OK, 1 row affected (0.00 sec)

mr.wang> commit;
Query OK, 0 rows affected (0.00 sec)

5. 接着,小明又查了一次成绩,这次他查到了,他很开心,因为他及格了

1
2
3
4
5
6
7
xiaoming> select * from scores where name = 'xiaoming';  
+----+----------+-------+
| id | name | score |
+----+----------+-------+
| 1 | xiaoming | 69 |
+----+----------+-------+
1 row in set (0.00 sec)

虽然解决了“脏读”问题,但是Read Committed不能保证在一个事务中每次读都能读到相同的数据,因为在每次读数据之后其他并发事务可能会对刚才读到的数据进行修改。就像上面,小明在一次事务中2次读取成绩返回的结果不一样。这也反映出了Read Committed事务隔离级别存在以下问题:

a. 不可重复读, 也即一个事务范围内两个相同的查询却返回了不同数据

Read Uncommitted(未提交读)

顾名思义,就是读未提交,也就是说事务所作的修改在未提交前,其他并发事务是可以读到的。

事务中的修改,即使没有提交,其他事务也可以看得到,会导致“脏读”、“幻读”和“不可重复读取”。

1. 假设现在有个学生小明连接到数据库去读取自己本学期的成绩,它设置session(当前连接)的事务隔离级别为Read Uncommitted:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
xiaoming> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set (0.00 sec)

xiaoming> set session transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)

xiaoming> select @@tx_isolation;
+------------------+
| @@tx_isolation |
+------------------+
| READ-UNCOMMITTED |
+------------------+
1 row in set (0.00 sec)

2. 就在这个时候,小明的班主任王老师也连接了数据库去登记学生本学期的成绩:

1
2
3
4
5
mr.wang> begin;
Query OK, 0 rows affected (0.00 sec)

mr.wang> insert into scores(name,score) values ("xiaoming", 59);
Query OK, 1 row affected (0.00 sec)

3. 当王老师还没有提交事务时,小明刚好开始查询自己的成绩,结果他查到自己考了59分,他伤心的要死:

1
2
3
4
5
6
7
8
9
10
xiaoming> begin;
Query OK, 0 rows affected (0.00 sec)

xiaoming> select * from scores where name = 'xiaoming';
+----+----------+-------+
| id | name | score |
+----+----------+-------+
| 1 | xiaoming | 59 |
+----+----------+-------+
1 row in set (0.00 sec)

4. 小明查成绩之后,王老师发现自己登错了成绩,其实小明考了69分,于是他回滚了当前事务, 并重新录入了小明的正确成绩:

1
2
3
4
5
6
7
8
9
10
11
mr.wang> rollback;
Query OK, 0 rows affected (0.00 sec)

mr.wang> begin;
Query OK, 0 rows affected (0.00 sec)

mr.wang> insert into scores(name,score) values ("xiaoming", 69);
Query OK, 1 row affected (0.00 sec)

mr.wang> commit;
Query OK, 0 rows affected (0.00 sec)

5. 小明也没有复查成绩,因此整个寒假都过的很不开心,毕竟自己没有”及格”!

通过上述场景,我们发现,Read Uncommitted这个最低的事务隔离级别存在以下这些问题:

a. 允许脏读(dirty reads),就像上面王老师录入的错误成绩(脏数据)被小明读到一样

2.脏读、不可重复读、幻读:

也许有很多读者会对上述隔离级别中提及到的 脏读、不可重复读、幻读 的理解有点吃力,我在这里尝试使用通俗的方式来解释这三种语义:

脏读:

所谓的脏读,其实就是读到了别的事务回滚前的脏数据。比如事务B执行过程中修改了数据X,在未提交前,事务A读取了X,而事务B却回滚了,这样事务A就形成了脏读。

也就是说,当前事务读到的数据是别的事务想要修改成为的但是没有修改成功的数据。

不可重复读:

事务A首先读取了一条数据,然后执行逻辑的时候,事务B将这条数据改变了,然后事务A再次读取的时候,发现数据不匹配了,就是所谓的不可重复读了。

也就是说,当前事务先进行了一次数据读取,然后再次读取到的数据是别的事务修改成功的数据,导致两次读取到的数据不匹配,也就照应了不可重复读的语义。

幻读:

事务A首先根据条件索引得到N条数据,然后事务B改变了这N条数据之外的M条或者增添了M条符合事务A搜索条件的数据,导致事务A再次搜索发现有N+M条数据了,就产生了幻读。

也就是说,当前事务读第一次取到的数据比后来读取到数据条目少。


转载无需注明来源,放弃所有权利