加锁规则

Tips:

  • 间隙锁在可重复读级别下才有效
  • 临键锁(Next-Key Lock)是由间隙锁和行锁组成的一个左开右闭区间

加锁规则:

  • 原则1:加锁的基本单位是临键锁
  • 原则2:查找过程中访问到的对象才会加锁
  • 优化1:索引上的等值查询,给唯一索引加锁的时候,临键锁退化为行锁
  • 优化2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,临键锁退化为间隙锁
  • bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止(8.0.18 已修复)

实验表结构及数据:

-- version: 8.0.32
DROP DATABASE IF EXISTS mysql45_21;
CREATE DATABASE mysql45_21;
USE mysql45_21;

CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `c` int(11) DEFAULT NULL,
  `d` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `c` (`c`)
) ENGINE=InnoDB;

INSERT INTO t VALUES
(0, 0, 0), (5, 5, 5), (10, 10, 10),
(15, 15, 15), (20, 20, 20), (25, 25, 25);

在可重复读隔离级别下,SELECT 查询使用快照读,是不会加任何读写锁的,下面查询会返回空:

SELECT * FROM performance_schema.data_locks;

索引等值查询临键锁退化为间隙锁

会话1:

-- 原则1:在主键索引查找 id = 7,加锁基本单位是临键锁,加临键锁 (5, 10]
-- 优化2:索引上的等值查询,最右一个值不满足等值条件,间临键锁退化为间隙锁 (5, 10)
BEGIN;
UPDATE t SET d = d + 1 WHERE id = 7;

会话2:

-- 被间隙锁 (5, 10) 阻塞
INSERT INTO t VALUES (8, 8, 8);

会话3:

-- 获取行锁(写锁) id = 10 成功
UPDATE t SET d = d + 1 WHERE id = 10;

非唯一索引等值锁

会话1:

-- 原则1:索引c 是普通索引(非唯一索引),扫描到第一行 c=5 的数据后还有向右扫描,加锁范围 (0, 5], (5, 10]
-- 原则2:覆盖索引不会访问到主键索引,主键索引没有加锁
-- 如果是 SELECT...FOR UPDATE,则会对主键索引上满足条件的行加锁
-- 优化2:索引等值查询,最右一个值不满足条件,临键锁退化为间隙锁 (5, 10)
-- 最终加锁范围:(0, 5], (5, 10)
BEGIN;
SELECT id FROM t WHERE c = 5 LOCK IN SHARE MODE;

会话2:

-- 主键索引没有加锁,更新成功
UPDATE t SET d = d + 1 WHERE id = 5;

会话3:

-- 因为间隙锁 (5, 10),插入失败
INSERT INTO t VALUES (7, 7, 7);

补充:当 会话1 执行 SELECT...FOR UPDATESELECT d FROM t WHERE c = 5 LOCK IN SHARE MODE; (非覆盖索引)后, 以下语句会被阻塞

-- 获取行锁 id=5 被阻塞
UPDATE t SET d = d + 1 WHERE id = 5;

主键索引范围查询

会话1:

-- 原则1:查找 id=10,加锁基本单位,加临键锁 (5, 10]
-- 优化1:唯一索引等值查询,临键锁退化为间隙锁 id=10

-- 原则1:范围查询 id > 10,加临键锁 (10, 15]
-- 虽然这里是范围查询不满足优化2,但在 8.0.32 版本,是会退化为间隙锁 (10, 15) 的

-- 最终加锁范围:行锁 id=10, 间隙锁 (10, 15)
BEGIN;
SELECT * FROM t WHERE id >= 10 AND id < 11 FOR UPDATE;

会话2:

-- 临键锁(5, 10] 退化为 行锁id=10,插入成功
INSERT INTO t VALUES (8, 8, 8);

-- 被间隙锁 (10, 15) 阻塞
INSERT INTO t VALUES (13, 13, 13);

会话3:

-- 临键锁(10, 15] 退化为 间隙锁(10, 15),获得行锁 id=5 成功
UPDATE t SET d = d + 1 WHERE id = 15;

非唯一索引范围查询

会话1:

-- 原则1:查找 c=10 加锁基本单位临键锁 (5, 10]
-- 非唯一索引,不符合 优化1,不会退化为行锁

-- c > 10 范围查询,继续向右扫描,加临键锁 (10, 15]
-- 8.0.32 索引c 不会像主键索引一样,将 (10, 15] 退化为间隙锁,但确实不会对主键 id=15 加锁

-- SELECT...FOR UDPATE,对主键加行锁 id=10

BEGIN;
SELECT * FROM t WHERE c >= 10 AND c < 11 FOR UPDATE;

会话2:

-- 间隙锁 (5, 10) 阻塞
INSERT INTO t VALUES (8, 8, 8);

会话3:

-- 被 索引c 临键锁 (10, 15] 阻塞
UPDATE t SET d = d + 1 WHERE c = 15;

-- 获取主键行锁 id=15 成功
UPDATE t SET d = d + 1 WHERE id = 15;