当前位置: 首页 > news >正文

MySQL 锁机制

一、锁机制的总体分类

MySQL 中的锁用于管理对共享资源的并发访问。理解其分类有助于选择合适的策略。

1. 从加锁粒度分类

锁定的数据范围大小。

  • 表级锁 (Table-level Lock):
    锁定整张数据表。
    优点:实现简单,开销小,加锁快,不会出现死锁。
    缺点:锁定粒度最大,并发冲突概率最高,并发性能最低。
    适用引擎:MyISAM (主要使用), InnoDB (特定情况下也会使用, 如 LOCK TABLES 或某些 DDL)。
    示例(显式加表锁):
-- 加写锁,其他会话无法读写
LOCK TABLES my_table WRITE;
-- 加读锁,其他会话可读但不可写,当前会话也不可写
LOCK TABLES my_table READ;
-- 释放当前会话持有的所有表锁
UNLOCK TABLES;
  • 行级锁 (Row-level Lock):
    仅锁定操作涉及的行记录(更准确地说是索引记录)。
    优点:锁定粒度最小,并发冲突概率最低,并发性能最好。
    缺点:实现复杂,开销较大,加锁较慢,可能会出现死锁。
    适用引擎:InnoDB (主要使用)。

  • 页级锁 (Page-level Lock):
    锁定数据页(InnoDB 默认页大小 16KB)。粒度和开销介于表锁和行锁之间。
    适用引擎:BDB (BerkeleyDB) 引擎(现在较少使用)。

2. 从加锁模式分类

锁的状态或类型,决定了兼容性。

  • 共享锁 (Shared Lock / S锁 / 读锁):
    事务持有 S 锁可以读取数据,但不能修改。
    多个事务可以同时持有同一资源的 S 锁(读读不阻塞)。
    其他事务不能获取该资源的 X 锁(读阻塞写)。
    获取方式(显式): SELECT ... LOCK IN SHARE MODE;

  • 排他锁 (Exclusive Lock / X锁 / 写锁):
    事务持有 X 锁可以读取和修改数据。
    同一时间只能有一个事务持有该资源的 X 锁。
    其他事务不能获取该资源的 S 锁或 X 锁(写阻塞读写)。
    获取方式(显式): SELECT ... FOR UPDATE; (隐式): INSERT, UPDATE, DELETE 操作会自动加 X 锁。

3. 从加锁方式分类

是用户主动请求还是系统自动添加。

  • 显式加锁:
    用户通过特定 SQL 语句明确请求加锁。
    示例: LOCK TABLES, UNLOCK TABLES, SELECT ... LOCK IN SHARE MODE, SELECT ... FOR UPDATE

  • 隐式加锁:
    由存储引擎根据事务隔离级别和执行的 SQL 语句自动添加,用户无需干预。这是 InnoDB 最常见的加锁方式。
    示例: 执行 UPDATE products SET stock = stock - 1 WHERE id = 10; 时,InnoDB 会自动在 id=10 的行(索引)上加 X 锁。


二、InnoDB 锁机制详解(核心)

InnoDB 是支持事务和行级锁的主流存储引擎。

1. 行级锁

InnoDB 行锁是基于索引实现的。如果查询条件未使用索引,可能导致全表扫描,锁定所有行,性能下降。

  • Record Lock (记录锁):
    锁定单个索引记录。这是最基本的行锁。
    示例:
-- 假设 id 是主键或唯一索引
-- 事务 T1 执行:
SELECT * FROM users WHERE id = 5 FOR UPDATE;
-- InnoDB 会在 id=5 的索引记录上加一个 X 记录锁。
-- 其他事务无法修改或删除 id=5 的行,也无法获取 id=5 的 S 锁。
  • Gap Lock (间隙锁):
    锁定索引记录之间的“间隙”,防止其他事务在这个间隙中插入新记录。它不锁定记录本身。主要用于 REPEATABLE READ 隔离级别防止幻读。
    示例 (概念):
-- 假设 age 索引上有值 20, 30
-- 事务 T1 执行 (RR 级别):
SELECT * FROM users WHERE age = 25 FOR UPDATE;
-- InnoDB 可能会锁定 (20, 30) 这个间隙,防止其他事务插入 age=25 或其他在 (20,30) 范围内的记录。
-- 注意:Gap Lock 本身不互斥,不同事务可以持有相同间隙的 Gap Lock。
  • Next-Key Lock (临键锁):
    Record Lock + Gap Lock 的组合。锁定一个索引记录以及该记录之前的间隙。这是 InnoDB 在 REPEATABLE READ 隔离级别下的默认锁策略,用于防止幻读。
    示例 (概念):
-- 假设 age 索引上有值 20, 30, 40
-- 事务 T1 执行 (RR 级别):
SELECT * FROM users WHERE age = 30 FOR UPDATE;
-- InnoDB 会锁定 age=30 这条记录 (Record Lock),同时锁定 (20, 30] 这个区间 (Next-Key Lock)。
-- 这意味着其他事务不能插入 age 在 (20, 30] 范围内的记录,也不能修改或删除 age=30 的记录。

2. 表级锁(InnoDB支持但不常用)

  • LOCK TABLES ... WRITE/READ: 显式表锁,会极大降低并发,不推荐在 InnoDB 中常规使用。
  • 自动表锁场景: DDL 操作(如 ALTER TABLE)会获取表级元数据锁 (MDL),阻塞 DML。某些特殊插入(如涉及 AUTO_INCREMENT 的复杂插入)可能短暂持有表级锁。

3. 意向锁(Intention Locks)

意向锁是 表级锁,用于指示事务 打算 在表中的某些行上加什么类型的锁(S 或 X)。它们不直接阻塞行锁,而是用于快速判断表级锁和行级锁的兼容性。

  • 意向共享锁 (IS): 事务准备在某些行上加 S 锁。获取行 S 锁前必须先获取表的 IS 锁。
  • 意向排他锁 (IX): 事务准备在某些行上加 X 锁。获取行 X 锁前必须先获取表的 IX 锁。

工作原理:当一个事务想获取整个表的 S 锁或 X 锁时,只需检查表上是否存在冲突的意向锁(如获取表 X 锁要检查是否有 IS 或 IX 锁),而无需扫描每一行,提高了效率。IS 与 IX 锁是兼容的。

4. 自动锁定行为

  • SELECT ... FOR UPDATE: 对匹配行加 X 锁(Record / Next-Key)。
  • SELECT ... LOCK IN SHARE MODE: 对匹配行加 S 锁(Record / Next-Key)。
  • INSERT: 在新插入行上加 X 锁。可能产生 Gap/Next-Key 锁以防唯一键冲突或幻读。
  • UPDATE: 对匹配的行加 X 锁。
  • DELETE: 对匹配的行加 X 锁。

三、事务与锁的关系

锁是实现 ACID 中隔离性 (Isolation) 的关键技术。

1. 四种事务隔离级别

隔离级别脏读不可重复读幻读InnoDB RR 典型锁策略 (简化)
READ UNCOMMITTED可能发生 ✅可能发生 ✅可能发生 ✅基本无锁 (很少用)
READ COMMITTED不会发生 ❌可能发生 ✅可能发生 ✅Record Lock (锁记录)
REPEATABLE READ不会发生 ❌不会发生 ❌基本不发生 ❌Next-Key Lock (锁记录+间隙)
SERIALIZABLE不会发生 ❌不会发生 ❌不会发生 ❌可能使用表锁/更强范围锁

2. 幻读与间隙锁
REPEATABLE READ 级别,InnoDB 主要通过 Next-Key Lock (包含 Gap Lock) 来防止幻读。通过锁定查询范围内的间隙,阻止了其他事务插入满足该范围条件的新行。


四、锁冲突与兼容性

1. 锁兼容性矩阵(简化版,行锁与意向锁)

请求锁 \ 已持有锁ISIXS (行)X (行)
IS兼容兼容兼容冲突
IX兼容兼容冲突冲突
S (行)兼容冲突兼容冲突
X (行)冲突冲突冲突冲突

(表级 S/X 锁与行级锁的兼容性通过意向锁判断)


五、死锁与事务管理

1. 死锁产生条件 (需同时满足)

  • 互斥条件: 资源独占。
  • 占有且等待: 持有资源同时请求其他资源。
  • 非抢占: 不能强行剥夺已持有资源。
  • 循环等待: 事务间形成等待资源的闭环。

2. InnoDB 死锁检测
InnoDB 能自动检测死锁,并自动回滚其中一个(通常是影响最小的)事务来解决死锁。可以通过 innodb_deadlock_detect 参数开关(通常保持开启)。

3. 死锁示例
场景:两个事务试图以相反顺序更新两行记录。

-- 终端 1 (事务 A)
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 成功,持有 id=1 的 X 锁
-- 稍等
UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- 尝试获取 id=2 的 X 锁,可能阻塞-- 终端 2 (事务 B)
START TRANSACTION;
UPDATE accounts SET balance = balance - 50 WHERE id = 2;  -- 成功,持有 id=2 的 X 锁
-- 稍等
UPDATE accounts SET balance = balance + 50 WHERE id = 1;  -- 尝试获取 id=1 的 X 锁,阻塞-- 此时,事务 A 等待事务 B 释放 id=2 的锁,事务 B 等待事务 A 释放 id=1 的锁,形成死锁。
-- InnoDB 会检测到死锁,并回滚其中一个事务,另一个事务得以继续。

4. 查看死锁日志
获取最近死锁的详细信息。

SHOW ENGINE INNODB STATUS;

在输出中查找 LATEST DETECTED DEADLOCK 部分。


六、锁的监控与分析

1. 查看锁信息视图

-- 查看当前活动事务
SELECT * FROM information_schema.innodb_trx;
-- 查看当前持有的锁 (可能需要高权限)
SELECT * FROM information_schema.innodb_locks;
-- 查看锁等待关系
SELECT * FROM information_schema.innodb_lock_waits;

2. 查看进程列表
识别慢查询或阻塞的连接。

SHOW FULL PROCESSLIST;

3. 终止连接
强制结束有问题的连接/事务。

KILL <thread_id>; -- 从 SHOW FULL PROCESSLIST 获取 Id

警告:KILL 操作可能导致事务回滚和数据问题,务必谨慎。


七、MyISAM 引擎的锁机制(对比了解)

1. 表级锁
MyISAM 只使用表级锁,并发性能较差。写操作会阻塞所有其他读写,读操作会阻塞写操作。

2. 锁类型

  • 表读锁 (共享): 允许多个读,阻塞写。
  • 表写锁 (排他): 阻塞所有其他读写。

3. 写优先
写锁请求通常优先于读锁请求,可能导致读请求长时间等待(饿死)。


八、锁的开发实践与优化建议

1. 使用 InnoDB 引擎: 获得事务支持和行级锁带来的高并发性。

2. 优化 SQL,精准锁定: 使用索引(尤其是主键/唯一索引)进行 WHERE 过滤,减少锁定的行数和范围。避免无索引条件的 DML 操作。

3. 控制事务范围和时长: 保持事务简短。将非数据库操作移出事务。快速 COMMITROLLBACK

4. 合理选择隔离级别: 在满足业务一致性前提下,使用最低隔离级别(如 READ COMMITTED)通常能获得更好并发性。

5. 谨慎使用显式锁: 仅在必要时使用 FOR UPDATE / LOCK IN SHARE MODE

6. 避免死锁:

  • 约定资源访问顺序。
  • 减小事务粒度。
  • 使用较低隔离级别。
  • 设置合理的 innodb_lock_wait_timeout

九、其他相关内容(延伸)

1. Metadata Lock (MDL / 元数据锁)
保护表结构定义。DDL 操作会获取 MDL,阻塞后续对该表的 DML 和 DDL。未提交事务持有的读锁会阻塞 DDL。

2. Auto-inc Lock (自增锁)
用于 AUTO_INCREMENT 列生成连续值。通常是表级锁(旧模式)或更轻量级锁(新模式,由 innodb_autoinc_lock_mode 控制),影响并发插入性能。

3. 外键锁
维护外键约束时,对父表相关行的短暂锁定(通常是 S 锁),以确保引用完整性检查的原子性。


练习题 (Practice Exercises - Locks with Answers)

  1. SELECT ... LOCK IN SHARE MODE 获取的是什么类型的锁?它允许其他事务做什么,不允许做什么?
    答案: 获取的是行级共享锁 (S 锁)。它允许其他事务读取这些行并获取 S 锁,但不允许其他事务获取这些行的 X 锁(即不允许修改或删除)。

  2. InnoDB 在 REPEATABLE READ 隔离级别下,默认使用哪种锁策略来防止幻读?它的组成是什么?
    答案: 默认使用 Next-Key Lock (临键锁)。它由 Record Lock (记录锁) 和 Gap Lock (间隙锁) 组成。

  3. 如果事务 A 更新了 id=1 的行,事务 B 也想更新 id=1 的行,会发生什么?为什么?
    答案: 事务 B 会被阻塞。因为事务 A 更新时会隐式地在 id=1 的行(索引记录)上获取排他锁 (X 锁)。X 锁与任何其他锁(包括其他事务想获取的 X 锁)都是冲突的。事务 B 必须等待事务 A 提交或回滚释放锁后才能继续。

  4. 如何查看 MySQL 中最近发生的死锁信息?
    答案: 使用命令 SHOW ENGINE INNODB STATUS;,然后在输出中查找 LATEST DETECTED DEADLOCK 部分。

  5. 为什么在 InnoDB 中通常不推荐使用 LOCK TABLES 命令?
    答案: 因为 LOCK TABLES 是显式的表级锁,它会锁定整个表,大大降低了 InnoDB 行级锁所能提供的高并发性能。应尽量利用 InnoDB 的自动行级锁定机制。

相关文章:

  • SwiftUI 2.Image介绍和使用
  • leve1.4
  • C# AutoResetEvent 详解
  • HTTP:十一.HTTP认证概述
  • 内存管理(Linux程序设计)
  • 宿主机和容器 ping 不通域名解决方法
  • 51c大模型~合集120
  • 汽车可变转向比系统的全面认识
  • Linux下载与安装
  • Python内置函数---breakpoint()
  • 基于deepseek的模型微调
  • 校园外卖服务系统的设计与实现(代码+数据库+LW)
  • 智能客服开发实战:用ONE-API构建多模态对话系统
  • 第1节:Backtrader到底是个啥?能干嘛?
  • c语言指针3
  • 免费且开源的企业级监控解决方案:Zabbix
  • JEnv-for-Windows​管理JDK版本
  • 如何提升个人解决问题的能力?
  • 【论文精读】Reformer:高效Transformer如何突破长序列处理瓶颈?
  • 本地服务器 Odoo 安装指南,并实现公网访问
  • 美联储褐皮书:关税政策背景下,美国部分地区物价上涨、经济前景恶化
  • 聚焦“共赢蓝色未来”,首届 “海洋命运共同体”上海论坛举行
  • 荣盛发展:拟以酒店、代建等轻资产板块业务搭建平台,并以其股权实施债务重组
  • 视频丨习近平同阿塞拜疆总统会谈:两国建立全面战略伙伴关系
  • 18米巨作绘写伏羲女娲,安徽展石虎最后十年重彩画
  • 特朗普称无意解雇鲍威尔,美国股债汇反弹、黄金高位下跌