mysql-锁的算法(记录锁、间隙锁、临键锁)
1.行锁的三种算法
有3种行锁算法,分别是:
- Record Lock:单个行记录上的锁,没有主键,会使用隐式的主键进行锁定
- Gap Lock:间隙锁,锁定一个范围,但不包含记录本身
- Next-Key Lock:Gap Lock + Record Lock,锁定一个范围,并且锁定记录本身
2.锁区间
锁区间:区间从字⾯意思解释就是⼀个范围。
创建时机:mysql的锁区间是在当前会话创建后事物开始时当前数据库已存在数据相邻数据之间两两作为区间边界值。
Record Lock总是会去锁住索引记录,如果InnoDB存储引擎表在建立的时候没有设置任何一个索引,那么这时InnoDB存储引擎会使用隐式的主键来进行锁定。
例如一个索引有10,11,13,和20这四个值,那么该索引可能被Next-Key Locking的区间为:
2.1间隙锁锁区间
触发条件:
- 范围查询落入一个锁区间
- 主键索引或者辅助等值查询不存在,等值落入一个锁区间
- 辅助索引等值查询存在,落入2个锁区间(其中一个是间隙锁锁区间)
可锁定的区间
(-∞, 10)、(10,11)、(11,13)、(13,20)、(20,+∞)
2.2临键锁锁区间
触发条件:
- 当范围查询超过多个间隙锁锁区间
- 辅助索引等值查询存在,落入2个锁区间(其中一个是临键锁锁区间)
Next-Key Lock 是结合了Gap Lock和Record Lock的一种锁定算法,在Next-Key Lock算法下,InnoDB对于行的查询都是采用这种锁定算法。
可锁定的区间
(-∞, 10]、(10,11]、(11,13]、(13,20]、(20,+∞)
采用Next-Key Lock的锁定技术称为Next-key Locking。其设计目的是为了解决Phantom Problem(幻读),而利用这种锁定技术,锁定的不是单个值,而是一个范围,是谓词锁的一种改进。除了next-key locking,还有previous-key locking技术。同样上诉的索引10、11、13和20,若采用previous-可以locking技术, 那么可锁定的区间为:
(-∞, 10)、[10,11)、[11,13)、[13,20)、[20,+∞)
辅助索引锁区间的锁定会造成该区间内所有主键索引记录锁也进行锁定
若事务T1已经通过next-key locking锁定了如下范围:
(10,11]、(11,13]
当插入新的人记录12时,则锁定的范围会变成:
(10,11]、(11,12]、(12,13]
3.记录锁(Record Lock)
当查询的索引还有唯一属性时,InnoDB存储引擎会对Next-key Lock进行优化吗,将其降为Record Lock,即仅锁住索引本身,而不是范围。看下面的例子,首先根据如下代码创建测试表t
DROP TABLE IF EXISTS t;
CREATE TABLE t(a INT PRIMARY KEY);
INSERT INTO t SELECT 1;
INSERT INTO t SELECT 2;
INSERT INTO t SELECT 5;
接着来执行表的SQL语句。
时间 | 会话A | 会话2 |
---|---|---|
1 | BEGIN; | |
2 | SELECT * FROM t WHERE a=5 FOR UPDATE | |
3 | BEGIN; | |
4 | INSERT INTO t serlect 4; | |
5 | COMMIT; #成功,不需要等待 | |
6 | COMMIT |
表t共有1、2、5三个值,在上面的例子中,在会话A中首先对a=5进行X锁定。而由于a是主键且唯一,因此锁定的仅是5这个值而不是(2,5)这个范围,这样在会话B中插入值4而不会阻塞,可以立即插入并返回。即锁定由Next-key Lock 算法降级为了Record Lock,从而提高应用的并发性。
正如前面锁介绍的,Next-Key Lock 降级为Record Lock 仅在查询的列是唯一索引的情况下。若辅助索引,则情况会完全不同。同样,首先根据如下代码创建测试表z:
CREATE TABLE z(a INT, b INT, PRIMARY KEY(a), KEY(b));
insert into z select 1,1;
insert into z select 3,1;
insert into z select 5,3;
insert into z select 7,6;
insert into z select 10,8;
表z的列b是辅助索引,若在会话A中执行下面的SQL语句:
SELECT * FROM z WHERE b=3 FOR UPDATE
很明显,这是SQL语句通过索引列b进行查询,因此其使用传统的Next-Key Locking技术枷锁,并且由于有两个索引,其需要分别进行锁定。对于聚簇索引,其仅对列a等于5的索引加上Record Lock。而对于辅助索引,其加上的是Next-Key Lock,锁定的范围(1,3],特别需要注意的是,InnoDB存储引擎还会对辅助索引下一个键值加上gap Lock,即还有一个辅助索引范围(3,6)的锁。因此,若在新会话B中运行下面sql语句,都会阻塞:
SELECT * FROM z WHERE a=5 LOCK IN SHARE MODE;
INSERT INTO z SELECT 4,2;
INSERT INTO z SELECT 6,7;
UPDATE z SET b=1 WHERE a=5;
第一个和第四个SQL语句不能执行,因为会话A中执行的SQL语句已经对聚簇索引列中a=5的值加上X锁,因此执行会被阻塞。第二个SQL语句,主键插入4,没问题,但是插入的辅助索引值2在锁定的范围(1,3]中,因此执行同样会被阻塞。第三个SQL语句,插入的主键6没有杯锁定,5也不在范围(1,3]之间,但插入的值5在另一个锁定的范围(3,6)中,故同样需要等待。而下面的SQL语句,不会被阻塞,可以立即执行:
INSERT INTO z SELECT 8,6;
INSERT INTO z SELECT 2,0;
INSERT INTO z SELECT 6,7;
从上面的例子中可以看到,Gap Lock的作用是为了组织多个事务将记录插入到同一个范围内,而这会导致Phantom Problem问题的产生。
用户可以通过以下两种方式显式地关闭Gap Lock:
将事务的隔离级别设置为 READ COMMITED
将参数innodb_locks_unsafe_for_binlog设置为1
在上述的配置下,除外键约束和唯一性检查依然需要的Gap Lock,其余情况仅使用Record Lock进行锁定。单需要牢记的是,上述设置破坏了事务的隔离性,并且对于replication,可能会导致主从数据的不一致。此外从性能上来看,READ COMMITED也不会优于默认的事务隔离级别READ REPEATABLE.
在InnoDB存储引擎中,对于INSERT的操作,其会检查插入记录的下一条记录是否被锁定,若已经被锁定,则不允许查询。对于上面的例子,会话A已经锁定了表z中b=3的记录,即锁定了(1,3]的范围,这是若在其他会话中进行如下的插入同样会导致阻塞:
INSERT INTO z SELECT 2,2
因为在辅助索引列b上插入值为2的记录时,会检测到下一个记录3已经被索引。而将加插入修改为如下的值,可以立即执行:
INSERT INTO z SELECT 2,0
最后需再次提醒的时,对于唯一键值的锁定,Next-key Lock 降级为 Record Lock仅存在于查询所有的唯一索引列。若唯一索引由多个列组成,而查询进士查找多个唯一索引列中的其中一个,那么查询其实是range类型查询,而不是point类型查询,故InnoDB存储引擎依然使用 Next-key Lock进行锁定。
4.解释
在上述的配置下,除外键约束和唯一性检查依然需要的Gap Lock,其余情况仅使用Record Lock进行锁定。
现在我们对该句话举例说明
即使在关闭间隙锁(Gap Lock)的情况下,某些操作如外键约束检查和唯一性约束检查仍然可能需要使用间隙锁来确保数据的一致性和完整性。这是因为这些操作需要防止并发事务在特定条件下插入违反约束的新记录。
1. 关闭 Gap Lock
在 MySQL 的 InnoDB 存储引擎中,可以通过设置 innodb_locks_unsafe_for_binlog=ON
或者将事务隔离级别设置为“读已提交”(Read Committed),来禁用范围查询中的间隙锁。这意味着对于普通的 SELECT ... FOR UPDATE
或 SELECT ... LOCK IN SHARE MODE
查询,InnoDB 不会对索引间隙加锁。
然而,为了维护数据库的完整性和一致性,某些特殊场景下的操作依然会使用间隙锁。
2. 外键约束与 Gap Lock
当一个表存在外键约束时,InnoDB 需要确保父表中的相关记录不能被删除或更新,除非子表中没有依赖这些记录的数据。即使关闭了 Gap Lock,InnoDB 也会对外键约束相关的操作加间隙锁,以防止违反外键约束的情况发生。
示例
假设我们有两个表:orders
和 customers
,其中 orders
表有一个外键引用 customers
表的 id
字段。
CREATE TABLE customers (
id INT PRIMARY KEY,
name VARCHAR(50)
);
CREATE TABLE orders (
order_id INT PRIMARY KEY,
customer_id INT,
amount DECIMAL(10, 2),
FOREIGN KEY (customer_id) REFERENCES customers(id)
);
现有数据如下:
customers
表:
id | name |
---|---|
1 | Alice |
3 | Bob |
orders
表:
order_id | customer_id | amount |
---|---|---|
1 | 1 | 100.00 |
2 | 3 | 200.00 |
事务 A
尝试删除 customers
表中的一个客户:
BEGIN;
DELETE FROM customers WHERE id = 2;
尽管 id = 2
的记录不存在,InnoDB 仍会在 customers
表的 id
列上对 (1, 3)
这个间隙加间隙锁。这是为了防止其他事务在这个间隙中插入新的订单记录(例如 customer_id = 2
),否则这会导致外键约束失效。
事务 B
尝试插入一个新的订单记录:
INSERT INTO orders (order_id, customer_id, amount) VALUES (3, 2, 150.00); -- 阻塞,因为 `customer_id = 2` 在间隙 (1, 3) 中
事务 B 的插入操作会被事务 A 持有的间隙锁阻塞,直到事务 A 提交或回滚。
3. 唯一性约束与 Gap Lock
当表中定义了唯一性约束(Unique Constraint)时,InnoDB 需要确保不会插入重复的值。即使关闭了 Gap Lock,InnoDB 也会对唯一性约束相关的操作加间隙锁,以防止并发事务插入相同的值。
示例
假设有一个表 users
,其中包含一个唯一性约束:
CREATE TABLE users (
id INT PRIMARY KEY,
email VARCHAR(50),
UNIQUE KEY uk_email (email)
);
现有数据如下:
users
表:
id | |
---|---|
1 | alice@example.com |
2 | bob@example.com |
事务 A
尝试插入一个新的用户记录:
BEGIN;
INSERT INTO users (id, email) VALUES (3, 'charlie@example.com');
此时,InnoDB 会在 email
列上对相应的间隙加间隙锁,以防止其他事务同时插入相同的 email
值。
事务 B
尝试插入另一个用户记录:
INSERT INTO users (id, email) VALUES (4, 'charlie@example.com'); -- 阻塞,因为 `charlie@example.com` 在间隙中
事务 B 的插入操作会被事务 A 持有的间隙锁阻塞,直到事务 A 提交或回滚。如果事务 A 提交且插入成功,则事务 B 将失败并抛出唯一性约束冲突错误。
4. 总结
-
外键约束:即使关闭了 Gap Lock,InnoDB 仍然会在涉及外键约束的操作中使用间隙锁,以确保父表中的记录不会被删除或更新,除非子表中没有依赖这些记录的数据。
-
唯一性约束:同样地,即使关闭了 Gap Lock,InnoDB 也会在涉及唯一性约束的操作中使用间隙锁,以防止并发事务插入重复的值。
这些特殊的锁定机制是为了保证数据库的完整性和一致性,避免并发事务导致的数据不一致问题。因此,在设计数据库架构和编写事务逻辑时,需要考虑到这些隐含的锁定行为。