Mysql 中的隔离级别
Mysql 中的隔离级别
三种问题
脏读
读到事务操作的中间结果
不可重复读
事务中第一次读取到的数据集和第二次读取到的数据集不同
幻读
解决可重复读问题时,第一次读取数据时,会将引用到的行数据锁定,直到整个事务结束。但是无法锁定未指定的行。 举例:
- A 事务,第一次 查询 2019 年之前的记录,得到一个结果集,然后对这个结果集全部做了修改。
- B 事务,向表内添加了一条 2018 年的记录,修改了结果集
- A 事务不知道 B 事务的行为,A 仍认为 2019年之前的事务都已经全部修改成功,然后重新查找 2019 年之前的数据,会发现一个没有修改过的 B 插入的记录,因此发生了幻读。
四种隔离级别
1. 读未提交
- 最基本问题:会发生脏读
- 解决办法:将事务未完成期间设置成不可见,保证事务的原子性。
mysql 测试读未提交会发生脏读
首先创建数据库 test,在 test 中创建 student 表,student 表中有三个字段,分别是 ID, NAME, AGE。
+-------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+----------------+
| ID | int(11) | NO | PRI | NULL | auto_increment |
| NAME | varchar(20) | NO | | NULL | |
| AGE | int(11) | NO | | NULL | |
+-------+-------------+------+-----+---------+----------------+
打开两个终端,将终端 session 的隔离级别修改成读未提交,A 终端负责将 ID = 3 的学生的姓名修改成 ‘wang’,休眠 10 s 后,再将其修改成 ‘sun’。B 终端在 A 终端开启之后查询 ID = 3 的学生的姓名。可以发现 ID = 3 的学生的姓名最终是 ‘sun’,但是 B 终端查询到的却是 ‘wang’。
A 终端 sql 语句
USE test;
SET autocommit = 0; -- 禁止自动提交
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -- 将 session 的隔离级别设置成读未提交
-- SELECT @@tx_isolation;
START TRANSACTION;
UPDATE student SET NAME = 'wang' WHERE ID = '3';
select sleep(10); -- 休眠 10 s,给 B 客户端充足的时间去查询
UPDATE student SET NAME = 'sun' WHERE ID = '3';
COMMIT;
B 终端 SQL 语句
USE test;
SET autocommit = 0; -- 禁止自动提交
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -- 将当前 session 设置成读未提交
-- SELECT @@tx_isolation;
START TRANSACTION;
SELECT NAME FROM student WHERE id = 3;
COMMIT;
B 终端的查询结果为
+------+
| name |
+------+
| wang |
+------+
B 事务读取到了 A 事务的中间结果。
2. 读已提交
事务原子化,B 事务完全提交后,A 事务才可以读取。将读未提交中的测试用例中的数据隔离级别提升到读已提交(READ COMMITTED),就不会读取到中间结果了,虽然能够解决脏读问题,但是无法解决重复读问题。
mysql 测试读已提交无法解决重复读问题
操作流程是这样的,A 事务连续读两次,中间睡眠 10 s,B 事务修改内容。A 事务先启动,B 事务后启动。
A 事务
-- A 事务修改
USE test;
-- 创建查询结果,用来保存 A 事务的两次查询结果
CREATE TABLE result IF NOT EXISTS(
result VARCHAR(12)
);
-- 清空结果表
DELETE FROM result;
SET autocommit = 0; -- 禁止自动提交
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- SELECT @@tx_isolation;
START TRANSACTION;
INSERT INTO result (SELECT NAME FROM student WHERE id = '3');
SELECT SLEEP(10);
INSERT INTO result (SELECT NAME FROM student WHERE id = '3');
COMMIT;
B 事务
-- B 事务修改
USE test;
SET autocommit = 0; -- 禁止自动提交
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 将当前 session 设置成读未提交
-- SELECT @@tx_isolation;
START TRANSACTION;
UPDATE student SET NAME = 'zhang' WHERE ID = '3';
COMMIT;
查询 result 的结果,发现两次的结果不同的,证明不能重复读。
+--------+
| result |
+--------+
| sun |
| zhang |
+--------+
3. 可重复读
事务 A 中,对数据集的两次读取操作得到的数据集相同,只针对 A 操作过的数据。使用 MVCC 机制就能保证可重复读,重复上面的实验,B 事务会一直在等 A 事务完成,最终 A 事务得到的数据都是相同的。但是无法解决幻读问题。
MVCC(多版本控制并发)
为了实现提交读和可重复读,InnoDB 使用 MVCC 机制。
版本号
- 系统版本号,假定为 n_s
- 事务版本号,假定为 n_t
每一个数据行都有两个隐藏列,一个存放创建版本号,一个存放删除版本号
- 创建版本号,假定为 n_c
- 删除版本号,假定为 n_d
系统版本号和事务版本号的获取规则
- 系统版本号,是一个递增的数字,每次开启一个事务,递增一个版本号。
- 事务版本号,事务开始时系统版本号。
实现过程
- SELECT
当前事务版本号为 n_t,如果
- n_t > n_c,有效,说明的当前事务读取到的行是当前事务开始之前创建的数据行
- n_t <= n_c,无效,说明当前事务开始后,有别的事务对数据行进行了修改,事务回滚
- n_t < n_d,有效,说明是事务开始之后,才被其他事务删除
- n_t <= n_d,无效,说明事务开始之前,该数据行已经被删除,不应该去读取这个
-
INSERT 当前行的 n_c = n_s
-
DELETE 当前行的 n_d = n_s
- UPDATE 当前行的 n_d = n_s,n_c = n_s
回滚
使用 Undo 日志将每个事务的快照保存下来,通过回滚指针将所有快照连接起来,以便回滚。
测试 mvcc 机制
尝试进行测试时发现事务 B 会被阻塞,看起来像是加锁了,也就是说 Mysql innodb 版本实际上并没有仅仅使用 mvcc 机制来保证可重复读。此处存疑。 源码分析
4. 串行化读
事务执行串行化。直接加锁,但是加锁有可能造成死锁现象。