InnoDB的锁机制原理,通俗易懂

InnoDB的锁机制原理

一、知识回顾

MySQL 的锁机制是其实现并发控制的核心,用于保证数据的一致性和完整性。锁可以从多个维度进行分类,最常见的是根据锁的粒度(范围)锁的模式(行为) 以及 锁的兼容性 来划分。以下是主要的分类:

1. 按锁的模式(行为/兼容性)分类

  1. 共享锁(Shared Lock , S Lock)

    • 行为:又称读锁,允许其他事务同时读取被锁定的资源(数据行,表),但不允许其他事务获取该资源的排他锁 (即阻止其他事务写入)

    • 兼容性: 多个事务可以同时持有同一资源上的共享锁。

    • 加锁方式:

      SELECT ... LOCK IN SHARE MODE (显式)

      SELECT ... FOR SHARE (MySQL 8.0+ 语法,更推荐)

  2. 排他锁 (Exclusive Lock, X Lock)

    • 行为:又称写锁,阻止其他事务同时读取被锁定的资源(数据行,表),即阻止其他事务读取(脏读除外)和写入。

    • 兼容性: 同一时间只有一个事务能持有某个资源的排他锁

    • 加锁方式:

      SELECT ... LOCK FOR UPDATE (显式)

      UPDATE, DELETE, INSERT 语句在操作涉及的行上会自动加排他锁(隐式)

2. 按锁的粒度(范围)分类

  1. 全局锁:
  • 作用范围: 锁定整个 MySQL 实例的所有数据库。

  • **命令:**FLUSH TABLES WITH READ LOCK

  • 效果: 使整个实例处于只读状态。所有数据修改操作(DML)和表结构修改操作(DDL)都会被阻塞。

  • 用途: 主要用于做全库逻辑备份(如 mysqldump 时使用 --single-transaction 参数通常更好,因为它使用 MVCC 不阻塞写)。非常重量级,会严重影响业务,慎用。

  1. 表级锁:

    表锁(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)冲突。
  2. 行级锁:

    • 作用范围: 锁定表中的单行或多行记录。这是 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 非唯一索引执行流程如下:

事务A:更新操作
使用索引?
在索引上加Next-Key Lock
在索引后间隙加Gap Lock
在主键加记录锁
全表Next-Key Lock
事务B:插入操作
索引值是否在锁定区间?
阻塞
允许
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)

锁信息解读

  1. 表级锁
    • TABLE; IX; GRANTED:意向排他锁,表示事务准备修改表中的某些行
  2. 非唯一索引锁(real_name)
    • RECORD; X; GRANTED; '王奕值', 8:Next-Key Lock(临键锁),锁定索引记录和间隙
    • RECORD; X,GAP; GRANTED; '舒创', 15:间隙锁,锁定索引值之间的区间
  3. 主键索引锁
    • RECORD; X,REC_NOT_GAP; GRANTED; 8:记录锁,仅锁定主键id=8的行

为什么需要三种锁?

  1. Next-Key Lock
    • 防止其他事务修改当前找到的记录(id=8)
    • 防止在记录前插入新值(保持当前记录的"前间隙"不变)
  2. Gap Lock
    • 防止在 '王奕值''舒创' 之间插入新记录
    • 避免幻读(Phantom Read)
    • 保证在事务期间执行相同查询会返回一致结果
  3. 主键记录锁
    • 确保实际数据行的独占访问
    • 允许安全地更新行数据

对并发操作的影响

操作类型示例是否被阻塞原因
插入操作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_nameidposition
124在区间前
c22在区间前
chaoge27在区间前
xuexiaohghfghfg26在区间前
刘志17在区间前
发光时代dd23在区间前
发送18在区间前
20在区间前
张刘四12在区间前
弹弹弹ddd21在区间前
16在区间前
测试坦诚据19在区间前
王奕值8在区间前
舒创15边界值
25在区间后
谭12328在区间后
谭测试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;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值