MySQL-锁机制

MySQL 锁机制

一、全局锁(Global Lock)

1. 核心定义

  • 锁定整个MySQL实例,所有库/表的读写操作均被阻塞

  • 仅支持读操作(加锁后),所有DDL、DML写操作均被阻塞

2. 加锁/解锁方式

sql 复制代码
-- 加全局读锁(阻塞所有写操作)
FLUSH TABLES WITH READ LOCK (FTWRL);
-- 解锁
UNLOCK TABLES;

3. 适用场景

  • 全库逻辑备份:保证备份数据的一致性

  • 替代方案 :MySQL 5.6+ 可使用 mysqldump --single-transaction(仅限InnoDB)

  • MySQL 8.0+ 推荐使用 mysqldump --source-data 或物理备份工具

4. 核心风险

  • 加锁期间业务完全阻塞(无法执行任何写操作)

  • 主从架构中,主库只读会导致从库同步延迟

  • FTWRL会关闭所有打开的表,可能导致性能抖动

二、表级锁(Table-Level Lock)

1. 基础表锁(READ/WRITE)

锁类型 核心定义 兼容关系(同一张表)
表读锁(READ) 允许所有会话读禁止所有会话写 多个READ锁兼容;与WRITE锁互斥
表写锁(WRITE) 允许持有锁的会话读写禁止其他所有会话读写 与任何表锁(READ/WRITE)互斥
sql 复制代码
-- 加表读锁
LOCK TABLES `user` READ;
-- 加表写锁
LOCK TABLES `user` WRITE;
-- 解锁
UNLOCK TABLES;

2. 元数据锁(MDL)

核心定义
  • 自动加锁 的表级锁,保护表结构不被并发修改

  • MySQL 5.5+引入,避免DDL与DML冲突

加锁规则
操作类型 加锁类型 核心特性
SELECT/INSERT/UPDATE/DELETE MDL读锁(共享) 多个读锁兼容
ALTER TABLE/DROP TABLE MDL写锁(排他) 与所有MDL锁互斥
MDL生命周期
  • 读锁:事务开始时自动获取,事务提交/回滚时释放

  • 写锁:DDL执行期间持有,执行完成立即释放

核心风险:MDL锁等待
sql 复制代码
-- 场景:长事务阻塞DDL
-- 会话1(长事务):
BEGIN;
SELECT * FROM users WHERE id = 1;  -- 获取MDL读锁
-- 不提交,保持事务打开

-- 会话2(DDL操作):
ALTER TABLE users ADD COLUMN age INT;  -- 等待MDL写锁,被阻塞!

-- 会话3(新查询):
SELECT * FROM users LIMIT 1;  -- 也被阻塞,等待MDL读锁

优化建议

  1. 监控长事务:SELECT * FROM information_schema.INNODB_TRX;

  2. DDL操作选择业务低峰期

  3. 使用Online DDL(MySQL 5.6+)

3. 意向锁(IS/IX)

核心定义
  • InnoDB专属表级标记锁

  • 行锁的"意向声明" ,避免表锁与行锁冲突

  • 减少锁冲突检查的开销(无需遍历每行)

分类与加锁规则
意向锁类型 触发条件 核心作用
IS(意向共享) 事务计划对某些行加S锁前 标记"表内有/将有共享行锁"
IX(意向排他) 事务计划对某些行加X锁前 标记"表内有/将有排他行锁"
兼容关系矩阵
请求锁↓ \ 已有锁 → 无锁 IS IX S(表读锁) X(表写锁)
IS
IX
S
X

三、行级锁(Row-Level Lock)⭐⭐⭐

注意:行锁实际上是加在索引上的锁,而在InnoDB中默认加行锁就是加临键锁

补充注意:仅当 WHERE 条件命中索引列(如主键)时才是行锁,无索引会升级为表锁 ;仅 InnoDB存储引擎 有行锁。

1. 基本行锁(S锁/X锁)

锁类型 核心定义 兼容关系(同一行) 加锁场景 解锁时机
共享锁(S锁) 允许读,禁止写 S-S兼容,S-X互斥 SELECT ... FOR SHARE 事务提交/回滚
排他锁(X锁) 禁止读写 X-X互斥,X-S互斥 SELECT ... FOR UPDATE UPDATE/DELETE 事务提交/回滚
兼容性矩阵(同一行)
请求锁 ↓ \ 当前锁 → 无锁 S锁 X锁
请求S锁
请求X锁

MySQL 中行锁的使用

1. SELECT ... FOR SHARE(共享锁 / S 锁)

核心说明 :加行级共享锁(S 锁),多个事务可同时对该行加共享锁,但无法加排他锁;适用于只读但需防止数据被修改的场景,MySQL 8.0 前可写为 SELECT ... LOCK IN SHARE MODE(等价)。兼容 / 阻塞场景

  • ✅ 允许:其他事务执行 SELECT ... FOR SHARE 锁定该行
  • ✅ 允许:普通 SELECT(不加锁查询)
  • ❌ 阻塞:其他事务执行 SELECT ... FOR UPDATE 锁定该行
  • ❌ 阻塞:其他事务执行 UPDATE 操作修改该行
  • ❌ 阻塞:其他事务执行 DELETE 操作删除该行
2. SELECT ... FOR UPDATE(排他锁 / X 锁)

核心说明 :加行级排他锁(X 锁),锁定后其他事务无法对该行加任何锁(共享锁 / 排他锁),仅能执行不加锁的普通查询;锁的释放时机为事务提交或回滚。兼容 / 阻塞场景

  • ✅ 允许:普通 SELECT(不加锁查询)
  • ❌ 阻塞:其他事务执行 SELECT ... FOR SHARE 锁定该行
  • ❌ 阻塞:其他事务执行 SELECT ... FOR UPDATE 锁定该行
  • ❌ 阻塞:其他事务执行 UPDATE 操作修改该行
  • ❌ 阻塞:其他事务执行 DELETE 操作删除该行
3. UPDATE/DELETE 语句(自动加排他锁 / X 锁)

核心说明:执行 UPDATE/DELETE 时,InnoDB 会自动为 WHERE 条件命中的行加排他锁(X 锁),锁释放时机为事务提交或回滚;依赖索引实现行锁,无索引则升级为表锁。兼容 / 阻塞场景:同上X锁

4. INSERT 语句(隐式排他锁 + 间隙锁 / 临键锁

核心说明 :INSERT 会为插入的行自动加排他锁(X 锁);兼容 / 阻塞场景:同上X锁;与其他不同的是:若插入列有唯一约束 / 主键,其他事务插入相同值会被阻塞;若无唯一约束,会触发间隙锁 / 临键锁(防止幻读)

补充关键注意事项

  1. 锁生效前提:仅 InnoDB 引擎下生效,事务需显式开启(关闭自动提交或用 BEGIN/START TRANSACTION);

  2. 索引影响:WHERE 条件未命中索引时,行锁升级为表锁;

  3. 锁释放时机:行锁仅在事务 COMMIT/ROLLBACK 时释放,长时间未提交易导致锁等待 / 死锁;

  4. 死锁处理InnoDB 自动检测死锁并回滚其中一个事务 ,可通过 SHOW ENGINE INNODB STATUS 查看死锁日志。

2. 记录锁、间隙锁、临键锁

在InnoDB中默认加行锁就是加临键锁

(1) 记录锁(Record Lock)
  • 锁定单行记录 (基于主键/唯一索引)

    • 记录锁是上述行锁的一种具体实现
  • 作用防止并发修改同一行

  • 示例

sql 复制代码
-- id=1的记录被锁定
UPDATE users SET balance = balance - 100 WHERE id = 1;
(2) 间隙锁(Gap Lock)
  • 锁定索引记录之间的间隙不包含记录本身

    • 即左开右开
  • 核心作用防止幻读(在RR隔离级别下)

  • 触发条件:RR隔离级别 + 范围查询/非唯一索引等值查询

sql 复制代码
-- 示例数据:id=10, 20, 30
-- 锁定间隙:(10, 20), (20, 30), (30, +∞)
-- 事务1:
SELECT * FROM users WHERE id > 15 FOR UPDATE;
-- 阻止其他事务插入 id=16, 25, 35 等记录

-- 事务2(被阻塞):
INSERT INTO users (id, name) VALUES (25, '张三');

间隙锁特性

  1. 仅RR级别生效:RC级别无间隙锁

  2. 索引相关:基于索引的键值间隙

  3. 多区间锁定:可能锁定多个间隙

  4. 兼容性规则 :间隙锁之间兼容(两个事务可以锁定相同间隙)

间隙锁的边界问题
sql 复制代码
-- 示例表:id (10, 20, 30)
-- 最小边界:(-∞, 第一行]
-- 最大边界:[最后一行, +∞)
-- 中间间隙:(前一行, 后一行)

-- 事务1:查询不存在的记录
SELECT * FROM users WHERE id = 15 FOR UPDATE;
-- 锁定间隙:(10, 20)  -- 注意:不包含10和20

-- 事务2:插入边界值测试
INSERT INTO users (id) VALUES (9);   -- ✅ 允许(不在锁定间隙)
INSERT INTO users (id) VALUES (10);  -- ✅ 允许(记录已存在,记录锁不冲突)
INSERT INTO users (id) VALUES (11);  -- ❌ 阻塞(在锁定间隙内)
INSERT INTO users (id) VALUES (20);  -- ✅ 允许(记录已存在)
(3) 临键锁(Next-Key Lock)
  • 记录锁 + 间隙锁(InnoDB默认行锁策略)

  • 公式:临键锁 = 记录锁 + 前一个间隙锁

    • 锁定范围:当前记录 + 前一个间隙

    • 即左开右闭

sql 复制代码
-- 示例数据:id=10, 20, 30

-- 事务1:
SELECT * FROM users WHERE id = 20 FOR UPDATE;
-- 临键锁范围:(10, 20]
-- 包含:记录20(记录锁) + 间隙(10, 20)(间隙锁)

-- 以下操作此时会被阻塞
INSERT INTO users (id) VALUES (15);  -- 插入间隙
UPDATE users SET name='test' WHERE id = 20;  -- 修改记录
临键锁的特殊情况

在InnoDB中默认加行锁就是加临键锁,以下时特殊情况

场景1:唯一索引等值查询
sql 复制代码
-- id是主键(唯一索引)
SELECT * FROM users WHERE id = 20 FOR UPDATE;
-- 仅加记录锁,不加间隙锁(因为唯一性保证)
场景2:非唯一索引等值查询
sql 复制代码
-- name有非唯一索引,数据:('A', 'A', 'B', 'B', 'C')
SELECT * FROM users WHERE name = 'B' FOR UPDATE;
-- 锁定:
-- 1. 所有name='B'的记录锁
-- 2. 间隙锁:(第一个'B', 最后一个'B')
-- 3. 可能的前后间隙
场景3:范围查询的锁升级
sql 复制代码
-- 事务1:
SELECT * FROM users WHERE id >= 15 AND id <= 25 FOR UPDATE;
-- 可能锁定:
-- 1. 所有15-25之间的记录锁
-- 2. 间隙:(上一个记录, 15], (15, 25], (25, 下一个记录]
-- 实际范围可能大于WHERE条件!

3. 行锁与隔离级别的关系

隔离级别 记录锁 间隙锁 临键锁 幻读防护
READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ ✅(默认)
SERIALIZABLE

四、乐观锁与悲观锁

这是锁的设计思想,并非 MySQL 内置的具体锁类型,而是业务 / 代码层面的实现方式。

1. 悲观锁(Pessimistic Lock)

  • 核心思想 :假设并发冲突一定会发生,先加锁再操作

  • 实现方式 :利用 MySQL 内置锁(如行锁SELECT ... FOR UPDATE、表锁)

  • 适用场景:并发冲突概率高、写操作多的场景

  • 示例

    sql 复制代码
    -- 用行锁实现悲观锁
    BEGIN;
    SELECT * FROM users WHERE id=1 FOR UPDATE; -- 加排他锁
    UPDATE users SET balance=balance-100 WHERE id=1;
    COMMIT;

2. 乐观锁(Optimistic Lock)

  • 核心思想 :假设并发冲突不会发生,先操作后校验

  • 实现方式 :通过版本号(version)或时间戳(update_time)实现

  • 适用场景:并发冲突概率低、读操作多的场景

  • 示例

    sql 复制代码
    -- 1. 查询数据时获取版本号
    SELECT balance, version FROM users WHERE id=1;
    -- 2. 更新时校验版本号
    UPDATE users 
    SET balance=balance-100, version=version+1 
    WHERE id=1 AND version=原版本号;
    -- 3. 判断影响行数:若为0则说明版本冲突,需重试

五、锁机制实战要点

1. 索引与锁的关系

sql 复制代码
-- 情况1:使用主键索引(记录锁)
UPDATE users SET age=25 WHERE id=100;  -- 仅锁定id=100的行

-- 情况2:使用普通索引
-- 假设name有普通索引
UPDATE users SET age=25 WHERE name='张三';
-- 锁定:1.name='张三'的记录锁 2.相关间隙锁

-- 情况3:无索引(表锁!)
UPDATE users SET age=25 WHERE age=30;  -- 如果age无索引→表锁

2. 死锁分析与规避

死锁场景示例:
sql 复制代码
-- 事务1
BEGIN;
UPDATE users SET balance=balance-100 WHERE id=1;  -- 锁定id=1
UPDATE users SET balance=balance+100 WHERE id=2;  -- 尝试锁定id=2

-- 事务2(并发执行)
BEGIN;
UPDATE users SET balance=balance-200 WHERE id=2;  -- 锁定id=2
UPDATE users SET balance=balance+200 WHERE id=1;  -- 尝试锁定id=1(死锁!)
死锁规避策略:
  1. 统一加锁顺序:约定所有事务按相同顺序加锁
  2. 减少事务粒度:拆分大事务,及时提交
  3. 使用索引:避免无索引查询导致表锁
  4. 设置超时SET innodb_lock_wait_timeout = 30;
  5. 死锁检测SHOW ENGINE INNODB STATUS\G 查看死锁信息

3. 锁优化建议

  1. 索引设计:为WHERE条件、JOIN条件创建合适索引

  2. 事务设计

    • 保持事务短小
    • 避免长事务中的锁持有
    • 批量操作使用LIMIT分批次
  3. SQL优化

    • 避免SELECT *,只取需要字段
    • 使用覆盖索引减少回表
    • 避免大范围UPDATE/DELETE
  4. 隔离级别选择

    • 默认使用RR级别(防幻读)
    • 高并发场景可考虑RC级别(减少间隙锁)

六、锁监控与排查命令

1. 锁状态查看

sql 复制代码
-- 查看表锁使用情况
SHOW OPEN TABLES WHERE In_use > 0;

-- 查看当前锁等待
SELECT * FROM information_schema.INNODB_LOCKS;
SELECT * FROM information_schema.INNODB_LOCK_WAITS;

-- 查看事务详情
SELECT * FROM information_schema.INNODB_TRX;

-- 查看MDL锁(MySQL 5.7+)
SELECT * FROM performance_schema.metadata_locks;

2. InnoDB引擎状态

sql 复制代码
-- 查看详细锁信息(包含最近死锁)
SHOW ENGINE INNODB STATUS\G

-- 重点关注:
-- 1. LATEST DETECTED DEADLOCK(最近死锁)
-- 2. TRANSACTIONS(当前事务)
-- 3. ROW OPERATIONS(行操作)

3. 性能监控

sql 复制代码
-- 锁等待超时设置
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
SET GLOBAL innodb_lock_wait_timeout = 30;

-- 死锁检测
SHOW VARIABLES LIKE 'innodb_deadlock_detect';
-- ON:自动检测死锁(默认)
-- OFF:关闭检测,依赖超时机制

-- 查看锁统计
SHOW STATUS LIKE 'Innodb_row_lock%';
-- Innodb_row_lock_current_waits:当前等待行锁数量
-- Innodb_row_lock_time:行锁总等待时间
-- Innodb_row_lock_time_avg:平均等待时间
-- Innodb_row_lock_time_max:最长等待时间
-- Innodb_row_lock_waits:行锁等待总次数

七、补充

实战建议

  1. 明确索引设计:了解每个查询使用的索引类型

  2. 监控锁等待:定期检查锁等待情况

  3. 测试锁行为:在测试环境验证锁范围

  4. 使用EXPLAIN:分析查询执行计划,预测锁行为

  5. 考虑业务场景:权衡一致性与并发性的需求


总结要点

  • 全局锁用于全库备份,慎用

  • 表级锁中,MDL锁是常见阻塞原因

  • 行级锁是InnoDB核心,理解记录锁、间隙锁、临键锁的区别

  • RR级别默认使用临键锁防幻读,RC级别无间隙锁

  • 死锁可以通过统一加锁顺序、短事务、合适索引来规避

  • 监控工具是排查锁问题的关键

相关推荐
华洛3 小时前
《回顾我的AI学习之路,上万字的AI学习思维导图分享》
前端·后端·产品经理
WZTTMoon3 小时前
Spring Boot Swagger3 使用指南
java·spring boot·后端·swagger3
骑着bug的coder3 小时前
第5讲:事务——数据一致性的保护伞
后端·mysql
Java编程爱好者3 小时前
咱们聊聊Spring循环依赖那点事儿:从“死锁”到“三级缓存”的奇妙之旅
后端
小橙编码日志3 小时前
Java事务常见的失效场景总结
后端·面试
马卡巴卡3 小时前
Java关键字解析之abstract:抽象的本质、规范定义与多态基石
后端
feathered-feathered3 小时前
Redis【事务】(面试相关)与MySQL相比较,重点在Redis事务
android·java·redis·后端·mysql·中间件·面试
神奇小汤圆4 小时前
Java关键字解析之volatile:可见性的守护者、有序性的调节器
后端
ShaneD7714 小时前
Redis实战: 利用逻辑过期解决缓存击穿
后端