前言
MySQL标准定义了4种隔离级别,包括了一些具体规则,用来限定事务内外的哪些改变是可见的,哪些是不可见的。低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。今天想把这块整理出来,尽量用最简洁的语言描述出来,供新人参考。
注:本文可以与上篇文章一同参考
Spring的七个事务传播行为
事务的基本要素(ACID)
1、原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。
2、一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。
3、隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。
4、持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。
事务的并发问题
1、脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
2、不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。
3、幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。
MySQL事务隔离级别
四种事务隔离级别按等级从低到高分别为:
- Read Uncommitted(读取未提交内容);
- Read Committed(读取提交内容);
- Repeatable Read(可重读);
- Serializable(可串行化)。
修改事务隔离级别
set [ global | session ] transaction isolation level
Read uncommitted | Read committed | Repeatable | Serializable;
例:
set global transaction isolation level read-uncommitted;
注意:
1、如果选择global,意思是此语句将应用于之后的所有session,而当前已经存在的session不受影响。
2、如果选择session,意思是此语句将应用于当前session内之后的所有事务。
3、如果什么都不写,意思是此语句将应用于当前session内的下一个还未开始的事务。
以下是查询当前事务隔离级别
select @@global.tx_isolation;
select @@session.tx_isolation;
准备工作
创建user表
mysql> select * from user;
+----+------+------+-------+
| id | name | pass | birth |
+----+------+------+-------+
| 54 | test | 1 | NULL |
+----+------+------+-------+
以下四种事务隔离级别都用这个user表进行演示说明。
1、Read Uncommitted(读取未提交内容)
在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。通俗地讲就是,在一个事务中可以读取到另一个事务中新增或修改但未提交的数据。因为另一个事务可能回滚,所以在第一个事务中读取到的数据很可能是无效的脏数据,造成脏读现象。读取未提交的数据,也被称为脏读。
例如:
修改事务隔离级别
mysql> set session transaction isolation level read uncommitted;
mysql> select @@session.tx_isolation;
+------------------------+
| @@session.tx_isolation |
+------------------------+
| READ-UNCOMMITTED |
+------------------------+
分别开始两个事务A、B,做如下操作:
事务A | 事务B |
---|---|
mysql> delete from user; Query OK, 1 row affected mysql> start transaction; Query OK, 0 rows affected mysql> select * from user; Empty set | |
mysql> start transaction; Query OK, 0 rows affected mysql> select * from user; Empty set | |
mysql> insert user (name,pass) value (“test”,”1”); Query OK, 1 row affected mysql> select * from user; +—-+——+——+——-+ | id | name | pass | birth | +—-+——+——+——-+ | 55 | test | 1 | NULL | +—-+——+——+——-+ | |
mysql> select * from user; +—-+——+——+——-+ | id | name | pass | birth | +—-+——+——+——-+ | 55 | test | 1 | NULL | +—-+——+——+——-+ 1 row in set | |
mysql> rollback; Query OK, 0 rows affected mysql> select * from user; Empty set | |
mysql> select * from user; Empty set |
经过上面的实例可以看出,事务B插入了一条数据但是没有提交,此时事务A可以查询到那条未提交的数据,但一旦事务B回滚后,事务A就查询不到那条数据。事务A在事务B提交之前查询到的那条数据是无效的,即脏数据,因为没法确保事务B最后是否会成功提交或者成功提交之前是否还会对该数据进行修改,依据脏数据所做的操作可能是不正确的,极有可能造成脏读现象。
2、Read Committed(读取提交内容)
这是大多数数据库系统的默认隔离级别(但不是mysql默认的),它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的修改。换句话说,一个事务从开始直到提交之前,所做的任何修改对其他事务都是不可见的。这个级别有时候也叫做不可重复读(nonrepeatable read),因为两次执行同样的查询,可能会得到不一样的结果。
例如:
修改事务隔离级别
mysql> set session transaction isolation level read committed;
Query OK, 0 rows affected
mysql> select @@session.tx_isolation;
+------------------------+
| @@session.tx_isolation |
+------------------------+
| READ-COMMITTED |
+------------------------+
分别开始两个事务A、B,做如下操作:
事务A | 事务B |
---|---|
mysql> delete from user; Query OK, 0 rows affected mysql> start transaction; Query OK, 0 rows affected mysql> select * from user; Empty set | |
mysql> start transaction; Query OK, 0 rows affected mysql> select * from user; Empty set | |
mysql> insert user (name,pass) value (“test”,”1”); Query OK, 1 row affected mysql> select * from user; +—-+——+——+——-+ | id | name | pass | birth | +—-+——+——+——-+ | 59 | test | 1 | NULL | +—-+——+——+——-+ | |
mysql> select * from user; Empty set | |
mysql> commit; Query OK, 0 rows affected | |
mysql> select * from user; +—-+——+——+——-+ | id | name | pass | birth | +—-+——+——+——-+ | 59 | test | 1 | NULL | +—-+——+——+——-+ |
从实例可以看到,事务A在事务B插入数据但未提交时,查询不到数据,当直到事务B提交事务之后,事务A才可以查询到新增的数据。
经过上面的实例可以得出结论,已提交读隔离级别解决了脏读的问题,但是出现了不可重复读的问题,即事务A在两次查询的数据不一致,因为在两次查询之间事务B更新了一条数据。
3、Repeatable Read(可重读)
这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。通俗来讲,可重复读在一个事务里读取数据,怎么读都不会变,除非提交了该事务,再次进行读取。
例如:
修改事务隔离级别
mysql> set session transaction isolation level repeatable read;
Query OK, 0 rows affected
mysql> select @@session.tx_isolation;
+------------------------+
| @@session.tx_isolation |
+------------------------+
| REPEATABLE-READ |
+------------------------+
分别开始两个事务A、B,做如下操作:
事务A | 事务B |
---|---|
mysql> delete from user; Query OK, 4 rows affected mysql> start transaction; Query OK, 0 rows affected mysql> select * from user; Empty set | |
mysql> start transaction; Query OK, 0 rows affected mysql> select * from user; Empty set | |
mysql> insert user (name,pass) value (“test”,”1”); Query OK, 1 row affected mysql> select * from user; +—-+——+——+——-+ | id | name | pass | birth | +—-+——+——+——-+ | 64 | test | 1 | NULL | +—-+——+——+——-+ | |
mysql> select * from user; Empty set | |
mysql> commit; Query OK, 0 rows affected | |
mysql> select * from user; Empty set | |
mysql> commit; Query OK, 0 rows affected mysql> select * from user; +—-+——+——+——-+ | id | name | pass | birth | +—-+——+——+——-+ | 64 | test | 1 | NULL | +—-+——+——+——-+ |
从实例可以看到,事务A在事务B插入数据但未提交时,查询不到数据,当直到事务B提交事务之后,事务A才可以查询到新增的数据。
经过上面的实例可以得出结论,已提交读隔离级别解决了脏读的问题,但是出现了不可重复读的问题,即事务A在两次查询的数据不一致,因为在两次查询之间事务B更新了一条数据。
4、Serializable(可串行化)
这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。通俗地讲就是,假如两个事务都操作到同一数据行,那么这个数据行就会被锁定,只允许先读取/操作到数据行的事务优先操作,只有当事务提交了,数据行才会解锁,后一个事务才能成功操作这个数据行,否则只能一直等待。就像java中的锁,写数据必须等待另一个事务结束。也正因为这样,可能导致大量的超时现象和锁竞争。
例如:
修改事务隔离级别
mysql> set session transaction isolation level serializable;
Query OK, 0 rows affected
mysql> select @@session.tx_isolation;
+------------------------+
| @@session.tx_isolation |
+------------------------+
| SERIALIZABLE |
+------------------------+
分别开始两个事务A、B,做如下操作:
事务A | 事务B |
---|---|
mysql> start transaction; Query OK, 0 rows affected mysql> select * from user; +—-+——+——+——-+ | id | name | pass | birth | +—-+——+——+——-+ | 64 | test | 1 | NULL | +—-+——+——+——-+ | |
mysql> start transaction; Query OK, 0 rows affected mysql> select * from user; +—-+——+——+——-+ | id | name | pass | birth | +—-+——+——+——-+ | 64 | test | 1 | NULL | +—-+——+——+——-+ | |
mysql> delete from user; 1205 - Lock wait timeout exceeded; try restarting transaction | |
mysql> commit; Query OK, 0 rows affected |
从以上两个实例可以看到,事务B对user表的新增操作需要等事务A提交之后才可以操作成功,若事务A一直不提交,在等待一定时间之后事务B就会超时报错。从实例中可以看出serializable完全锁定字段,若后一个事务来查询同一份数据就必须等待,直到前一个事务完成并解除锁定为止,是完整的隔离级别,会锁定对应的数据表,因而会有效率的问题。
PS:实践发现,事务A查询user数据但未提交时,事务B增删改时需要等待,但查询数据不需要等待;事务A增删改数据但未提交时,事务B增删查改都需要等待(当然前提是有操作到同一行数据)。