谈谈数据库隔离级别
1.隔离级别介绍
隔离级别并不是某个SQL数据库所特有的,而所有SQL数据库都要实现的一种并发事务隔离机制。隔离性其实比想象的要复杂。在SQL标准中定义了四种隔离级别,每一种隔离级别都规定了一个事务中所作的修改,哪些在事务内和事务间是可见的,哪些是不可见的。较低的级别的隔离通常可以执行更高的并发,系统的开销也更低,然而数据的改变在事务间几乎是透明,也更容易引发各种无法预估的问题。下面简单介绍下四种隔离级别:
Read Uncommitted(未提交读)
在Read Uncommitted级别,事务的修改,即使没有提交,对其他事务也是可见的。事务可以读取其它事务未提交的数据,这也被称为脏读(Dirty Read)。这个级别会导致很多问题,从性能上说,Read Uncommitted不会比其他级别好太多,但缺乏其他级别的很多好处。除非真的有非常必要的理由,在实际应用中一般很少使用。
Read Committed(已提交读)
大多数数据库的默认隔离级别都是Read Committed(但MySQL 不是)。Read Committed满足隔离性的简单定义:一个事务开始时,只能看见已经提交的事务所作的修改。换句话说,一个事务从开始知道提交之前,所作的任何修改对其它事务是不可见的。这个级别有时候也叫做不可重复读(Nonrepeatable Read),因为事务可以读到另一个事务提交的数据,可能存在先后两次读取到的数据不一致(中间存在另一个事务提交的数据),执行两次同样的查询,得到的结果不一致。
Repeatable Read(可重复读,MySQL默认隔离级别)
Repeatable Read解决了不可重复读的问题。该级别保证了在同一个事务中多次读取同样的记录的结果是一致的。但是理论上,可重复读隔离级别还是无法解决另外一个幻读问题。所谓幻读,指的是当某个事务在读取某个范围内的记录时,另一个事务又在这个范围内插入了新记录,当之前的事务再次读取该范围的记录,会产生幻行。也就是说可重复读只会在修改事务有效,比如一个事务先后读取同一个范围的记录,而在这中间另一个事务对某一条记录做了修改,当前事务两次读取到的结果是一样的,但是如果是新增数据就会产生幻读的现象。为了解决在可重复读级别下发生的幻读的问题,MySQL的InnoDB和XtraDB存储引擎通过多版本并发控制机制(MVCC)解决了幻读的问题。
Serializable(可串化读)
Serializable是最高的隔离级别。它通过强制事务串行执行,避免幻读问题。简单来说Serializable会在读取的每一行上加锁,所以可能导致大量的超时和锁竞争问题。实际开发中也很少用到这个隔离级别,只有在非常需要保证数据的一致性而且可以接受没有并发的情况下,才考虑采用该级别。
2.隔离级别演示
以mysql演示前三个隔离级别(串读个人觉得没啥必要)。以表user(id,name,salary)为例。
Read Uncommitted(未提交读)演示
设置隔离级别为Read Uncommitted,并开启两个事务A,B。事务A先开启事务并查询user,数据为空。
mysql> set session transaction isolation level read uncommitted; --设置隔离级别 Query OK, 0 rows affected mysql> start transaction; --事务A Query OK, 0 rows affected mysql> select * from user;--查询user表,数据为空 Empty set
再开启事务B,新增一条记录不提交。
mysql> set session transaction isolation level read uncommitted; --设置隔离级别 Query OK, 0 rows affected mysql> start transaction; --事务B Query OK, 0 rows affected mysql> insert into user(name,salary) values ('jack',1000); --插入一条数据未提交 Query OK, 1 row affected mysql>
这时事务A再查user表能查到数据,查到B未提交的数据
mysql> set session transaction isolation level read uncommitted;
Query OK, 0 rows affected
mysql> start transaction;
Query OK, 0 rows affected
mysql> select * from user;
Empty set
mysql> select * from user; --事务A再查询,能查到数据
+----+------+--------+
| id | name | salary |
+----+------+--------+
| 1 | jack | 1000 |
+----+------+--------+
1 row in set
Read Committed(已提交读)演示
设置隔离级别为Read Committed,并先后开启三个事务A,B,C。事务A先开启查询user表,查询到一条数据。
mysql> set session transaction isolation level read committed; Query OK, 0 rows affected (0.01 sec) mysql> start transaction; Query OK, 0 rows affected (0.01 sec) mysql> select * from user; +----+------+--------+ | id | name | salary | +----+------+--------+ | 1 | jack | 1000 | +----+------+--------+ 1 row in set (0.02 sec) mysql>
再开启B事务,新增一条数据不提交。
mysql> set session transaction isolation level read committed; Query OK, 0 rows affected (0.01 sec) mysql> start transaction; Query OK, 0 rows affected (0.01 sec) mysql> insert into user(name,salary) values ('lucy',2000); Query OK, 1 row affected (0.01 sec) mysql>
再回到A事务,查询user表,还是一条数据
mysql> set session transaction isolation level read committed; Query OK, 0 rows affected (0.01 sec) mysql> start transaction; Query OK, 0 rows affected (0.01 sec) mysql> select * from user; +----+------+--------+ | id | name | salary | +----+------+--------+ | 1 | jack | 1000 | +----+------+--------+ 1 row in set (0.02 sec) mysql> select * from user; +----+------+--------+ | id | name | salary | +----+------+--------+ | 1 | jack | 1000 | +----+------+--------+ 1 row in set (0.03 sec) mysql>
回到B事务,提交刚才插入的一条数据。
mysql> set session transaction isolation level read committed; Query OK, 0 rows affected (0.01 sec) mysql> start transaction; Query OK, 0 rows affected (0.01 sec) mysql> insert into user(name,salary) values ('lucy',2000); Query OK, 1 row affected (0.01 sec) mysql> commit; Query OK, 0 rows affected (0.02 sec) mysql>
最后回到A事务,再次查询user表,能查到B事务提交的数据
mysql> set session transaction isolation level read committed; Query OK, 0 rows affected (0.01 sec) mysql> start transaction; Query OK, 0 rows affected (0.01 sec) mysql> select * from user; +----+------+--------+ | id | name | salary | +----+------+--------+ | 1 | jack | 1000 | +----+------+--------+ 1 row in set (0.02 sec) mysql> select * from user; +----+------+--------+ | id | name | salary | +----+------+--------+ | 1 | jack | 1000 | +----+------+--------+ 1 row in set (0.03 sec) mysql> select * from user; +----+------+--------+ | id | name | salary | +----+------+--------+ | 1 | jack | 1000 | | 2 | lucy | 2000 | +----+------+--------+ 2 rows in set (0.03 sec) mysql>
再开启一个事务C,事务C修改一条记录并提交。
mysql> set session transaction isolation level read committed; Query OK, 0 rows affected (0.01 sec) mysql> start transaction; Query OK, 0 rows affected (0.01 sec) mysql> update user set salary=salary-200 where id=1; Query OK, 1 row affected (0.01 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql> commit; Query OK, 0 rows affected (0.01 sec) mysql>
然后继续回到A事务中,读取user表数据,可以读到C事务已经修改的数据
mysql> set session transaction isolation level read committed; Query OK, 0 rows affected (0.01 sec) mysql> start transaction; Query OK, 0 rows affected (0.01 sec) mysql> select * from user; +----+------+--------+ | id | name | salary | +----+------+--------+ | 1 | jack | 1000 | +----+------+--------+ 1 row in set (0.02 sec) mysql> select * from user; +----+------+--------+ | id | name | salary | +----+------+--------+ | 1 | jack | 1000 | +----+------+--------+ 1 row in set (0.03 sec) mysql> select * from user; +----+------+--------+ | id | name | salary | +----+------+--------+ | 1 | jack | 1000 | | 2 | lucy | 2000 | +----+------+--------+ 2 rows in set (0.03 sec) mysql> select * from user; +----+------+--------+ | id | name | salary | +----+------+--------+ | 1 | jack | 800 | | 2 | lucy | 2000 | +----+------+--------+ 2 rows in set (0.18 sec) mysql>
所以说提交读,会读取到已经提交的数据,不管数据是修改还是新增,只要提交了,在另一个事务中前后读取的结果会不一致。
Repeatable Read(可重复读)演示
设置事务隔离级别为Repeatable Read。先后开启事务A,B。首先开启事务A,查询user表数据,可以查到2条数据。
mysql> set session transaction isolation level repeatable read; Query OK, 0 rows affected (0.01 sec) mysql> start transaction; Query OK, 0 rows affected (0.01 sec) mysql> select * from user; +----+------+--------+ | id | name | salary | +----+------+--------+ | 1 | jack | 800 | | 2 | lucy | 2000 | +----+------+--------+ 2 rows in set (0.03 sec) mysql>
再开启B事务,修改一条数据并提交。
mysql> set session transaction isolation level repeatable read; Query OK, 0 rows affected (0.01 sec) mysql> start transaction; Query OK, 0 rows affected (0.01 sec) mysql> update user set salary=600 where id=1; Query OK, 1 row affected (0.01 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql> commit; Query OK, 0 rows affected (0.05 sec) mysql>
回到A事务继续读取user表数据,发现还是之前的数据,并没有因为B事务的修改发生改变,重复读取的结果一样。
mysql> set session transaction isolation level repeatable read; Query OK, 0 rows affected (0.01 sec) mysql> start transaction; Query OK, 0 rows affected (0.01 sec) mysql> select * from user; +----+------+--------+ | id | name | salary | +----+------+--------+ | 1 | jack | 800 | | 2 | lucy | 2000 | +----+------+--------+ 2 rows in set (0.03 sec) mysql> select * from user; +----+------+--------+ | id | name | salary | +----+------+--------+ | 1 | jack | 800 | | 2 | lucy | 2000 | +----+------+--------+ 2 rows in set (0.03 sec) mysql>
这个时候开启事务C,往user表里插入一条数据并提交。
mysql> set session transaction isolation level repeatable read; Query OK, 0 rows affected (0.01 sec) mysql> start transaction; Query OK, 0 rows affected (0.01 sec) mysql> insert into user(name , salary) values ('lucy',3000); Query OK, 1 row affected (0.04 sec) mysql> commit; Query OK, 0 rows affected (0.03 sec) mysql>
然后再次回到事务A,查询user表的数据,发现仍然还是2条。
mysql> set session transaction isolation level repeatable read; Query OK, 0 rows affected (0.01 sec) mysql> start transaction; Query OK, 0 rows affected (0.01 sec) mysql> select * from user; +----+------+--------+ | id | name | salary | +----+------+--------+ | 1 | jack | 800 | | 2 | lucy | 2000 | +----+------+--------+ 2 rows in set (0.03 sec) mysql> select * from user; +----+------+--------+ | id | name | salary | +----+------+--------+ | 1 | jack | 800 | | 2 | lucy | 2000 | +----+------+--------+ 2 rows in set (0.03 sec) mysql> select * from user; +----+------+--------+ | id | name | salary | +----+------+--------+ | 1 | jack | 800 | | 2 | lucy | 2000 | +----+------+--------+ 2 rows in set (0.03 sec) mysql>
那么问题来了,前面我们提到过,可重复读是会产生幻读的情况的。可重复读只能保证在范围内的数据被其它事务修改并提交,在当前事务读取到的数据一致,但是如果某一个事务在范围内新增了数据,当前事务是能查到的。也就是说事务A三次查询user表的所有数据,第一次和第二次之间,事务B修改了一条数据,在可重复读级别下,事务A读到的数据是一样的。但是C事务是在范围内(select *)新增了一条数据,按照幻读的现象,事务A应该会读取到这条记录才对。其实,这里就是采用的MVCC(多版本并发控制)。MVCC机制使得一个事务只能访问到在事务开启之前所有已提交的事务产生的数据。关于该机制的原理后续再谈。
注意:本文仅代表个人理解和看法哟!和本人所在公司和团体无任何关系!