--- tags: [mysql, transaction-isolation, gap-lock, next-key-lock, deadlock-detection] create time: 2026-08-08 18:00 update time: 2026-08-08 18:00 --- # 事务隔离级别与锁机制 ## 概述 MySQL InnoDB 实现了四种标准事务隔离级别(READ UNCOMMITTED / READ COMMITTED / REPEATABLE READ / SERIALIZABLE),每种级别对应不同的锁策略和并发冲突处理能力。理解行锁、间隙锁(Gap Lock)、临键锁(Next-Key Lock)的工作原理及其交互关系,是分析和解决死锁、幻读、并发更新冲突等问题的基础。 ## 核心原理 ### 行锁、间隙锁与临键锁的关系 InnoDB 的锁不是单一维度的,而是由多种锁类型组合而成: | 锁类型 | 锁定范围 | 适用场景 | |--------|---------|---------| | Record Lock | 索引记录本身 | 单行精确锁定 | | Gap Lock | 索引记录之间的"间隙",不含记录本身 | 防止其他事务在该间隙插入新记录 | | Next-Key Lock | Record Lock + Gap Lock | 锁定区间(前开后闭),默认锁粒度 | ```mermaid graph LR subgraph "索引序列: 1, 5, 10, 15, 20" G1["(-∞, 1)"] -->|"Gap Lock"| R1["1: Record Lock"] R1 -->|"Gap Lock"| G2["(1, 5)"] G2 -->|"Gap Lock"| R2["5: Record Lock"] R2 -->|"Gap Lock"| G3["(5, 10)"] G3 -->|"Gap Lock"| R3["10: Record Lock"] R3 -->|"Gap Lock"| G4["(10, 15)"] G4 -->|"Gap Lock"| R4["15: Record Lock"] R4 -->|"Gap Lock"| G5["(15, +∞)"] end style G1 fill:#ffe0e0 style G2 fill:#ffe0e0 style G3 fill:#ffe0e0 style G4 fill:#ffe0e0 style G5 fill:#ffe0e0 style R1 fill:#e0ffe0 style R2 fill:#e0ffe0 style R3 fill:#e0ffe0 style R4 fill:#e0ffe0 ``` **关键结论**: - 默认情况下(RR 隔离级别),InnoDB 使用 Next-Key Lock,即 Record + Gap 的组合 - 如果使用唯一的索引(如主键)等值查询,Gap Lock 会被取消,只剩 Record Lock - GC 隔离级别下,Gap Lock 被关闭(只加 Record Lock),这是它与 RR 的本质区别之一 ### 锁的兼容矩阵 | 已有锁 ↓ \ 请求锁 → | Record R (共享锁) | Record X (排他锁) | Gap R | Gap X | |---------------------|------------------|------------------|-------|-------| | Record R | ✅ 兼容 | ❌ 冲突 | ✅ 兼容 | ❌ 冲突 | | Record X | ❌ 冲突 | ❌ 冲突 | ❌ 冲突 | ❌ 冲突 | | Gap R | ✅ 兼容 | ❌ 冲突 | ✅ 兼容 | ❌ 冲突 | | Gap X | ❌ 冲突 | ❌ 冲突 | ✅ 兼容* | ❌ 冲突 | > [!NOTE] 共享锁与排他锁 > 共享锁(S Lock)允许多个事务同时持有,但与其他 S/X 锁都不兼容。排他锁(X Lock)独占资源,不与其他任何锁兼容。INSERT/UPDATE/DELETE 隐式加 X 锁,SELECT ... FOR UPDATE 显式加 X 锁,SELECT ... LOCK IN SHARE MODE 加 S 锁。 ### 死锁检测机制 InnoDB 使用**等待图(Wait-For Graph)**进行死锁检测: ```mermaid flowchart TD subgraph "等待图示例" T1["事务 T1"] -->|"等待"| L1["锁 L1"] L1 -->|"持有者"| T2["事务 T2"] T2 -->|"等待"| L2["锁 L2"] L2 -->|"持有者"| T3["事务 T3"] T3 -->|"等待"| L3["锁 L3"] L3 -->|"持有者"| T1 end CycleCheck{"检测到环
T1→T2→T3→T1"} CycleCheck -->|是| ChooseVictim["选择牺牲者
回滚代价最小的事务"] CycleCheck -->|否| ContinueWait["继续等待"] ChooseVictim --> ReleaseDeadlock["释放死锁
回滚牺牲者的事务"] ``` **死锁参数配置**: | 参数 | 默认值 | 含义 | |------|--------|------| | `innodb_lock_wait_timeout` | 50 秒 | 等待锁的超时时间(不涉及死锁检测) | | `innodb_deadlock_detect` | ON | 是否开启死锁检测 | | `innodb_max_dirty_pages_pct` | 75% | 脏页比例阈值,超过时强制刷新 | ``` 死锁处理流程: 1. 事务 A 申请锁 B → 被事务 B 持有 → 加入等待队列 2. 事务 B 申请锁 A → 被事务 A 持有 → 检测到环路 3. InnoDB 选择"回滚代价最小"的事务作为牺牲者 4. 回滚牺牲者的当前语句(非整个事务),释放其持有的锁 5. 另一事务继续执行 ``` > [!WARNING] 牺牲者选择策略 > 回滚的是"当前语句"而非"整个事务"。如果语句包含多条 SQL,只回滚最后一条。这意味着事务可以继续重试后续语句,不会中断整个业务流程。 ### Insert Intention Gap Lock Insert Intention Gap Lock 是一种特殊的间隙锁,专门用于 INSERT 操作。它表示"我准备在这个间隙插入一条记录"。 ```mermaid graph LR subgraph "索引值: 5 和 10" R5["记录 5"] -->|"Gap (5,10)"| G1["间隙区"] G1 -->|"Gap (5,10)"| R10["记录 10"] end T1["事务 T1: INSERT 值=7"] -->|"申请 Insert Intention
Gap Lock on (5,10)"| G1 T2["事务 T2: INSERT 值=8"] -->|"申请 Insert Intention
Gap Lock on (5,10)"| G1 G1 -->|"Insert Intention 锁互相兼容"| T2 ``` 两条 INSERT 语句即使目标间隙重叠,它们的 Insert Intention Gap Lock 也是**互相兼容**的。但如果其中一条已经是该间隙上的 Gap Lock(来自 UPDATE/DELETE),则新的 INSERT 会等待。 ## 代码示例 ### 场景分析:更新某区间内不存在记录时会加什么锁 ```sql -- 假设表中有记录: id IN (1, 5, 10, 15, 20) -- 事务 A 执行: UPDATE orders SET status = 1 WHERE id > 5 AND id < 15; -- InnoDB 的行为分析: -- 1. 扫描到 id=5 的记录 → Next-Key Lock: (5, 10](Record+Gap) -- 2. 发现 id=10 的记录 → Record Lock: 10 本身(但 id=10 满足条件,被修改) -- 3. 继续扫描,没有 id=11~14 的记录 → Next-Key Lock: (10, 15] -- 4. 扫描到 id=15 → Record Lock: 15 本身(但不满足条件,不加锁) -- 5. 最终锁定的范围:(5, 15],包括间隙和存在的记录 -- 此时另一个事务执行: -- 事务 B: INSERT INTO orders (id, status) VALUES (12, 0); -- 结果:被阻塞!因为 (10, 15) 间隙已被事务 A 锁定 ``` ```sql -- 验证锁类型的 SQL 写法 -- 会话 1:开始事务并加锁 START TRANSACTION; SELECT * FROM orders WHERE id = 10 FOR UPDATE; -- 由于 id 是主键(唯一索引),只加 Record Lock 在 10 上 -- 会话 2: SELECT * FROM orders WHERE id = 10 FOR UPDATE; -- 阻塞! SELECT * FROM orders WHERE id = 5 FOR UPDATE; -- 不会被阻塞(5 ≠ 10,主键等值查询的 Gap Lock 已被去掉) -- 如果是非唯一索引: -- 索引 (status) 上有重复值,UPDATE ... WHERE status=1 -- 会对所有 status=1 的记录加 Record Lock + Gap Lock ``` ## 实践场景 **场景一:排查生产环境死锁** ```sql -- InnoDB 会打印死锁详情到错误日志 -- 也可以通过 performance_schema 查看 SELECT * FROM performance_schema.data_locks; SELECT * FROM performance_schema.data_lock_waits; -- 分析死锁日志的步骤: -- 1. 找到死锁发生的时间点和涉及的 SQL -- 2. 画出等待图,确定循环等待链 -- 3. 按照推荐方案调整 SQL 执行顺序或加锁范围 ``` **场景二:减少间隙锁的影响** - 如果需要在高并发环境下批量插入数据,可以将隔离级别降到 RC - RC 级别下 InnoDB 不使用 Gap Lock,INSERT 操作不会阻塞 - 但要注意:RC 不能解决幻读问题,需评估业务可接受度 **场景三:合理的索引设计降低锁竞争** - 尽量使用唯一索引(主键)做精确更新,可以避免 Gap Lock - 避免在大宽表的非唯一索引上做大范围 UPDATE/DELETE - 大批量更新时分批次进行,缩短持有锁的时间 ## 扩展阅读 - [[B+树索引原理]] - [[ACID 与 MVCC 机制]]