数据库是要被广大客户所共享访问的,在大量用户并发访问数据库过程中很有可能出现以下几种不不确定情况:
A、脏读:一个事务读取到了另一个事务未提交的数据操作结果。这是相当危险的,因为很可能另一个事务的所有的操作都被回滚;
B、更新丢失:两个事务都同时更新一行数据,一个事务对数据的更新把另一个事务对数据的更新覆盖了。这是因为系统没有执行任何的锁操作,因此并发事务并没有被隔离开来。
C、不可重复读:一个事务对同一行数据重复读取两次,但是却得到了不同的结果。
为了避免上面出现的几种情况,标准SQL规范中定义了四种事务隔离级别,包括了一些具体规则,用来限定事务内外的哪些改变是可见的,哪些是不可见的。不同的隔离级别对事务的处理不同,事务隔离级别越高,为避免冲突所花的精力也就越多。低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。
四种事务隔离级别按等级从低到高分别为:
A、Read Uncommitted(读取未提交内容);
B、Read Committed(读取提交内容);
C、Repeatable Read(可重读);
D、Serializable(可串行化)。
下面分别对这四种事务隔离级别进行讲解并举例。
为了方便测试说明,先建立测试表user如下(仅含id和name两个字段,是不是够简单,没错,就是要简单):
以下四种事务隔离级别都用这个user表进行演示说明。
1、Read Uncommitted(读取未提交内容)
在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。通俗地讲就是,在一个事务中可以读取到另一个事务中新增或修改但未提交的数据。因为另一个事务可能回滚,所以在第一个事务中读取到的数据很可能是无效的脏数据,造成脏读现象。读取未提交的数据,也被称为脏读。
实例如下:
更改事务隔离级别为read-uncommitted读取未提交内容:
分别开始两个事务A、B,做如下操作:
事务A | 事务B |
Query OK, 0 rows affected (0.00 sec) mysql> select * from user; Empty set (0.00 sec) | 1、开始事务: mysql> start transaction; Query OK, 0 rows affected (0.00 sec) 2、查询user表的数据: mysql> select * from user; Empty set (0.00 sec) |
| 3、插入一条数据: mysql> insert into user values(1,'user1'); Query OK, 1 row affected (0.00 sec) 4、查询user表的数据: mysql> select * from user; +----+-------+ | id | name | +----+-------+ | 1 | user1 | +----+-------+ 1 row in set (0.00 sec) |
3、查询user表的数据: mysql> select * from user; +----+-------+ | id | name | +----+-------+ | 1 | user1 | +----+-------+ 1 row in set (0.00 sec) |
|
| 5、回滚事务: mysql> rollback; Query OK, 0 rows affected (0.01 sec) mysql> select * from user; Empty set (0.00 sec) |
4、查询user表的数据: (在事务B回滚后查看数据) mysql> select * from user; Empty set (0.00 sec) |
|
经过上面的实例可以看出,事务B插入了一条数据但是没有提交,此时事务A可以查询到那条未提交的数据,但一旦事务B回滚后,事务A就查询不到那条数据。事务A在事务B提交之前查询到的那条数据是无效的,即脏数据,因为没法确保事务B最后是否会成功提交或者成功提交之前是否还会对该数据进行修改,依据脏数据所做的操作可能是不正确的,极有可能造成脏读现象。
2、Read Committed(读取提交内容)
这是大多数数据库系统的默认隔离级别(但不是mysql默认的),它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的修改。换句话说,一个事务从开始直到提交之前,所做的任何修改对其他事务都是不可见的。这个级别有时候也叫做不可重复读(nonrepeatable read),因为两次执行同样的查询,可能会得到不一样的结果。
实例如下:
更改事务隔离级别为read-committed读取未提交内容:
分别开始两个事务A、B,做如下操作:
事务A | 事务B |
1、开始事务: mysql> start transaction; Query OK, 0 rows affected (0.00 sec) 2、查询user表的数据: mysql> select * from user; Empty set (0.00 sec) | 1、开始事务: mysql> start transaction; Query OK, 0 rows affected (0.00 sec) 2、查询user表的数据: mysql> select * from user; Empty set (0.00 sec) |
| 3、插入一条数据: mysql> insert into user values(1,'user1'); Query OK, 1 row affected (0.00 sec) 4、查询user表的数据: mysql> select * from user; +----+-------+ | id | name | +----+-------+ | 1 | user1 | +----+-------+ 1 row in set (0.00 sec) |
3、查询user表的数据: (在事务B插入数据但未提交时查看数据) mysql> select * from user; Empty set (0.00 sec) |
|
| 5、提交事务: mysql> commit; Query OK, 0 rows affected (0.01 sec) |
4、查询user表的数据: (在事务B提交后查看数据) mysql> select * from user; +----+-------+ | id | name | +----+-------+ | 1 | user1 | +----+-------+ 1 row in set (0.00 sec) |
|
从实例可以看到,事务A在事务B插入数据但未提交时,查询不到数据,当直到事务B提交事务之后,事务A才可以查询到新增的数据。
经过上面的实例可以得出结论,已提交读隔离级别解决了脏读的问题,但是出现了不可重复读的问题,即事务A在两次查询的数据不一致,因为在两次查询之间事务B更新了一条数据。
3、Repeatable Read(可重读)
这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。通俗来讲,可重复读在一个事务里读取数据,怎么读都不会变,除非提交了该事务,再次进行读取。
实例如下:
更改事务隔离级别为repeatable-read读取未提交内容:
分别开始两个事务A、B,做如下操作:
事务A | 事务B |
1、开始事务: mysql> start transaction; Query OK, 0 rows affected (0.00 sec) 2、查询user表的数据: mysql> select * from user; +----+-------+ | id | name | +----+-------+ | 1 | user1 | +----+-------+ 1 row in set (0.00 sec) | 1、开始事务: mysql> start transaction; Query OK, 0 rows affected (0.00 sec) 2、查询user表的数据: mysql> select * from user; +----+-------+ | id | name | +----+-------+ | 1 | user1 | +----+-------+ 1 row in set (0.00 sec) |
| 3、插入一条数据: mysql> insert into user values(2,'user2'); Query OK, 1 row affected (0.00 sec) 4、查询user表的数据: mysql> select * from user; +----+-------+ | id | name | +----+-------+ | 1 | user1 | | 2 | user2 | +----+-------+ 2 rows in set (0.00 sec) |
3、查询user表的数据: (在事务B插入数据但未提交时查看数据) mysql> select * from user; +----+-------+ | id | name | +----+-------+ | 1 | user1 | +----+-------+ 1 row in set (0.00 sec) |
|
| 5、提交事务: mysql> commit; Query OK, 0 rows affected (0.01 sec) |
4、查询user表的数据: (在事务B提交后查看数据) mysql> select * from user; +----+-------+ | id | name | +----+-------+ | 1 | user1 | +----+-------+ 1 row in set (0.00 sec) |
|
5、事务A提交事务之后查询user表数据: mysql> commit; Query OK, 0 rows affected (0.00 sec) mysql> select * from user; +----+-------+ | id | name | +----+-------+ | 1 | user1 | | 2 | user2 | +----+-------+ 2 rows in set (0.00 sec) |
从实例可以看到,事务A在事务B插入数据但未提交时,查询不到数据,当直到事务B提交事务之后,事务A才可以查询到新增的数据。
经过上面的实例可以得出结论,已提交读隔离级别解决了脏读的问题,但是出现了不可重复读的问题,即事务A在两次查询的数据不一致,因为在两次查询之间事务B更新了一条数据。
4、Serializable(可串行化)
这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。通俗地讲就是,假如两个事务都操作到同一数据行,那么这个数据行就会被锁定,只允许先读取/操作到数据行的事务优先操作,只有当事务提交了,数据行才会解锁,后一个事务才能成功操作这个数据行,否则只能一直等待。就像java中的锁,写数据必须等待另一个事务结束。也正因为这样,可能导致大量的超时现象和锁竞争。
实例如下:
更改事务隔离级别为serializable读取未提交内容:
分别开始两个事务A、B,做如下操作(两个实例):
第1个实例:
事务A | 事务B |
1、开始事务: mysql> start transaction; Query OK, 0 rows affected (0.00 sec) 2、查询user表的数据: mysql> select * from user; +----+-------+ | id | name | +----+-------+ | 1 | user1 | +----+-------+ 1 row in set (0.00 sec) |
|
| 1、开始事务: mysql> start transaction; Query OK, 0 rows affected (0.00 sec) 2、查询user表的数据: mysql> select * from user; +----+-------+ | id | name | +----+-------+ | 1 | user1 | +----+-------+ 1 row in set (0.00 sec) 3、插入一条数据: mysql> insert into user values(2,'user2'); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction |
3、提交事务 mysql> commit; Query OK, 0 rows affected (0.01 sec) |
|
第2个实例:
事务A | 事务B |
1、开始事务: mysql> start transaction; Query OK, 0 rows affected (0.00 sec) 2、查询user表的数据: mysql> select * from user; +----+-------+ | id | name | +----+-------+ | 1 | user1 | +----+-------+ 1 row in set (0.00 sec) |
|
| 1、开始事务: mysql> start transaction; Query OK, 0 rows affected (0.00 sec) 2、查询user表的数据: mysql> select * from user; +----+-------+ | id | name | +----+-------+ | 1 | user1 | +----+-------+ 1 row in set (0.00 sec) 3、插入一条数据: mysql> insert into user values(2,'user2');
一直等待一直等待一直等待... |
3、提交事务 mysql> commit; Query OK, 0 rows affected (0.01 sec) |
|
Query OK, 1 row affected (0.00 sec) 4、查询user表的数据: mysql> select * from user; +----+-------+ | id | name | +----+-------+ | 1 | user1 | | 2 | user2 | +----+-------+ 2 rows in set (0.00 sec) |
从以上两个实例可以看到,事务B对user表的新增操作需要等事务A提交之后才可以操作成功,若事务A一直不提交,在等待一定时间之后事务B就会超时报错。从实例中可以看出serializable完全锁定字段,若后一个事务来查询同一份数据就必须等待,直到前一个事务完成并解除锁定为止,是完整的隔离级别,会锁定对应的数据表,因而会有效率的问题。
PS:实践发现,事务A查询user数据但未提交时,事务B增删改时需要等待,但查询数据不需要等待;事务A增删改数据但未提交时,事务B增删查改都需要等待(当然前提是有操作到同一行数据)。