一直以来,对事务的隔离级别的理解一直有点模糊,今天借此文对这一块的知识点进行梳理,并配合相关实验进行说明,以期加深理解。知识点部分的文字采集于网络,文末附上参考链接,感谢大神的贡献!
在说明举例事务隔离级别之前,我们需要搞清楚为什么需要存在不同的隔离级别。
我们在数据库涉及到事务的操作中,往往需要在数据的正确性和效率之间,根据实际业务需求的特点,求得一个平衡。
大多数数据库为我们提供了不同的隔离级别,就是为了方便我们从数据库层面来实现该平衡。隔离级别越高,读数的正确性越高,但是数据库操作的效率会越低,因为为了实现较高的隔离级别,需要采用各种锁来保证不同事务之间的隔离性。反之隔离级别越低,读数的正确性越得不到保障,但是效率会越高。
下面说明在不同的事务隔离情况下,我们读取数据时可能会存在的异常情况:
(1)更新丢失
两个事务都同时更新一行数据,一个事务对数据的更新把另一个事务对数据的更新覆盖了。这是因为系统没有执行任何的锁操作,因此并发并没有被隔离开来。
(2)脏读
一个事务读取到了另一事务未提交的数据操作结果。这是相当危险的,因为很可能所有的操作都被回滚。
(3)不可重复读
不可重复读(Non-repeatable Reads):又叫虚读,一个事务对同一行数据重复读取两次,但是却得到了不同的结果。
(4)幻读
幻读是说事务T1读取某一数据后,事务T2插入了数据,当事务T1再次读取时,虽然读取结果和第一次读取的结果一样,但是如果我们进行插入的时候(与事务T2的插入的数据主键相同),发现数据已经存在,造成无法插入。注意,幻读并不是说事务T1在事务T2插入数据并提交的前后两次读取结果不一致,事实上,两次读取的结果是一致的,后面我们将用实验来说明。
这里说点题外话,个人认为幻读的这个翻译不够准确,容易造成误解。所谓幻,应该是指事实上不存在的东西人认为其存在谓之幻。但是幻读的现象恰好相反,是事实上已经存在的东西,查询结果却不存在,并且因为查询结果与真实情况不一致而影响了后续的插入动作。
上述内容直接阅读会略显抽象,稍后的实验中,我们会通过实验来一一模拟在不同的事务隔离级别情况下,上述异常情况是否会出现。
这里还是放一下四种事务隔离级别:
读未提交(Read Uncommitted):只处理更新丢失。如果一个事务已经开始写数据,则不允许其他事务同时进行写操作,但允许其他事务读此行数据。可通过“排他写锁”实现。
读提交(Read Committed):处理更新丢失、脏读。读取数据的事务允许其他事务继续访问改行数据,但是未提交的写事务将会禁止其他事务访问改行。可通过“瞬间共享读锁”和“排他写锁”实现。
可重复读取(Repeatable Read):处理更新丢失、脏读和不可重复读取。读取数据的事务将会禁止写事务,但允许读事务,写事务则禁止任何其他事务。可通过“共享读锁”和“排他写锁”实现。
序列化(Serializable):提供严格的事务隔离。要求失去序列化执行,事务只能一个接一个地执行,不能并发执行。仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。
接下来准备开始我们的实验。实验前,我们需要准备好一个mysql服务端,并有两个可以访问该服务端的客户端。
1. 不开启事务
在任一客户端执行以下命令,创建好我们实验要用的表格,并插入相关数据:
DROP TABLE test;
CREATE TABLE test(id int,name varchar(20),PRIMARY KEY(id)) engine MyISAM; --使用该存储引擎,不支持事务
INSERT INTO test VALUES(1,'刘备');
INSERT INTO test VALUES(2,'关羽');
INSERT INTO test VALUES(3,'张飞');
在客户端1和客户端2分别执行以下命令,设置SESSION的事务隔离级别为读未提交(Read Uncommitted),并关闭事务自动提交。
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; --设置隔离级别为读未提交
SET autocommit = 0; --关闭自动提交
注意,两个客户端都要执行上述命令进行相关设置。
1.1 模拟更新丢失
在客户端1执行
BEGIN;
UPDATE test SET name='张翼德' WHERE id=3;
此时我们去客户端2执行
BEGIN;
UPDATE test SET name='张飞' WHERE id=3;
然后在客户端1查询
SELECT * FROM test;
+----+--------+
| id | name |
+----+--------+
| 2 | 关羽 |
| 3 | 张飞 |
| 1 | 刘备 |
+----+--------+
可以看到,我们在客户端1执行的更新动作直接被客户端2执行的更新动作覆盖。此即更新丢失。
因为没有开启事务控制,在并发情况下不同线程对数据库的操作会相互干扰,最终导致操作结果不可预测。
在不开启事务的情况下,更新丢失、脏读、幻读等异常情况都会出现,为节约篇幅,对脏读等异常的模拟我们放到下一个
事务隔离级别为读未提交的部分来进行。
2、事务隔离级别为读未提交
在任一客户端执行如下命令,创建好表格并插入数据:
DROP TABLE test;
CREATE TABLE test(id int,name varchar(20),PRIMARY KEY(id)) engine InnoDB; --使用该存储引擎,支持事务
INSERT INTO test VALUES(1,'刘备');
INSERT INTO test VALUES(2,'关羽');
INSERT INTO test VALUES(3,'张飞');
在客户端1和客户端2分别执行以下命令,设置SESSION的事务隔离级别为读未提交(Read Uncommitted),并关闭事务自动提交。
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; --设置隔离级别为读未提交
SET autocommit = 0; --关闭自动提交
注意,两个客户端都要执行上述命令进行相关设置。
2.1 模拟更新丢失
在客户端1,执行如下命令,开启事务,对数据进行修改但不提交:
BEGIN; --开启事务
UPDATE test SET name='张翼德' WHERE id=3;
在客户端2,执行如下命令,开启事务,对数据进行修改
BEGIN; --开启事务
UPDATE test SET name='张飞' WHERE id=3;
可以发现流程会阻塞一段时间后报错。
因为客户端1对开启了事务对这条记录进行操作后并未提交,所以在客户端2开启其他事务来对同一条记录进行操作时,会发现该记录会被锁住。
对比1.1模拟不开启事务情况下的更新丢失,我们可以发现开启事务后,即使事务隔离级别设置为最低的读未提交,也可以避免更新丢失的情况。
在客户端1执行ROLLBACK,对操作进行回滚。
ROLLBACK;
2.2 模拟脏读
脏读根据前面的介绍,即读取到了未提交的数据,未提交的数据随时有被回滚的可能,因此出现脏读是不安全的。下面通过实验对脏读进行模拟。
在客户端1,执行如下命令,开启事务,对数据进行修改但不提交:
BEGIN; --开启事务
UPDATE test SET name='张翼德' WHERE id=3;
在客户端2,我们执行下面的语句进行查询:
SELECT * FROM test;
+----+-----------+
| id | name |
+----+-----------+
| 1 | 刘备 |
| 2 | 关羽 |
| 3 | 张翼德 |
+----+-----------+
根据上述查询结果,可以发现我们在客户端1执行的修改动作还没提交,但是在客户端2我们已经可以读到未提交的修改。
上述操作模拟在隔离级别设置为读未提交的情况下修改数据发生脏读的情况,增删数据同样也会发生脏读,大家可以自行测试。
在该隔离级别下,也会发生虚读、幻读等其他异常情况,为节约篇幅,对虚读、幻读的模拟我们放到下一个隔离级别的内容汇总进行模拟。
在客户端1,对数据进行回滚:
ROLLBACK;
3、事务隔离级别为读提交
在客户端1和客户端2分别执行以下命令,设置SESSION的事务隔离级别为读提交(Read Committed),并关闭事务自动提交。
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; --设置隔离级别为读提交
SET autocommit = 0; --关闭自动提交
3.1 模拟脏读
在客户端1,执行如下命令,开启事务,对数据进行修改但不提交:
BEGIN; --开启事务
UPDATE test SET name='张翼德' WHERE id=3;
在客户端2,我们执行下面的语句进行查询:
SELECT * FROM test;
+----+--------+
| id | name |
+----+--------+
| 1 | 刘备 |
| 2 | 关羽 |
| 3 | 张飞 |
+----+--------+
可以发现,id为3的数据name还是‘张飞’,没有读到未提交的数据。因此可以看出,当事务的隔离级别设置为读提交时,可以有效避免出现脏读。
在客户端1,对数据进行回滚:
ROLLBACK;
3.2 模拟虚读
虚读:事务T1读取某一数据后,事务T2对其做了修改,当事务T1再次读取该数据时得到与前一次不同的值。
虚读与幻读的区别在于,前者针对修改,后者针对增删。
在客户端1,开启事务,读取数据:
BEGIN; --开启事务
SELECT * FROM test;
+----+--------+
| id | name |
+----+--------+
| 1 | 刘备 |
| 2 | 关羽 |
| 3 | 张飞 |
+----+--------+
在客户端2,开启事务,修改数据并提交:
BEGIN; --开启事务
UPDATE test SET name='张翼德' WHERE id=3;
COMMIT;
在客户端1,开启事务,再次读取数据:
SELECT * FROM test;
+----+-----------+
| id | name |
+----+-----------+
| 1 | 刘备 |
| 2 | 关羽 |
| 3 | 张翼德 |
+----+-----------+
通过上面的实验可以发现,我们在同一个事务中两次读取的结果不一致,即出现了虚读,说明在读提交的隔离级别下,会出现虚读的情况。幻读的模拟我们放到后面进行。
在客户端2,我们修改还原数据并提交:
BEGIN; --开启事务
UPDATE test SET name='张飞' WHERE id=3;
COMMIT;
4 事务隔离级别为可重复读
在客户端1和客户端2分别执行以下命令,设置SESSION的事务隔离级别为可重复读(Repeatable Read),并关闭事务自动提交。
SET SESSION TRANSACTION ISOLATION LEVEL Repeatable Read; --设置隔离级别为可重复读
SET autocommit = 0; --关闭自动提交
4.1 模拟虚读
在客户端1,开启事务,读取数据:
BEGIN; --开启事务
SELECT * FROM test;
+----+--------+
| id | name |
+----+--------+
| 1 | 刘备 |
| 2 | 关羽 |
| 3 | 张飞 |
+----+--------+
在客户端2,开启事务,修改数据并提交:
BEGIN; --开启事务
UPDATE test SET name='张翼德' WHERE id=3;
COMMIT;
在客户端1,开启事务,再次读取数据:
SELECT * FROM test;
+----+--------+
| id | name |
+----+--------+
| 1 | 刘备 |
| 2 | 关羽 |
| 3 | 张飞 |
+----+--------+
可以发现,在客户端1两次读取的结果一样,即使在两次读取的间隙我们在客户端2修改并提交了数据。因此可以看出,在可重复读的隔离级别下,可以避免虚读的出现。
在客户端2,我们修改还原数据并提交,方便进行下一次实验:
BEGIN; --开启事务
UPDATE test SET name='张飞' WHERE id=3;
COMMIT;
4.2 模拟幻读
在客户端1,开启事务,读取数据:
BEGIN; --开启事务
SELECT * FROM test;
+----+--------+
| id | name |
+----+--------+
| 1 | 刘备 |
| 2 | 关羽 |
| 3 | 张飞 |
+----+--------+
在客户端2,开启事务,插入一条数据并提交(注意和实验3.1的区别之处):
BEGIN; --开启事务
INSERT INTO test VALUES(4,'赵云');
COMMIT;
在客户端1,我们再次读取:
SELECT * FROM test;
+----+--------+
| id | name |
+----+--------+
| 1 | 刘备 |
| 2 | 关羽 |
| 3 | 张飞 |
+----+--------+
我们我们读取到的结果和客户端2插入数据签一模一样。尽管如此,这并不是说在这种隔离级别下,我们就可以认为没有发生幻读的情况,我们将用以下的例子继续说明,实际已经发生了幻读。
在客户端1,我们插入数据:
INSERT INTO test VALUES(4,'赵云');
ERROR 1062 (23000): Duplicate entry '4' for key 'PRIMARY'
可以发现会报错,主键重复。这个现象,才用以说明幻读。查询返回结果ID为4的记录不存在,但是当我们进行插入操作的时候发现该记录其实已经存在了。
在客户端2,还原数据并提交:
BEGIN; --开启事务
DELETE FROM test WHERE id=4;
COMMIT;
5 事务隔离级别为序列化
在客户端1和客户端2分别执行以下命令,设置SESSION的事务隔离级别为序列化(Serializable),并关闭事务自动提交。
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE; --设置隔离级别为可序列化
SET autocommit = 0; --关闭自动提交
4.1 模拟幻读
幻读,在通一次事务中两次读取的记录条数不一样。下面我们开始模拟幻读的情况。
在客户端1,开启事务,读取数据:
BEGIN; --开启事务
SELECT * FROM test;
+----+--------+
| id | name |
+----+--------+
| 1 | 刘备 |
| 2 | 关羽 |
| 3 | 张飞 |
+----+--------+
在客户端2,开启事务,插入一条数据并提交:
BEGIN; --开启事务
INSERT INTO test VALUES(4,'赵云');
COMMIT;
我们可以发现此时无法提交。
说明在SERIALIZABLE的隔离级别下,事务T1开启之后会锁住该表,在提交之前其他事务不能对该表进行操作。
从这里我们可以看到序列化和可重复读的隔离级别之间的区别。
以上,我们在不同的事务隔离级别的情况下模拟了脏读、虚读、幻读等异常情况。希望可以帮助加深对事务隔离级别的理解。
参考链接:https://blog.csdn.net/tolcf/article/details/49283575