Shellbye.github.io
Shellbye.github.io copied to clipboard
MySQL事务的四种隔离级别总结笔记
MySQL
事务的四大特性(原子性Atomicity
/一致性Consistency
/隔离性Isolation
/持久性Durability
)基本是人尽皆知,但是关于其事务隔离级别这一概念,我也是在最近读书时才回想起来,当年的数据库课程似乎隐隐的听到过,所以这里结合一些资料对四种隔离基本做一个简单的总结。
这个隔离的级别当然还是要从四大特性中的隔离性(Isolation
)开始说起,隔离的定义是事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。理想很美满,现实很骨感,虽然隔离性的定义非常清楚也没有啥问题,但是考虑到效率问题,实际应用中,隔离是分为以下四级的:
- READ-UNCOMMITTED
- READ-COMMITTED
- REPEATABLE-READ
- 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 |
参考资料
- https://www.cnblogs.com/kismetv/p/10331633.html
- https://www.cnblogs.com/zhoujinyi/p/3437475.html
- http://www.cnblogs.com/zhoujinyi/p/3435982.html
- http://hedengcheng.com/?p=771
- https://www.percona.com/blog/2015/01/14/mysql-performance-implications-of-innodb-isolation-modes/