【MySQL】间隙锁 与 排他锁 的区别

【MySQL】间隙锁 与 排他锁 的区别

文章目录

  • [【MySQL】间隙锁 与 排他锁 的区别](#【MySQL】间隙锁 与 排他锁 的区别)
    • [📊 核心区别对比表](#📊 核心区别对比表)
    • [🔍 详细解释](#🔍 详细解释)
      • [1. 排他锁(X锁)- 行级锁](#1. 排他锁(X锁)- 行级锁)
      • [2. 间隙锁(Gap Lock)- 范围锁](#2. 间隙锁(Gap Lock)- 范围锁)
    • [🎯 实际工作场景](#🎯 实际工作场景)
      • [场景1:防止幻读(Phantom Read)](#场景1:防止幻读(Phantom Read))
      • [场景2:组合使用 - 临键锁(Next-Key Lock)](#场景2:组合使用 - 临键锁(Next-Key Lock))
    • [⚠️ 重要注意事项](#⚠️ 重要注意事项)
      • [1. 间隙锁只作用于非唯一索引](#1. 间隙锁只作用于非唯一索引)
      • [2. 查看锁信息](#2. 查看锁信息)
      • [3. 死锁风险](#3. 死锁风险)
    • [🔧 性能影响与优化](#🔧 性能影响与优化)
    • [💡 最佳实践建议](#💡 最佳实践建议)
    • [📝 总结要点](#📝 总结要点)

这是一个非常核心的数据库锁机制问题。简单来说:排他锁(X锁)是"锁什么",间隙锁(Gap Lock)是"锁哪里"

📊 核心区别对比表

特性 排他锁 (X锁) 间隙锁 (Gap Lock)
锁的类型 锁类型 - 定义锁的"权限" 锁范围 - 定义锁的"位置"
作用对象 已存在的数据行 数据行之间的间隙
兼容性 与任何其他锁都不兼容 与其他间隙锁兼容,但与插入意向锁冲突
主要目的 防止其他事务读写数据 防止其他事务在范围内插入新数据
可见范围 行记录本身 索引记录之间的"空隙"
何时使用 UPDATE、DELETE、SELECT...FOR UPDATE 可重复读隔离级别下的范围查询

🔍 详细解释

1. 排他锁(X锁)- 行级锁

sql 复制代码
-- 示例:对uid=8的行加排他锁
SELECT * FROM student WHERE uid = 8 FOR UPDATE;
-- 或
UPDATE student SET age = 60 WHERE uid = 8;

特点:

  • 锁定具体的数据行
  • 其他事务不能读取(SELECT...FOR UPDATE)也不能修改(UPDATE/DELETE)该行
  • 可以看作"写锁"

2. 间隙锁(Gap Lock)- 范围锁

sql 复制代码
-- 示例:锁定uid在(5, 10)之间的所有间隙
SELECT * FROM student WHERE uid BETWEEN 6 AND 9 FOR UPDATE;
-- 或
SELECT * FROM student WHERE uid > 5 AND uid < 10 FOR UPDATE;

假设你的表数据:

sql 复制代码
uid: 1, 2, 3, 4, 5, 7, 8, 10, 11

间隙包括:(-∞,1), (1,2), (2,3), ..., (5,7), (7,8), (8,10), (10,11), (11,+∞)

特点:

  • 锁定索引记录之间的空隙,而不是记录本身
  • 防止其他事务在范围内插入新数据
  • 只在可重复读(REPEATABLE-READ) 隔离级别下有效

🎯 实际工作场景

场景1:防止幻读(Phantom Read)

sql 复制代码
-- 事务一
BEGIN;
SELECT * FROM student WHERE age BETWEEN 20 AND 30 FOR UPDATE;
-- 锁定所有age在20-30之间的间隙

-- 事务二尝试插入age=25的新记录(会被阻塞)
INSERT INTO student (name, age, sex) VALUES ('new_student', 25, 'M');

场景2:组合使用 - 临键锁(Next-Key Lock)

sql 复制代码
MySQL 实际上经常使用 **临键锁 = 记录锁 + 间隙锁**
-- 锁定uid=8的行及其前后的间隙
SELECT * FROM student WHERE uid = 8 FOR UPDATE;
-- 实际锁定:(7, 8] 和 (8, 10)

⚠️ 重要注意事项

1. 间隙锁只作用于非唯一索引

sql 复制代码
-- 如果name有唯一索引,不会加间隙锁
SELECT * FROM student WHERE name = 'bbbb' FOR UPDATE;
-- 只对'bbbb'这一行加排他锁

-- 如果name没有索引或不是唯一索引,可能会锁全表间隙
SELECT * FROM student WHERE name LIKE 'b%' FOR UPDATE;

2. 查看锁信息

sql 复制代码
-- 查看当前的锁(MySQL 8.0+)
SELECT 
    engine_transaction_id as trx_id,
    lock_type,
    lock_mode,
    lock_data,
    index_name
FROM performance_schema.data_locks 
WHERE object_name = 'student';

3. 死锁风险

间隙锁容易导致死锁,特别是并发插入时:

sql 复制代码
-- 事务A
BEGIN;
SELECT * FROM student WHERE uid = 15 FOR UPDATE; -- 间隙锁(10, +∞)

-- 事务B  
BEGIN;
SELECT * FROM student WHERE uid = 16 FOR UPDATE; -- 间隙锁(10, +∞)

-- 事务A
INSERT INTO student (uid, name) VALUES (15, 'test'); -- 等待B

-- 事务B
INSERT INTO student (uid, name) VALUES (16, 'test'); -- 等待A → 死锁!

🔧 性能影响与优化

情况 影响 解决方案
全表间隙锁 锁住所有间隙,并发极差 使用唯一索引查询
范围查询 锁住大范围间隙 缩小查询范围
批量操作 可能锁大量间隙 分批次操作
高并发插入 容易死锁 降低隔离级别为RC

💡 最佳实践建议

  1. 明确使用场景:

    • 需要防止幻读 → 使用间隙锁(RR隔离级别)
    • 只需要防止数据修改 → 使用行级排他锁
  2. 索引设计:

    sql 复制代码
    -- 好的设计:使用唯一索引避免不必要的间隙锁
    CREATE UNIQUE INDEX idx_uid ON student(uid);
    
    -- 查询时使用唯一索引
    SELECT * FROM student WHERE uid = 8 FOR UPDATE; -- 只锁一行
  3. 隔离级别选择:

    sql 复制代码
    -- 如果不需要防止幻读,使用读已提交
    SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
    -- 此时不会加间隙锁
  4. 避免长事务:

    sql 复制代码
    -- 尽快提交事务,减少锁持有时间
    BEGIN;
    -- 立即执行需要加锁的操作
    SELECT ... FOR UPDATE;
    UPDATE ...;
    -- 尽快提交
    COMMIT;

📝 总结要点

  1. 排他锁 是"行锁" - 锁已存在的数据
  2. 间隙锁 是"范围锁" - 锁不存在的间隙
  3. MySQL 默认使用 临键锁(行锁+间隙锁)
  4. 间隙锁主要在 RR隔离级别 防止幻读
  5. 设计好索引可以大大减少不必要的间隙锁

在你的学生表例子中,如果 uid 是唯一索引,SELECT ... WHERE uid=8 FOR UPDATE 只会加行锁。如果 name 不是唯一索引,相同的查询可能会加间隙锁。

相关推荐
浒畔居2 小时前
机器学习模型部署:将模型转化为Web API
jvm·数据库·python
一个响当当的名号2 小时前
lectrue9 索引并发控制
java·开发语言·数据库
liu****3 小时前
4.Qt窗口开发全解析:菜单栏、工具栏、状态栏及对话框实战
数据库·c++·qt·系统架构
三水不滴3 小时前
Redis缓存更新策略
数据库·经验分享·redis·笔记·后端·缓存
企鹅侠客3 小时前
第35章—内核解析篇:Redis内存淘汰机制
数据库·redis
西柚小萌新3 小时前
【人工智能:Agent】--12.多智能体(二)
数据库
快乐非自愿3 小时前
【面试题】MySQL 的索引类型有哪些?
数据库·mysql·面试
naruto_lnq3 小时前
使用Fabric自动化你的部署流程
jvm·数据库·python
曹轲恒4 小时前
【Redis持久化核心】AOF/RDB通俗详解+多场景对比
数据库·redis·bootstrap