MySQL 锁机制

一、锁机制的总体分类

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

1. 从加锁粒度分类

锁定的数据范围大小。

  • 表级锁 (Table-level Lock):
    锁定整张数据表。
    优点:实现简单,开销小,加锁快,不会出现死锁。
    缺点:锁定粒度最大,并发冲突概率最高,并发性能最低。
    适用引擎:MyISAM (主要使用), InnoDB (特定情况下也会使用, 如 LOCK TABLES 或某些 DDL)。
    示例(显式加表锁):
sql 复制代码
-- 加写锁,其他会话无法读写
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 (记录锁):
    锁定单个索引记录。这是最基本的行锁。
    示例:
sql 复制代码
-- 假设 id 是主键或唯一索引
-- 事务 T1 执行:
SELECT * FROM users WHERE id = 5 FOR UPDATE;
-- InnoDB 会在 id=5 的索引记录上加一个 X 记录锁。
-- 其他事务无法修改或删除 id=5 的行,也无法获取 id=5 的 S 锁。
  • Gap Lock (间隙锁):
    锁定索引记录之间的"间隙",防止其他事务在这个间隙中插入新记录。它不锁定记录本身。主要用于 REPEATABLE READ 隔离级别防止幻读。
    示例 (概念):
sql 复制代码
-- 假设 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 隔离级别下的默认锁策略,用于防止幻读。
    示例 (概念):
sql 复制代码
-- 假设 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. 锁兼容性矩阵(简化版,行锁与意向锁)

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

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


五、死锁与事务管理

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

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

2. InnoDB 死锁检测

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

3. 死锁示例

场景:两个事务试图以相反顺序更新两行记录。

sql 复制代码
-- 终端 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. 查看死锁日志

获取最近死锁的详细信息。

sql 复制代码
SHOW ENGINE INNODB STATUS;

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


六、锁的监控与分析

1. 查看锁信息视图

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

2. 查看进程列表

识别慢查询或阻塞的连接。

sql 复制代码
SHOW FULL PROCESSLIST;

3. 终止连接

强制结束有问题的连接/事务。

sql 复制代码
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 的自动行级锁定机制。

相关推荐
学地理的小胖砸1 小时前
【Python 操作 MySQL 数据库】
数据库·python·mysql
不知几秋1 小时前
sqlilab-Less-18
sql
dddaidai1231 小时前
Redis解析
数据库·redis·缓存
数据库幼崽1 小时前
MySQL 8.0 OCP 1Z0-908 121-130题
数据库·mysql·ocp
Amctwd2 小时前
【SQL】如何在 SQL 中统计结构化字符串的特征频率
数据库·sql
betazhou2 小时前
基于Linux环境实现Oracle goldengate远程抽取MySQL同步数据到MySQL
linux·数据库·mysql·oracle·ogg
lyrhhhhhhhh3 小时前
Spring 框架 JDBC 模板技术详解
java·数据库·spring
喝醉的小喵4 小时前
【mysql】并发 Insert 的死锁问题 第二弹
数据库·后端·mysql·死锁
付出不多5 小时前
Linux——mysql主从复制与读写分离
数据库·mysql
初次见面我叫泰隆5 小时前
MySQL——1、数据库基础
数据库·adb