事务隔离级别
引言
事务是一组要么全部执行,要么全部不执行的语句(满足原子性)。事务在定义上必须要满足ACID
四大要素,即Atomicity原子性
、Consistency一致性
、Isolation隔离性
、Durability持久性
。Mysql
中Inoodb
存储引擎是支持事务的,其满足ACID
四大特性。其中,除了隔离性以外的三大特性均由redo log
和undo log
来保证,而隔离性由锁来提供。锁机制可以保证最高程度上的隔离性, 例如串行的依次执行多个事务,从而保证在并发修改下的正确性。但事实上这样的开销太大了。为了在正确性和效率之间折衷,SQL
标准,定义了四种隔离级别供使用者选择。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ UNCOMMITED | 可能发生 | 可能发生 | 可能发生 |
READ COMMITED | 不可能发生 | 可能发生 | 可能发生 |
REPEATABLE READ | 不可能发生 | 不可能发生 | 可能发生 |
SERIALIZABLE | 不可能发生 | 不可能发生 | 不可能发生 |
四种隔离级别从上到下依次提高,隔离级别越低,事务所持有的锁越少,或时间越短。此外,以上的四个隔离级别只是SQL
定义的标准,提供参考。具体的存储引擎在实现时往往有自己的一套标准,比如Inoodb
默认情况下是REPEATABLE READ
的,但实际上得益于间隙锁,在这个级别下Inoodb
也杜绝了幻读现象的发生。下面通过举例的方式来解释什么是脏读、不可重读、幻读。在此之前先建一张示例表,其中id是INT类型的主键。
CREATE TABLE tb1(
id INT,
name VARCHAR(100),
PRIMARY KEY(id)
)Engine=InnoDB CHARSET=utf8;
在这张表里插入一行演示数据
INSERT INTO tb1 VALUES(1, 'Tom');
+----+------+
| id | name |
+----+------+
| 1 | Tom |
+----+------+
脏读
只有READ UNCOMMITED
隔离级别下会发生,如图READ UNCOMMITED
这个名字一样,脏读就是读到了(其他事务)修改但未提交(uncommited
)的脏数据。例如两者同时进行的事务A和事务B,事务B对某个记录执行的修改操作,再没有提交之前就可以被事务A察觉到,那么此时就发生了脏读现象,其时序图如下。
在实验之前我们首先要在每个事物开始之前修改当前会话的隔离级别为READ UNCOMMITED
。
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SELECT @@tx_isolation
+------------------+
| @@tx_isolation |
+------------------+
| READ-UNCOMMITTED |
+------------------+
然后依照上面的执行流程执行SQL语句,可以发现在事务A第一次SELECT的时候,此时主键id=1
数据还未被更改,依然为Tom
SELECT * FROM tb1 WHERE id=1;
+----+------+
| id | name |
+----+------+
| 1 | Tom |
+----+------+
当第二次执行SELECT查询时,此时数据却变为Ben
,且事务B执行更新操作后还未提交。
SELECT * FROM tb1 WHERE id = 1;
+----+------+
| id | name |
+----+------+
| 1 | Ben |
+----+------+
此时发生了脏读现象。
不可重复读
不可重复读,是指在一个事务中,先后两次对同一条记录的读取结果出现不一致的情况,也就是说该记录被其他事务修改过了,注意这里要和脏读区分开,在脏读里,事务B即使未提交(uncommited),也可以影响到事务A的查询结果,而在不可重复读
条件下,未提交的事务B是不会影响到事务A的查询结果的(未发生脏读),只有当是事务B提交后,才会影响到事务A的查询结果,下图给出发生不可重复读
现象的执行时序图,图中黄色部分重点强调了commit的时间点,用以与脏读现象区分。
依照这个时序图来依次执行sql语句,首先,让我们把隔离级别改成READ COMMITED
,这个级别下脏读不会发生,但会出现不可重复读现象。
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT @@tx_isolation;
+----------------+
| @@tx_isolation |
+----------------+
| READ-COMMITTED |
+----------------+
先看下目前tb1
表中的记录
SELECT * FROM tb1;
+----+------+
| id | name |
+----+------+
| 1 | Tom |
+----+------+
事务A开始事务,执行第一次查询语句,然后等待
# 事务A
BEGIN;
SELECT * FROM tb1 WHERE id = 1;
+----+------+
| id | name |
+----+------+
| 1 | Tom |
+----+------+
事务B开始事务,更新id=1
的记录name
字段为ben
,然后提交
# 事务B
BEGIN;
UPDATE tb1 SET name = 'Ben' WHERE id = 1;
COMMIT;
再回到事务A,再执行第二次查询,发现记录已经被已提交
的事务B修改了,发生了不可重复读现象。
# 事务A
SELECT * FROM tb1 WHERE id = 1;
+----+------+
| id | name |
+----+------+
| 1 | Ben |
+----+------+
幻读
幻读是说,在一个事务中,先后两次范围查询,由于另一个事务的插入操作,导致第二次查询结果的记录数比第一次多,凭空多出了一些记录的现象。其执行时序图如下。
幻读
注意接下来的实验中,不能将隔离级别设置为REPEATABLE READ
,因为之前说过,在Innodb
中使用了间隙锁,在这个REPEATABLE READ
级别也不会发生幻读,所以这里还是用READ-COMMITTED
隔离级别做演示。
首先开始事务A,进行第一次查询所有主键值id>=1
的记录,然后等待。
# 事务A
BEGIN;
SELECT * FROM tb1 WHERE id >= 1;
+----+------+
| id | name |
+----+------+
| 1 | Tom |
+----+------+
开始执行事务B,插入id=2 name='Ben'
的记录,并提交
BEGIN;
INSERT INTO tb1 VALUES(2, 'Ben');
COMMIT;
回到事务A,再次查询,发现多出了上次查询中不存在的记录(2, 'Ben')
,发生了幻读现象
SELECT * FROM tb1 WHERE id >= 1;
+----+------+
| id | name |
+----+------+
| 1 | Tom |
| 2 | BEN |
+----+------+