MySQL 事物隔离级别与锁(下) | 8lovelife's life
0%

MySQL 事物隔离级别与锁(下)

上篇文章 MySQL 事物隔离级别与锁(上) 中,我们主要介绍了在 Read UnCommitted 和 Read Committed 事物隔离级别下执行特定 SQL 的加锁情况,在本篇文章中,我们将重点分析 MySQL InnoDB 默认的事务隔离级别 Repeatable Read 在执行特定 SQL 时的加锁情况。

案例数据

我们会从执行以下 SQL 语句开始,分析 Repeatable Read 隔离级别下的加锁情况。

SQL 语句
SELECT * FROM table WHERE column = ?;
SELECT * FROM table WHERE column < ?;
SELECT * FROM table WHERE column = ? LOCK IN SHARE MODE;
SELECT * FROM table WHERE column < ? LOCK IN SHARE MODE;
SELECT * FROM table WHERE column = ? FOR UPDATE;
SELECT * FROM table WHERE column < ? FOR UPDATE;

下文事例都将基于表 employees 中的数据:

id(主键) name(非索引) employee_number
(唯一索引)
age(普通索引)
1 Alice 1001 30
5 Bob 1020 25
13 Charlie 1010 35
14 David 1035 25
25 Eve 1040 32

Repeatable Read

SQL 语句 锁情况
SELECT * FROM table WHERE column = ?; 不加锁
SELECT * FROM table WHERE column < ?; 不加锁
SELECT * FROM table WHERE column = ? LOCK IN SHARE MODE; 意向共享锁(IS)
和 行级共享锁(S)
或 临键锁(Next- Key Lock)
或 间隙锁(Gap Lock)
SELECT * FROM table WHERE column < ? LOCK IN SHARE MODE; 意向共享锁(IS)
和 行级共享锁(S)
或 临键锁(Next- Key Lock)
或 间隙锁(Gap Lock)
SELECT * FROM table WHERE column = ? FOR UPDATE; 意向排他锁(IX)
和 行级排他锁(X)
或 临键锁(Next-Key Lock)
或 间隙锁(Gap Lock)
SELECT * FROM table WHERE column < ? FOR UPDATE; 意向排他锁(IX)
和 行级排他锁(X)
或 临键锁(Next-Key Lock)
或 间隙锁(Gap Lock)

可以看到 Repeatable Read 支持 Net-Key Lock 和 Gap Lock,并且不同的 WHERE 条件可能导致不同的加锁方案,查询条件 column 是否是索引、是否是唯一索引、查询的记录是否存在,都会影响加锁方案。接下来,我们会分析查询条件为聚簇索引、唯一索引、普通索引 和 非索引的情况下,对应的不同加锁方案。

聚簇索引列查询

等值查询

查询有记录

时间线 事务A 事务B
T1 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
T2 START TRANSACTION;
T3 SELECT * FROM employees WHERE id = 5 LOCK IN SHARE MODE;
T4 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
T5 START TRANSACTION;
T6 UPDATE employees SET name = 'Robert' WHERE id = 5; (被阻塞,因事务A锁定记录)
T7 COMMIT; (事务A提交,释放锁)
T8 COMMIT; (事务B提交)

事物 A 在 T1 的查询条件 id = 5 记录存在,id 索引上首先会添加 Next-Key Lock,范围是(1, 5],由于 id = 5 的记录存在,并且 id 是唯一索引,Next-Key Lock 会退化为行记录锁,所以这里会添加行级共享锁 S 和 表级意向共享锁 IS,行级锁会添加在聚簇索引 id 上,事物 B 在 T6 尝试获取此行 id = 5 的排他锁 X 导致冲突,操作被阻塞。

查询无记录

时间线 事务A 事务B
T1 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
T2 START TRANSACTION;
T3 SELECT * FROM employees WHERE id = 2 LOCK IN SHARE MODE;
T4 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
T5 START TRANSACTION;
T6 INSERT INTO employees (id, name, employee_number, age) VALUES (3, 'Frank', 1051, 30); (被阻塞,因事务A的 Gap Lock)
T7 COMMIT; (事务A提交,释放锁)
T8 COMMIT; (事务B提交)

事物 A 在 T1 的查询条件 id = 2 的记录是不存在的,id 索引上首先会添加 Next-Key Lock,范围是(1, 5],由于 id = 5 的记录不存在,并且 id 是唯一索引,Next-Key Lock 会退化为 Gap Lock,所以这里会添加表级意向共享锁 IS 和 Gap Lock,Gap 的范围为(1, 5),事物 B 在 T6 尝试插入 id = 3 的记录导致冲突,操作被阻塞。

范围查找

时间线 事务A 事务B
T1 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
T2 START TRANSACTION;
T3 SELECT * FROM employees WHERE id >= 5 AND id < 32 LOCK IN SHARE MODE;
T4 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
T5 START TRANSACTION;
T6 INSERT INTO employees (id, name, employee_number, age) VALUES (10, 'Frank', 1050, 30); (阻塞)
T7 COMMIT; (事务A提交,释放锁)
T8 COMMIT; (事务B提交)

事物 A 在 id 索引上首先会添加 Next-Key Lock,范围是 (1, 5],(5, 13],(13,14],(14, 25],(25, 32],由于 id = 5 的记录存在,并且 id 是唯一索引,最先被扫描到的 Next-Key Lock (1, 5] 会退化为行记录锁,由于 id = 32 不满足 id < 32,最后被扫描到的 Next-Key Lock (25, 32] 会退化为 Gap Lock,所以这里会添加表级意向共享锁 IS 、行级共享锁 S、 Gap Lock,Gap 的范围为(5, 13],(13,14],(14, 25],(25, 32),行级锁会被添加在 id = 5 的记录上,事物 B 在 T6 尝试插入 id = 10 的记录导致锁冲突,操作被阻塞。

时间线 事务A 事务B
T1 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
T2 START TRANSACTION;
T3 SELECT * FROM employees WHERE id <= 13 LOCK IN SHARE MODE;
T4 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
T5 START TRANSACTION;
T6 INSERT INTO employees (id, name, employee_number, age) VALUES (10, 'Frank', 1050, 30); (阻塞)
T7 COMMIT; (事务A提交,释放锁)
T8 COMMIT; (事务B提交)

事物 A 在 id 索引上首先会添加 Next-Key Lock,范围是 (-∞, 1], (1, 5],(5, 13] ,id = 13 的记录存在,但这里的 (5, 13] 并不会退化为记录锁,所以这里会添加**表级意向共享锁 IS、 Next-Key Lock,范围为 (-∞, 1], (1, 5],(5, 13]**,事物 B 在 T6 尝试插入 id = 10 的记录导致锁冲突,操作被阻塞。

唯一索引列查询

等值查询

查询有记录

时间线 事务A 事务B
T1 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
T2 START TRANSACTION;
T3 SELECT * FROM employees WHERE employee_number = 1010 LOCK IN SHARE MODE;
T4 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
T5 START TRANSACTION;
T6 UPDATE employees SET name = 'Robert' WHERE id = 13; (被阻塞,因事务A锁定记录)
T7 COMMIT; (事务A提交,释放锁)
T8 COMMIT; (事务B提交)

事物 A 在 T3 的查询条件 employee_number = 1010 的记录是存在的,employee_number 索引上首先会添加 Next-Key Lock,范围是(1001, 1010],employee_number = 1010 的记录存在,并且 employee_number 是唯一索引,Next-Key Lock 会退化为行记录锁,所以这里会添加行级共享锁 S 和 表级意向共享锁 IS,行级锁会添加在聚簇索引 id = 13 和 唯一索引 employee_number = 1010 上,事物 B 在 T6 尝试更新 id = 13 的记录导致锁冲突,操作被阻塞。

查询无记录

时间线 事务A 事务B
T1 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
T2 START TRANSACTION;
T3 SELECT * FROM employees WHERE employee_number = 1011 LOCK IN SHARE MODE;
T4 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
T5 START TRANSACTION;
T6 INSERT INTO employees (id, name, employee_number, age) VALUES (6, ‘Robert’, 1012, 30);`
(被阻塞,因事务A 的 Gap Lock)
T7 COMMIT; (事务A提交,释放锁)
T8 COMMIT; (事务B提交)

事物 A 在 T3 的查询条件 employee_number = 1011 的记录是不存在的,employee_number 索引上首先会添加 Next-Key Lock,范围是(1010, 1020],employee_number = 1011 的记录不存在,Next-Key Lock 会退化为 Gap Lock,所以这里会添加表级意向共享锁 IS 和 Gap Lock,Gap 的范围为(1010, 1020),事物 B 在 T6 尝试插入 employee_number = 1012 的记录导致冲突,操作被阻塞。

范围查询

时间线 事务A 事务B
T1 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
T2 START TRANSACTION;
T3 SELECT * FROM employees WHERE employee_number < 1011 LOCK IN SHARE MODE;
T4 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
T5 START TRANSACTION;
T6 INSERT INTO employees (id, name, employee_number, age) VALUES (6, ‘Robert’, 1012, 30);`
(被阻塞,因事务A 的 Gap Lock)
T7 COMMIT; (事务A提交,释放锁)
T8 COMMIT; (事务B提交)

事物 A 在 employee_number 索引上首先会添加 Next-Key Lock,范围是 (-∞, 1001], (1001, 1010], (1010, 1020], employee_number = 1020 的记录不满足 employee_number < 1011,Next-Key Lock (1010, 1020] 会退化为 Gap Lock,所以这里会添加**表级意向共享锁 IS、 Gap Lock,Gap 范围为 (-∞, 1001], (1001, 1010] , (1010, 1020)**,事物 B 在 T6 尝试插入 employee_number = 1012 的记录导致锁冲突,操作被阻塞。

时间线 事务A 事务B
T1 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
T2 START TRANSACTION;
T3 SELECT * FROM employees WHERE employee_number >= 1040 LOCK IN SHARE MODE;
T4 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
T5 START TRANSACTION;
T6 UPDATE employees SET name = 'Robert' WHERE id = 25; (被阻塞,因事务A锁定记录)
T7 COMMIT; (事务A提交,释放锁)
T8 COMMIT; (事务B提交)

事物 A 在 employee_number 索引上首先会添加 Next-Key Lock,范围是 (1035, 1040], (1040, +∞], employee_number = 1040 的记录存在,并且 employee_number 是唯一索引,Next-Key Lock (1035, 1040] 会退化为行记录锁,所以这里会添加**行级共享锁 S、Next-Key Lock 和 表级意向共享锁 IS,行级锁会添加在聚簇索引 id = 25 和 唯一索引 employee_number = 1040 上,Next- Key Lock 范围为 (1040, +∞]**,事物 B 在 T6 尝试更新 id = 25 的记录导致锁冲突,操作被阻塞。

非唯一索引列查询

等值查询

查询有记录

时间线 事务A 事物C
T1 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
T2 START TRANSACTION;
T3 SELECT * FROM employees WHERE age = 25 LOCK IN SHARE MODE;
T4 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
T5 START TRANSACTION;
T6 INSERT INTO employees (name, employee_number, age) VALUES ('Frank', 1050, 26); (被阻塞,因事务A的 Gap Lock)
T7 COMMIT; (事务A提交,释放锁)
T8 COMMIT; (事务B提交)

事物 A 在 age 索引上首先会添加 Next-Key Lock,范围是(∞, 25], 由于 age = 25 的记录存在且 age 是非唯一索引,还需添加 Gap Lock(遍历至下一个不符合条件的 age 索引) 和 对应的行记录锁,所以这里会添加 **Next-Key Lock 、 Gap Lock、行级共享锁 S 和 表级意向共享锁 IS,锁定 age = 25 记录对应的 id = 5 和 id = 14 的聚簇索引记录,索引 age 上的 Next-Key Lock 范围为(∞, 25](age 并非唯一索引,所以这里的 Next-Key Lock 并不会退化为行记录锁),索引 age 上的 Gap Lock 范围为(25, 30)**。事物 B 在 T6 尝试插入 age = 26 的记录导致冲突,操作被阻塞。

下边界插入

下边界如果是闭区间,则无法修改、删除,有机会插入成功,如果是开区间,则可以修改、删除,有机会插入成功。

时间线 事务A 事务B
T1 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
T2 START TRANSACTION;
T3 SELECT * FROM employees WHERE age = 25 LOCK IN SHARE MODE;
T4 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
T5 START TRANSACTION;
T6 INSERT INTO employees (id, name, employee_number, age) VALUES (6, 'Frank', 1050, 30); (插入成功)
T7 COMMIT; (事务A提交,释放锁)
T8 COMMIT; (事务B提交)

插入 age = 30 的记录一定会成功嘛?不一定都会成功。

时间线 事务A 事务B
T1 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
T2 START TRANSACTION;
T3 SELECT * FROM employees WHERE age = 25 LOCK IN SHARE MODE;
T4 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
T5 START TRANSACTION;
T6 INSERT INTO employees (id, name, employee_number, age) VALUES (0, 'Frank', 1050, 30); (阻塞)
T7 COMMIT; (事务A提交,释放锁)
T8 COMMIT; (事务B提交)

我们来分析看看为什么事物 B 此时插入的记录被阻塞,上一条的插入却成功了。普通索引是有序存储的,当普通索引值相同时,则会按照聚簇(主键)索引排序,索引 age 的排序情况如下,括号表示事物 A 在 T3 操作添加的 Gap Lock 范围(25, 30)

index age (25 25 30) 32 35
primary id ( 5 14 1 ) 25 13

我们假设插入 age = 30 和 id = 0 可以成功,那么这条记录会在(25, 30)之间,所以操作是会阻塞的,索引 age 的排序情况应该如下:

index age (25 25 30 ☆ 30) 32 35
primary id ( 5 14 0 ☆ 1 ) 25 13

我们假设插入 age = 30 和 id = 6 可以成功,这条记录是在(25, 30)之外的,所以操作是可以成功的,索引 age 的排序情况应该如下:

index age (25 25 30) 30 ☆ 32 35
primary id ( 5 14 1 ) 1 ☆ 25 13

上边界插入

与下边界规则相同

查询无记录

时间线 事务A 事务B
T1 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
T2 START TRANSACTION;
T3 SELECT * FROM employees WHERE age = 27 LOCK IN SHARE MODE;
T4 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
T5 START TRANSACTION;
T6 INSERT INTO employees (name, employee_number, age) VALUES ('Frank', 1050, 26); (被阻塞,因事务A的 Gap Lock)
T7 COMMIT; (事务A提交,释放锁)
T8 COMMIT; (事务B提交)

事物 A 在 age 索引上首先会添加 Next-Key Lock,范围是(25, 30],由于 age = 27 的记录不存在,Next-Key Lock 会退化为 Gap Lock。所以这里会添加 表级意向共享锁 IS 和 Gap Lock ,索引 age 上的 Gap Lock 范围为(25, 30),事物 B 在 T6 尝试插入 age = 26 的记录导致冲突,操作被阻塞。

范围查询

时间线 事务A 事务B
T1 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
T2 START TRANSACTION;
T3 SELECT * FROM employees WHERE age >= 25 AND age < 30 LOCK IN SHARE MODE;
T4 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
T5 START TRANSACTION;
T6 INSERT INTO employees (name, employee_number, age) VALUES ('Frank', 1050, 26); (被阻塞,因事务A的 Gap Lock)
T7 COMMIT; (事务A提交,释放锁)
T8 COMMIT; (事务B提交)

事物 A 在 age 索引上首先会添加 Next-Key Lock,范围是 (-∞, 25], (25, 30],由于 age 是非唯一索引,所以 Next-Key Lock 不会退化为 Gap Lock 和 行级锁,这里会添加**Next-Key Lock 和 表级意向共享锁 IS,Next- Key Lock 范围为 (-∞, 25], (25, 30]**,事物 B 在 T6 尝试插入 age = 26 的记录导致冲突,操作被阻塞。

无索引列

等值查询

查询有记录

时间线 事务A 事务B
T1 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
T2 START TRANSACTION;
T3 SELECT * FROM employees WHERE name = 'Bob' LOCK IN SHARE MODE;
T4 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
T5 START TRANSACTION;
T6 UPDATE employees SET name = 'Robert' WHERE age = 25; (阻塞)
T7 COMMIT; (事务A提交,释放锁)
T8 COMMIT; (事务B提交)

事物 A 查询的 name = ‘Bob’ 记录是存在的,由于 name 是非索引列,查询会进行全表扫描,所以这里会添加表级**意向共享锁 IS、行级共享锁 S、Next-Key Lock,锁定 id=1、5、13、14、25 的聚簇索引记录,每行聚簇索引都会添加 Next-Key Lock,范围分别为 (-∞,1]、(1,5]、(5,13]、(13,14]、(14,25]、(25,+∞]**,整个表被锁住了,事物 B 的更新操作被阻塞。

查询无记录

如果事物 A 查询的 name = ‘ob’ 记录不存,由于 name 是非索引列,查询也会进行全表扫描,锁添加情况同 name = ‘Bob’ 查询到记录的情况一致 。应该避免这种情况发生,这会严重降低整个系统的处理能力。

范围查找

同样会进行全部扫描,每行记录都会添加Next-Key Lock,最终锁住整个表。

FOR UPDATE

FOR UPDATE 在不同列类型的查询条件下,锁定的记录与 LOCK IN SHARE MODE 一致,只是锁的类型变成了 IX 和 X。

Serializable

SQL 语句 锁情况
SELECT * FROM table WHERE column = ?; 意向共享锁(IS)
和 行级共享锁(S)
或 临键锁(Next- Key Lock)
或 间隙锁(Gap Lock)
SELECT * FROM table WHERE column < ?; 意向共享锁(IS)
和 行级共享锁(S)
或 临键锁(Next- Key Lock)
或 间隙锁(Gap Lock)
SELECT * FROM table WHERE column = ? LOCK IN SHARE MODE; 意向共享锁(IS)
和 行级共享锁(S)
或 临键锁(Next- Key Lock)
或 间隙锁(Gap Lock)
SELECT * FROM table WHERE column < ? LOCK IN SHARE MODE; 意向共享锁(IS)
和 行级共享锁(S)
或 临键锁(Next- Key Lock)
或 间隙锁(Gap Lock)
SELECT * FROM table WHERE column = ? FOR UPDATE; 意向排他锁(IX)
和 行级排他锁(X)
或 临键锁(Next-Key Lock)
或 间隙锁(Gap Lock)
SELECT * FROM table WHERE column < ? FOR UPDATE; 意向排他锁(IX)
和 行级排他锁(X)
或 临键锁(Next-Key Lock)
或 间隙锁(Gap Lock)

在 Serializable 隔离级别下,普通查询与显示 LOCK IN SHARE MODE 查询相同,执行不同 SQL 语句下的加锁情况与在 Repeatable Read 隔离级别下类似。