前文回顾
在上篇博文史上最简单MySQL教程详解(进阶篇)之锁与事务处理分离水平(一)中,我们已经对MySQL中锁进行了一个大概的介绍,并介绍了几种常见的锁。这里需要重申的是,使用锁的目的是为了在多个用户同时更新的情况下,也能保证数据的整合性。但同时我们根据上篇博文了解到,如果我们锁定的粒度越大,保持锁定的时间越长,并发性也就会越差。也就是说,在为一个用户保持锁定的时候,同时就会有多个用户只能等待着锁定解除。 所以,在数据库操作中,为了有效保证并发读取数据的正确性,提出的事务隔离级别(也称为:分离水平,表示并发情况下各个事务之间的相互影响程度)。我们的数据库锁,也是为了构建这些隔离级别存在的。
事务处理的四种分离水平(隔离级别)
隔离级别 | 非提交读取 | 不可重复读取 | 幻想读取 |
---|---|---|---|
未提交读(Read uncommitted) | 可能 | 可能 | 可能 |
已提交读(Read committed) | 不可能 | 可能 | 可能 |
可重复读(Repeatable read) | 不可能 | 不可能 | 可能 |
可串行化(Serializable ) | 不可能 | 不可能 | 不可能 |
注意事项:
不同数据库对于事务隔离级别的支持各有不同,但是MySQL中的InnpDB引擎支持所有的分离水平
分离水平的设置
我们使用【SELECT】语句来查看当前分离水平,具体语法如下:
mysql> select @@session.tx_isolation;
我们使用【SET】语句进行分离水平的修改,具体的语法如下:
set [global | session] transaction isolation level 隔离级别名称;
注意事项
- 默认的行为即:不指定【session】和【global】关键字,是为下一个(未开始)事务设置隔离级别;
- 指定了【session】关键字后,设定只适用于当前的连接
- 指定了【global】关键字后,则用于其后的所有新连接(需要SUPER权限)
非提交读取(脏读)
非提交读取又称为脏读(Dirty Read),指一个事务处理过程里读取了另一个未提交的事务中的数据。例如:A事务读取B事务尚未提交的更改数据,并在这个数据基础上操作。如果B事务回滚,那么A事务读到的数据根本不是合法的,这样的情况就称为脏读。通常脏读的现象只发生在分离水平为【Read uncommitted】的场合,但是【Read uncommitted】是对其他事务的操作没有进行任何的限制,所以通常不使用。
例如:我们同时开启两个窗口(不同的用户 ,下面就用A窗口和B窗口来进行指代),
Session A:
# 设置事务分离水平
mysql> set session transaction isolation level READ UNCOMMITTED;
Query OK, 0 rows affected (0.00 sec)
# 选择数据库,并确认【user_a】表中的数据
mysql> use test
Database changed
mysql> SELECT * FROM user_a;
+--------+------+------+
| userId | name | sex |
+--------+------+------+
| 1 | 张三 | 0 |
+--------+
1 row in set (0.04 sec)
Session B:
# 设置事务分离水平
mysql> set session transaction isolation level READ UNCOMMITTED;
Query OK, 0 rows affected (0.00 sec)
# 选择数据库,并确认【user_a】表中的数据
mysql> use test
Database changed
mysql> SELECT * FROM user_a;
+--------+------+------+
| userId | name | sex |
+--------+------+------+
| 1 | 张三 | 0 |
+--------+------+------+
1 row in set (0.00 sec)
Session A:
# 更新【user_a】表中userId为1的数据
mysql> UPDATE user_a SET sex=1 WHERE userID=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
# 确认更新后的数据
mysql> SELECT * FROM user_a;
+--------+------+------+
| userId | name | sex |
+--------+------+------+
| 1 | 张三 | 1 |
+--------+------+------+
1 row in set (0.00 sec)
Session B:
# 确认【user_a】表中的数据
mysql> SELECT * FROM user_a;
+--------+------+------+
| userId | name | sex |
+--------+------+------+
| 1 | 张三 | 1 |
+--------+------+------+
1 row in set (0.00 sec)
Session A:
# 提交事务
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
Session B:
# 提交事务
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
结论:可以从上面的操作结果看出,Session B读取到了Session A中尚未提交(Commit)的数据,这样很有可能会在后面的操作中出现有关数据整合性的问题。
不可重复读取
不可重复读取(Non-Repeatable Read)是在某一事务中对同一数据进行多次读取,但由于其他事务处理的更新动作读取的数据状态了发生了改变。例如:A事务读取了B事务已经提交的更改(或删除)数据。比如A事务第一次读取数据,然后B事务更改该数据并提交,A事务再次读取数据,两次读取的数据不一样。
例如:我们同时开启两个窗口(不同的用户 ,下面就用A窗口和B窗口来进行指代),
Session A:
# 设置事务分离水平为READ COMMITTED
mysql> set session transaction isolation level READ COMMITTED;
Query OK, 0 rows affected (0.00 sec)
# 开启事务
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
# 第一次确认【user_a】表中的数据
mysql> SELECT * FROM user_a;
+--------+------+------+
| userId | name | sex |
+--------+------+------+
| 1 | 张三 | 1 |
+--------+------+------+
1 row in set (0.00 sec)
Session B:
# 设置事务分离水平为 READ COMMITTED
mysql> set session transaction isolation level READ COMMITTED;
Query OK, 0 rows affected (0.00 sec)
# 将【user_a】中userId为1的name字段更改为“李四”
mysql> UPDATE user_a SET name="李四" WHERE userId = 1;
Query OK, 1 row affected (0.04 sec)
Rows matched: 1 Changed: 1 Warnings: 0
# 确认更改结果
mysql> SELECT * FROM user_a;
+--------+------+------+
| userId | name | sex |
+--------+------+------+
| 1 | 李四 | 1 |
+--------+IT;
Query OK, 0 rows affected (0.00 sec)
Session A:
# 事务A第二次确认【user_a】中的数据
mysql> SELECT * FROM user_a;
+--------+------+------+
| userId | name | sex |
+--------+------+------+
| 1 | 李四 | 1 |
+--------+------+------+
1 row in set (0.00 sec)
# 提交事务
mysql> COMMIT;
Query OK, 0 rows affected (0.00 sec)
结论:我们可以看出,当Session A在执行的过程中,Session B 完成了对于数据的更新并提交了事务以后,当Session A再次去读取数据时,读取到事务已经和第一次不同,这就造成了数据的冲突。需要注意的是,不可重复读取发生于【READ COMMITED】以下的级别,即【READ COMMITED】和【READ UNCOMMITED】。所以要回避这种现象,必须将分离水平调整到高于【READ COMMITED】水平。
幻象读取
幻象读取(Phantom Read)简称:幻读。是在某一事务处理中对同一表进行多次读取时,但由于其他事务处理中进行了记录的插入/删除动作产生了结果中出现了第一次读取时不存在的数据,或者第一次读取时有的数据消失的现象。这里需要注意的是:和不可重复读的区别在于,这里是新增,不可重复读是更改(或删除)。这两种情况对策是不一样的,对于不可重复读,只需要采取行级锁防止该记录数据被更改或删除,然而对于幻读必须加表级锁,防止在这个表中新增一条数据。
例如:我们同时开启两个窗口(不同的用户 ,下面就用A窗口和B窗口来进行指代)
Session A:
# 设置事务分离水平为 READ COMMITTED
mysql> set session transaction isolation level READ COMMITTED;
Query OK, 0 rows affected (0.00 sec)
# 开启事务
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
# 第一次确认表【user_a】中的数据
mysql> SELECT * FROM user_a;
+--------+------+------+
| userId | name | sex |
+--------+------+------+
| 1 | 李四 | 1 |
+--------+------+------+
1 row in set (0.00 sec)
Session B:
# 设置事务分离水平为 READ COMMITTED
mysql> set session transaction isolation level READ COMMITTED;
Query OK, 0 rows affected (0.00 sec)
# 确认表【user_a】中的数据
mysql> SELECT * FROM user_a;
+--------+------+------+
| userId | name | sex |
+--------+------+------+
| 1 | 李四 | 1 |
+--------+------+------+
1 row in set (0.00 sec)
# 插入新数据
mysql> INSERT INTO user_a VALUES ('2',"王五",0);
Query OK, 1 row affected (0.00 sec)
# 确认插入后的数据
mysql> SELECT * FROM user_a;
+--------+------+------+
| userId | name | sex |
+--------+------+------+
| 1 | 李四 | 1 |
| 2 | 王五 | 0 |
+--------+------+------+
2 rows in set (0.00 sec)
# 事务提交
mysql> COMMIT;
Query OK, 0 rows affected (0.00 sec)
Session A :
# 第二次确认表【user_a】中数据
mysql> SELECT * FROM user_a;
+--------+------+------+
| userId | name | sex |
+--------+------+------+
| 1 | 李四 | 1 |
| 2 | 王五 | 0 |
+--------+------+------+
2 rows in set (0.00 sec)
# 事务提交
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
结论:我们可以看到,在Session A的执行过程中,第二次查询到了Session B 提交的新数据。与第一次查询到的结果相冲突。所以要消除幻读的现象就必须将分离水平提高到【SERIALIZABLE】的水平。但需要注意的是,我们前面也已经提到了:分离水平越高,维持锁定的时间就越长,这样并发性就会降低很多。所以并非所有情况下都适合将分离水平提高到【SERIALIZABLE】水平。
总结:
四个分离逐渐增强,四种级别都有各自的特点,每个分离解决一个问题。事务级别越高,性能也就越差。同时需要注意的是:在任何一种隔离机制下,都是不允许一个事务删除或修改另一个事务影响过而未提交的数据的。因为事务增、删、改数据以后,会在该行加上排它锁,排它锁会阻塞其他事务再次对该行数据操作。也正是由于排它锁的存在,这四种隔离机制都不会出现任何一种更新丢失的现象,因为一条信息根本不允许第二个事务进行修改。
参考文献:
《MySQL高效编程》