InnoDB的锁机制原理
一、知识回顾
MySQL 的锁机制是其实现并发控制的核心,用于保证数据的一致性和完整性。锁可以从多个维度进行分类,最常见的是根据锁的粒度(范围)、锁的模式(行为) 以及 锁的兼容性 来划分。以下是主要的分类:
1. 按锁的模式(行为/兼容性)分类
-
共享锁(Shared Lock , S Lock)
-
行为:又称读锁,允许其他事务同时读取被锁定的资源(数据行,表),但不允许其他事务获取该资源的排他锁 (即阻止其他事务写入)
-
兼容性: 多个事务可以同时持有同一资源上的共享锁。
-
加锁方式:
SELECT ... LOCK IN SHARE MODE
(显式)
SELECT ... FOR SHARE
(MySQL 8.0+ 语法,更推荐)
-
-
排他锁 (Exclusive Lock, X Lock)
-
行为:又称写锁,阻止其他事务同时读取被锁定的资源(数据行,表),即阻止其他事务读取(脏读除外)和写入。
-
兼容性: 同一时间只有一个事务能持有某个资源的排他锁
-
加锁方式:
SELECT ... LOCK FOR UPDATE
(显式)
UPDATE
,DELETE
,INSERT
语句在操作涉及的行上会自动加排他锁(隐式)
-
2. 按锁的粒度(范围)分类
- 全局锁:
-
作用范围: 锁定整个 MySQL 实例的所有数据库。
-
**命令:**FLUSH TABLES WITH READ LOCK
-
效果: 使整个实例处于只读状态。所有数据修改操作(DML)和表结构修改操作(DDL)都会被阻塞。
-
用途: 主要用于做全库逻辑备份(如
mysqldump
时使用--single-transaction
参数通常更好,因为它使用 MVCC 不阻塞写)。非常重量级,会严重影响业务,慎用。
-
表级锁:
表锁(Table Lock):
- 作用范围: 锁定整个表
- 命令:
LOCK TABLES table_name [READ | WRITE]
(显式锁,一般少用) / MySQL Server 层在某些语句执行时自动加。 - 读锁(共享锁): 允许其他会话读该表,但阻塞其他会话写该表(包括加写锁)。
- 写锁(排他锁): 阻塞其他会话对该表的所有读写操作(包括加读锁和写锁)。
- 用途: 主要用于做全库逻辑备份(如
mysqldump
时使用--single-transaction
参数通常更好,因为它使用 MVCC 不阻塞写)。非常重量级,会严重影响业务,慎用。
元数据锁(Metadata Lock, MDL):
- 作用范围: 保证表结构(元数据)的一致性。当对一个表做 CRUD 操作(DML)时,加 MDL 读锁;当要对表结构做变更(DDL)时,加 MDL 写锁。
- 命令: 无 , MySQL Server 层自动管理,无需用户干预。
- 冲突: MDL 读锁之间不互斥;MDL 写锁与 MDL 读锁、MDL 写锁都互斥。DDL 操作需要等待所有正在执行的 DML 事务释放 MDL 读锁,反之新的 DML 操作会等待 DDL 操作释放 MDL 写锁。长时间未提交的事务持有 MDL 锁可能导致 DDL 操作阻塞甚至拖垮数据库
意向锁(Intention Lock):
- 目的: 表级锁,由 InnoDB 引擎引入,用于快速判断表中是否有更细粒度(行级)的锁存在,避免为了检查行锁而遍历所有行。
- 意向共享锁(IS): 表示事务打算在表中的某些行上加共享锁(S Lock)。
- 意向排他锁(IX): 表示事务打算在表中的某些行上加排他锁(X Lock)。
- 兼容性: 意向锁之间相互兼容(IS 和 IX 不互斥),但它们会与普通的表级共享锁(S)和排他锁(X)冲突。
-
行级锁:
- 作用范围: 锁定表中的单行或多行记录。这是 InnoDB 存储引擎 支持且最常用的锁粒度,提供了最高的并发度。
常见类型(InnoDB 实现):
-
记录锁(Record Lock):
锁定索引记录本身。如果表没有定义索引,InnoDB 会创建一个隐藏的聚簇索引,记录锁就作用在这个隐藏索引上。
保证锁定的行不能被其他事务修改或删除。
-
间隙锁(Gap Lock):
锁定索引记录之间的间隙(开区间),或者第一个索引记录之前或最后一个索引记录之后的间隙。
目的: 防止其他事务在间隙中插入新的记录(解决幻读问题的一种手段)。
兼容性: 间隙锁之间不冲突(不同事务可以锁定同一个间隙)。
- 临键锁(Next-Key Lock):
记录锁 + 间隙锁 的组合。锁定一个索引记录以及该记录之前的间隙(左开右闭区间)。
目的: InnoDB 默认的行锁算法。既防止了幻读(通过间隙锁部分),又能锁定记录本身(通过记录锁部分)。
- 插入意向锁(Insert Intention Lock):
特殊的间隙锁。在 INSERT 操作插入行之前设置。
目的: 表示一个事务打算在某个间隙中插入新行。多个事务只要插入的位置(间隙)不冲突(即使在同一间隙内插入不同的位置),它们可以同时持有插入意向锁。插入意向锁会等待已有的间隙锁或临键锁释放。
3. 总结汇总如下:
分类维度 | 锁类型 | 描述 | 主要实现/命令 | 特点/用途 |
---|---|---|---|---|
粒度 | 全局锁 | 锁整个实例 | FLUSH TABLES WITH READ LOCK | 全库备份,重量级 |
表级锁 | 锁整张表 | |||
- 表锁 | 显式表锁 | LOCK TABLES ... READ/WRITE | 读写互斥 | |
- MDL | 元数据锁 | 自动 | 保护表结构,DML vs DDL | |
- 意向锁 | 表级意向锁 | InnoDB 自动 | 指示行锁意图,加速冲突检查 | |
行级锁 | 锁单行或多行 | InnoDB 特有 | 高并发 | |
- 记录锁 | 锁索引记录 | UPDATE/DELETE/SELECT FOR UPDATE 等 | 防止修改/删除 | |
- 间隙锁 | 锁索引间隙 | 自动 | 防止插入,解决幻读 | |
- 临键锁 | 记录锁+间隙锁 | InnoDB 默认行锁算法 | 解决幻读和记录锁定 | |
- 插入意向锁 | 特殊间隙锁 | INSERT | 表示在间隙插入意图,等待间隙锁释放 | |
模式/行为 | 共享锁 (S) | 允许读,阻塞写 | SELECT ... LOCK IN SHARE MODE / FOR SHARE | 多个事务可同时持有 |
排他锁 (X) | 阻塞读写 | SELECT ... FOR UPDATE / UPDATE / DELETE | 独占锁 | |
其他概念 | 乐观锁 | 应用层策略,基于版本号 | UPDATE ... WHERE version = old_version | 无锁读,更新时检查版本 |
悲观锁 | 先获取数据库锁 | 使用 FOR UPDATE 等 | 强一致,可能降低并发 | |
死锁 | 事务循环等待 | InnoDB 自动检测并回滚一个事务 |
二、测试表以及数据
主键索引 ID
非唯一索引 real_name
CREATE TABLE `user` (
`id` BIGINT NOT NULL COMMENT 'id',
`user_name` VARCHAR(20) NOT NULL COMMENT '姓名' COLLATE 'utf8mb4_bin',
`mobile` VARCHAR(16) NULL DEFAULT NULL COMMENT '手机号码 联系方式' COLLATE 'utf8mb4_bin',
`real_name` VARCHAR(20) NOT NULL COMMENT '真实姓名' COLLATE 'utf8mb4_bin',
`enable` INT NULL DEFAULT '1' COMMENT '0:禁用,1:启用',
`job_title` VARCHAR(32) NULL DEFAULT NULL COMMENT '职位' COLLATE 'utf8mb4_bin',
`user_type` VARCHAR(16) NULL DEFAULT NULL COMMENT '用户类型 管理员 ADMIN , 运营 OPERATE, 助教 ASSISTANT, 学校管理员SCHOOL_ADMIN' COLLATE 'utf8mb4_bin',
`create_at` DATETIME NOT NULL COMMENT '创建时间',
`create_by` BIGINT NOT NULL COMMENT '创建人',
`update_at` DATETIME NOT NULL COMMENT '最后修改时间',
`update_by` BIGINT NOT NULL COMMENT '最后修改人',
PRIMARY KEY (`id`) USING BTREE,
INDEX `real_name` (`real_name`) USING BTREE
)
COMMENT='用户表\r\n'
COLLATE='utf8mb4_bin'
ENGINE=InnoDB;
INSERT INTO `user` (`id`, `user_name`, `mobile`, `real_name`, `enable`, `job_title`, `user_type`, `create_at`, `create_by`, `update_at`, `update_by`) VALUES (1, '超管2', '1222', '超管', 1, NULL, 'ADMIN', '2023-05-11 10:02:30', 0, '2023-05-18 11:05:49', 1656479863111929856);
INSERT INTO `user` (`id`, `user_name`, `mobile`, `real_name`, `enable`, `job_title`, `user_type`, `create_at`, `create_by`, `update_at`, `update_by`) VALUES (8, '超管2', '1521317', '王奕值', 1, NULL, 'OPERATE', '2023-05-17 10:24:18', 1656479863111929856, '2023-05-17 10:24:18', 1656479863111929856);
INSERT INTO `user` (`id`, `user_name`, `mobile`, `real_name`, `enable`, `job_title`, `user_type`, `create_at`, `create_by`, `update_at`, `update_by`) VALUES (12, 'zlq', '15213', '张刘四', 1, NULL, 'OPERATE', '2023-05-17 10:24:42', 1656479863111929856, '2023-05-17 10:24:42', 1656479863111929856);
INSERT INTO `user` (`id`, `user_name`, `mobile`, `real_name`, `enable`, `job_title`, `user_type`, `create_at`, `create_by`, `update_at`, `update_by`) VALUES (15, 'sc', '1521', '舒创', 1, NULL, 'OPERATE', '2023-05-17 10:26:25', 1656479863111929856, '2023-05-18 19:38:22', 1656479863111929856);
INSERT INTO `user` (`id`, `user_name`, `mobile`, `real_name`, `enable`, `job_title`, `user_type`, `create_at`, `create_by`, `update_at`, `update_by`) VALUES (16, 'lp', '153198', '李', 1, NULL, 'OPERATE', '2023-05-17 10:43:34', 1656479863111929856, '2023-05-17 10:43:34', 1656479863111929856);
INSERT INTO `user` (`id`, `user_name`, `mobile`, `real_name`, `enable`, `job_title`, `user_type`, `create_at`, `create_by`, `update_at`, `update_by`) VALUES (17, 'apptest', '155789', '刘志', 1, NULL, 'OPERATE', '2023-05-17 17:04:32', 1656479863111929856, '2023-06-20 16:47:25', 1656479863111929856);
INSERT INTO `user` (`id`, `user_name`, `mobile`, `real_name`, `enable`, `job_title`, `user_type`, `create_at`, `create_by`, `update_at`, `update_by`) VALUES (18, 'test3', '1858034', '发送', 1, NULL, 'SCHOOL_ADMIN', '2023-05-17 21:46:40', 1656479863111929856, '2023-06-02 18:35:27', 1656479863111929856);
INSERT INTO `user` (`id`, `user_name`, `mobile`, `real_name`, `enable`, `job_title`, `user_type`, `create_at`, `create_by`, `update_at`, `update_by`) VALUES (19, 'nishishuia', '1858034', '测试坦诚据', 0, NULL, 'SCHOOL_ADMIN', '2023-05-17 21:58:20', 1656479863111929856, '2023-05-18 14:34:18', 1659077948723904512);
INSERT INTO `user` (`id`, `user_name`, `mobile`, `real_name`, `enable`, `job_title`, `user_type`, `create_at`, `create_by`, `update_at`, `update_by`) VALUES (20, 'ggg1', '1858034', '张', 0, NULL, 'ASSISTANT', '2023-05-17 22:01:41', 1656479863111929856, '2023-05-18 14:12:08', 1659077948723904512);
INSERT INTO `user` (`id`, `user_name`, `mobile`, `real_name`, `enable`, `job_title`, `user_type`, `create_at`, `create_by`, `update_at`, `update_by`) VALUES (21, 'test2', '', '弹弹弹ddd', 1, NULL, 'ASSISTANT', '2023-05-17 23:00:55', 1656479863111929856, '2023-06-05 16:06:50', 1656479863111929856);
INSERT INTO `user` (`id`, `user_name`, `mobile`, `real_name`, `enable`, `job_title`, `user_type`, `create_at`, `create_by`, `update_at`, `update_by`) VALUES (22, 'clh', '', 'c', 1, NULL, 'OPERATE', '2023-05-18 14:06:22', 1656479863111929856, '2023-05-19 15:52:56', 1656479863111929856);
INSERT INTO `user` (`id`, `user_name`, `mobile`, `real_name`, `enable`, `job_title`, `user_type`, `create_at`, `create_by`, `update_at`, `update_by`) VALUES (23, 'ff333', '', '发光时代dd', 1, NULL, 'OPERATE', '2023-05-18 19:34:32', 1656479863111929856, '2023-05-18 20:07:23', 1656479863111929856);
INSERT INTO `user` (`id`, `user_name`, `mobile`, `real_name`, `enable`, `job_title`, `user_type`, `create_at`, `create_by`, `update_at`, `update_by`) VALUES (24, 'hgh', '', '1', 1, NULL, 'ASSISTANT', '2023-05-18 19:36:47', 1656479863111929856, '2023-06-02 10:58:33', 1656479863111929856);
INSERT INTO `user` (`id`, `user_name`, `mobile`, `real_name`, `enable`, `job_title`, `user_type`, `create_at`, `create_by`, `update_at`, `update_by`) VALUES (25, 'tcj', '18888888', '谭', 1, NULL, 'OPERATE', '2023-05-18 19:44:31', 1656479863111929856, '2023-05-22 17:49:23', 1659163048132145152);
INSERT INTO `user` (`id`, `user_name`, `mobile`, `real_name`, `enable`, `job_title`, `user_type`, `create_at`, `create_by`, `update_at`, `update_by`) VALUES (26, 'xuexiao', '', 'xuexiaohghfghfg', 1, NULL, 'SCHOOL_ADMIN', '2023-05-21 14:54:35', 1656479863111929856, '2023-05-21 18:53:04', 1656479863111929856);
INSERT INTO `user` (`id`, `user_name`, `mobile`, `real_name`, `enable`, `job_title`, `user_type`, `create_at`, `create_by`, `update_at`, `update_by`) VALUES (27, 'chaoge', '138831', 'chaoge', 1, NULL, 'ASSISTANT', '2023-05-22 16:18:56', 1656479863111929856, '2023-05-26 12:40:43', 1656479863111929856);
INSERT INTO `user` (`id`, `user_name`, `mobile`, `real_name`, `enable`, `job_title`, `user_type`, `create_at`, `create_by`, `update_at`, `update_by`) VALUES (28, 'tant', '', '谭123', 1, NULL, 'ASSISTANT', '2023-05-22 17:54:04', 1656479863111929856, '2023-05-22 20:26:45', 1656479863111929856);
INSERT INTO `user` (`id`, `user_name`, `mobile`, `real_name`, `enable`, `job_title`, `user_type`, `create_at`, `create_by`, `update_at`, `update_by`) VALUES (29, 'tantest', '1858888', '谭测试', 1, NULL, 'ASSISTANT', '2023-06-02 17:46:49', 1656479863111929856, '2023-06-05 14:08:21', 1656479863111929856);
INSERT INTO `user` (`id`, `user_name`, `mobile`, `real_name`, `enable`, `job_title`, `user_type`, `create_at`, `create_by`, `update_at`, `update_by`) VALUES (30, 'tancj', '333', '非凡', 1, NULL, 'ASSISTANT', '2023-06-09 12:30:42', 1656479863111929856, '2023-06-09 12:32:52', 1656479863111929856);
1 非唯一索引执行流程如下:
1.非唯一索引且数据存在:
步骤1:开启事务
##real_name非唯一索引,数据存在
BEGIN;
UPDATE user SET user_name='超管2' WHERE real_name = '王奕值2';
步骤2:查询加锁情况
performance_schema.data_locks 表有详细的统计
执行SQL: SELECT * FROM performance_schema.data_locks
; 结果如下:
我们重点关注 LOCK_MODE,LOCAK_DATA字段,可以看到 同时添加了 临键锁(X),间隙锁( X,GAP), 记录锁(X,REC_NOT_GAP),表级锁(IX)
锁信息解读
- 表级锁:
TABLE; IX; GRANTED
:意向排他锁,表示事务准备修改表中的某些行
- 非唯一索引锁(
real_name
):RECORD; X; GRANTED; '王奕值', 8
:Next-Key Lock(临键锁),锁定索引记录和间隙RECORD; X,GAP; GRANTED; '舒创', 15
:间隙锁,锁定索引值之间的区间
- 主键索引锁:
RECORD; X,REC_NOT_GAP; GRANTED; 8
:记录锁,仅锁定主键id=8的行
为什么需要三种锁?
- Next-Key Lock:
- 防止其他事务修改当前找到的记录(id=8)
- 防止在记录前插入新值(保持当前记录的"前间隙"不变)
- Gap Lock:
- 防止在
'王奕值'
和'舒创'
之间插入新记录 - 避免幻读(Phantom Read)
- 保证在事务期间执行相同查询会返回一致结果
- 防止在
- 主键记录锁:
- 确保实际数据行的独占访问
- 允许安全地更新行数据
对并发操作的影响
操作类型 | 示例 | 是否被阻塞 | 原因 |
---|---|---|---|
插入操作 | INSERT ... real_name='王奕值' | ✅ 阻塞 | 被Next-Key Lock和Gap Lock阻止 |
更新相同行 | UPDATE ... WHERE id=8 | ✅ 阻塞 | 主键记录锁(X,REC_NOT_GAP) |
更新其他行 | UPDATE ... WHERE id=15 | ❌ 允许 | 不影响其他行 |
读操作 | SELECT ... | ❌ 允许 | 快照读不受影响 |
范围查询 | SELECT ... FOR UPDATE WHERE real_name>'A' | ✅ 部分阻塞 | 可能冲突的区间被阻塞 |
步骤3:有了上面的理论基础,我们来验证,开启事务B
事务A先不提交,然后开启事务B
验证更新相同的行:结果阻塞,等待事务超时,截图如下:
验证更新其他行:执行成功,未阻塞,截图如下:
这里可能有同学会问,这个间隙锁(开区间)加锁的区间是怎么样的,而且本文的索引内容是中文,肉眼也没法判断,这里超哥给大家一个判断区间的方法,在上面的例子中,查询锁信息间隙锁显示为 '舒创', 15
,那么可以用sql来计算SQL如下:
SELECT
real_name,
id,
CASE
WHEN real_name < '舒创' THEN '在区间前'
WHEN real_name = '舒创' THEN '边界值'
ELSE '在区间后'
END AS position
FROM user
ORDER BY real_name;
结果如下:
real_name | id | position |
---|---|---|
1 | 24 | 在区间前 |
c | 22 | 在区间前 |
chaoge | 27 | 在区间前 |
xuexiaohghfghfg | 26 | 在区间前 |
刘志 | 17 | 在区间前 |
发光时代dd | 23 | 在区间前 |
发送 | 18 | 在区间前 |
张 | 20 | 在区间前 |
张刘四 | 12 | 在区间前 |
弹弹弹ddd | 21 | 在区间前 |
李 | 16 | 在区间前 |
测试坦诚据 | 19 | 在区间前 |
王奕值 | 8 | 在区间前 |
舒创 | 15 | 边界值 |
谭 | 25 | 在区间后 |
谭123 | 28 | 在区间后 |
谭测试 | 29 | 在区间后 |
超管 | 1 | 在区间后 |
非凡 | 30 | 在区间后 |
则锁定区间为: (王奕值,舒创 ),如果插入王奕值1则会阻塞
BEGIN;
INSERT INTO `user` (`id`, `user_name`, `mobile`, `real_name`, `enable`, `job_title`, `user_type`, `create_at`, `create_by`, `update_at`, `update_by`) VALUES (32, '超管', '12222222222', '王奕值1', 1, NULL, 'ADMIN', '2023-05-11 10:02:30', 0, '2023-05-18 11:05:49', 1656479863111929856);
COMMIT;