Shellbye.github.io icon indicating copy to clipboard operation
Shellbye.github.io copied to clipboard

MySQL事务的四种隔离级别总结笔记

Open Shellbye opened this issue 5 years ago • 0 comments

MySQL事务的四大特性(原子性Atomicity/一致性Consistency/隔离性Isolation/持久性Durability)基本是人尽皆知,但是关于其事务隔离级别这一概念,我也是在最近读书时才回想起来,当年的数据库课程似乎隐隐的听到过,所以这里结合一些资料对四种隔离基本做一个简单的总结。

这个隔离的级别当然还是要从四大特性中的隔离性(Isolation)开始说起,隔离的定义是事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。理想很美满,现实很骨感,虽然隔离性的定义非常清楚也没有啥问题,但是考虑到效率问题,实际应用中,隔离是分为以下四级的:

  1. READ-UNCOMMITTED
  2. READ-COMMITTED
  3. REPEATABLE-READ
  4. SERIALIZABLE

下面我们分别详细的看一下各个级别的定义、表现和问题,首先,我们先定义一个我们自己的测试表

create table tb
(
    my_id   int auto_increment primary key,
    my_name varchar(255) not null
) engine = innodb;

1. READ-UNCOMMITTED

根据字面意思理解,这种隔离级别,允许读取其他事务中还没有递交的数据,比如如下代码,我们打开一个新的session A,设置隔离级别,并开始事务,然后读取表tb,为空,一切正常

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

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from tb;
Empty set (0.00 sec)

然后这个时候,我们在新开一个session B,打开新的事务,插入一条数据,但没有递交

// sessionB
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into tb (my_name) value ("name1");
Query OK, 1 row affected (0.02 sec)

此时再回到session A,我们发现没有递交(UNCOMMITTED )的数据被读出来了

// sessionA
mysql> select * from tb;
+-------+---------+
| my_id | my_name |
+-------+---------+
|     1 | name1   |
+-------+---------+
1 row in set (0.00 sec)

这个时候我们在 session B中执行回滚,

// sessionB
mysql> rollback;
Query OK, 0 rows affected (0.02 sec)

然后在session A中,发现数据又没了

// sessionA
mysql> select * from tb;
Empty set (0.01 sec)

以上这种现象,就叫做读脏(Dirty Read),即读取了无效数据。

2. READ-COMMITTED

既然上面读取未递交的数据有读脏风险,那我们就只读取已经递交的数据就好了,READ-COMMITTED就是这样一种隔离级别。但是这种隔离级别也有一个问题,就是在一个事务中,同一条语句,两次的执行结果是不一样的,比如如下语句,我们打开一个新的session A,设置隔离级别,并开始事务,然后读取表tb,为空,一切正常

// sessionA
mysql> set session transaction isolation level read committed;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from tb;
Empty set (0.00 sec)

然后我们在新建一个session B,插入一条数据并递交

// sessionB
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into tb (my_name) value ("name1");
Query OK, 1 row affected (0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.01 sec)

这个时候回到session A,再执行同样的语句,发现有数据了

// sessionA
mysql> select * from tb;
+-------+---------+
| my_id | my_name |
+-------+---------+
|     2 | name1   |
+-------+---------+
1 row in set (0.00 sec)

这样看起来符合直觉,但是其实是违背了“彼此隔离”的思想,因为 B 对 A 的影响,导致 A 里面出现了不可重复读(NonRepeatable Read)的问题,即同一个语句的读取,两次的内容不一致。

3. REPEATABLE-READ

REPEATABLE-READ可以解决两次读取不一致的问题,它也是MySQL默认的隔离级别。这里需要强调一下,这个可重复读(REPEATABLE-READ)这个概念,是有两种层面的理解,一种是SQL标准,一种是MySQL的实现,这两者之间的区别在于能否解决幻读的问题

3.1 幻读(Phantom Read

至于什么是幻读,我们通过下面的例子来解释一下,首先把隔离级别设置为REPEATABLE-READ,然后读取一下表,发现没有内容

// sessionA
mysql> set session transaction isolation level repeatable read;
Query OK, 0 rows affected (0.00 sec)

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from tb;
Empty set (0.00 sec)

这时我们新开一个session B,插入一条数据,

// sessionB
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into tb (my_name) value ("name1");
Query OK, 1 row affected (0.03 sec)

此时我们回到session A,重复执行读取操作,发现没有数据,这里是符合REPEATABLE-READ的设定的(也符合READ-COMMITTED,因为此时session B还没有递交)

// sessionA
mysql> select * from tb;
Empty set (0.00 sec)

然后我们在回到session B,递交事务

// sessionB
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

接着回到session A,重复执行读取操作,发现依然没有数据,这是就符合READ-COMMITTED了,而是纯粹的REPEATABLE-READ,这是完全符合SQL标准的定义的,也是符合我们对“隔离”的理解的

// sessionA
mysql> select * from tb;
Empty set (0.00 sec)

但是,这个时候session A中要插入以下数据时,就出问题了

// sessionA
mysql> insert into tb (my_id, my_name) value (3, "name2");
ERROR 1062 (23000): Duplicate entry '3' for key 'PRIMARY'
mysql> select * from tb;
Empty set (0.00 sec)

这时session A的用户就觉得有点闹鬼了,我读取时,你告诉我没数据,我插入时,你又说重复了,是我出现幻觉了吗?这种现象,就叫做幻读(Phantom Read)。那么解决幻读的问题呢?这就又引出了新的概念。

3.2 快照读 (snapshot read)与当前读 (current read)

MySQL中的并发读,在Multi-Version Concurrency Control的控制下,分为两类

快照读,读取的是记录的可见版本 (有可能是历史版本),不用加锁。当前读,读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。

我们上面之所以遇到插入报错,就是我们读取时,是用的“快照读”,没有加锁,所以导致session B成功插入一条数据干扰到了我们,并没有做到真正的“隔离”。那么我们应该怎么进行“当前读”呢?看下面的例子,在session A中进行以下操作

// sessionA
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from tb for update;
Empty set (0.00 sec)

注意select语句最后的for update,这是一个非常明确的信号,告诉MySQL,我这个读,不是一个简单的读(“快照读”),我接下来要在我读的基础上进行写操作(“当前读”)。这个读语句,对其读取的范围,加上了S(共享锁/读锁),此时是不在允许其他事务进行写操作的,比如此时在session B中进行如下操作时,就会遇到问题

// sessionB
mysql> insert into tb (my_name) value ("name1");
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

因为session A对整张表都加了S锁,所以导致无法进行写操作,当然这是一个比较极端的例子,实际上MySQL的加锁机制是非常高效的,想要了解详细的信息,可以参考这篇很牛的博客

4. SERIALIZABLE

串行是最安全,也是效率最低的,一般都不会用到。

总结

概括起来讲,就是在MySQL中,如果隔离做的不好,就会产生读脏(Dirty Read)、不可重复读(NonRepeatable Read)和幻读(Phantom Read)的现象,于是,MySQL通过四种隔离级别,层层递进的解决了以上三种现象,见下表

隔离级别 读脏
Dirty Read
不可重复读
NonRepeatable Read
幻读
Phantom Read
READ-UNCOMMITTED YES YES YES
READ-COMMITTED NO YES YES
REPEATABLE-READ NO NO YES(SQL)
NO(MySQL)
SERIALIZABLE NO NO NO

参考资料

  1. https://www.cnblogs.com/kismetv/p/10331633.html
  2. https://www.cnblogs.com/zhoujinyi/p/3437475.html
  3. http://www.cnblogs.com/zhoujinyi/p/3435982.html
  4. http://hedengcheng.com/?p=771
  5. https://www.percona.com/blog/2015/01/14/mysql-performance-implications-of-innodb-isolation-modes/

Shellbye avatar May 20 '19 08:05 Shellbye