大家好,我是十三!欢迎来到十三Tech。

前几年一次半夜故障——运维跑备份脚本,里面用了 FLUSH TABLES WITH READ LOCK(FTWRL),整库卡了将近三分钟,业务侧大量超时报警。当时很奇怪:备份脚本为什么不能在业务低峰跑?后来才懂——FTWRL 是全局锁,跟行锁完全不在一个粒度上,它会让整个数据库变成只读状态,长事务在跑就持续等。

这件事让我重新整理 MySQL 的锁体系——很多人讲"MySQL 锁"直接跳到行锁,太片面。MySQL 锁是三层粒度:全局锁、表级锁、行级锁——每层各有用途、各有坑。理解三层结构,才能在"加锁"和"性能"之间做对选择。

这一篇就聊聊这三层锁、各自的典型场景,以及生产中常见但容易混淆的几个锁类型。

MySQL 三层锁粒度

一、全局锁 — FTWRL 的真实用途

全局锁就是对整个数据库实例加锁——加完后整个库变成只读状态。

FLUSH TABLES WITH READ LOCK;   -- 加全局读锁
UNLOCK TABLES;                 -- 释放

典型用途:备份工具需要拿到"一致性快照"。mysqldump --single-transaction --master-data 用 InnoDB MVCC 拿一致性视图,不需要 FTWRL;但非 InnoDB 表(MyISAM、Memory)不支持事务,备份它们必须用 FTWRL。

FTWRL 的副作用

  1. 阻塞所有 DML 和大部分 DDL——业务不能写
  2. 长查询会阻塞 FTWRL——FTWRL 要等所有活跃事务结束才能拿到锁
  3. FTWRL 期间任何新连接进来查询也会被阻塞(部分情况下)

为什么不用 set global readonly=true:readonly 是系统变量,跟 FTWRL 的语义不同——readonly 在主从架构下会让从库认为自己是主库,FTWRL 不会;readonly 异常退出时会保留状态,FTWRL 自动释放。生产备份一律用 FTWRL,不用 readonly

避免 FTWRL 的方法:所有表都用 InnoDB → 用 mysqldump --single-transaction 即可,完全不需要 FTWRL——这是为什么 8.0 之后 MyISAM 越来越边缘化的原因之一。

三层锁的具体类型

二、表级锁 — 三种类型

表级锁粒度比全局锁细一些,但仍以"表"为单位。InnoDB 内部有三种表级锁:

表锁(Table Lock):手动 LOCK TABLES ... WRITE/READ 加的锁——生产几乎不用。会阻塞所有其他会话对该表的任何操作。用途很窄——某些古老的迁移脚本可能用。

元数据锁(MDL,Metadata Lock)这是最容易踩坑的表级锁。MDL 是 5.5+ 自动加的——所有 DML(CRUD)会加 MDL 读锁,所有 DDL(ALTER/CREATE/DROP)会加 MDL 写锁。

MDL 的设计意图是防止"DML 进行中表结构被改"——但生产中常见的故障模式是:

  1. 一个长事务(或长查询)持有 MDL 读锁
  2. 此时执行 DDL(如 ALTER TABLE)请求 MDL 写锁,被读锁阻塞
  3. 后续所有新来的 DML 也请求 MDL 读锁,但被前面排队的 MDL 写锁阻塞(FIFO)
  4. 整个表的 DML 全部堵在 MDL 队列里——整表 hang 死

这就是为什么生产中 ALTER TABLE 是高危操作——必须用工具如 pt-online-schema-changegh-ost,它们不会阻塞 DML。

自增锁(AUTO-INC Lock):插入自增主键时加的锁。5.1+ 引入了 innodb_autoinc_lock_mode 控制:

  • = 0(traditional):全程持有表级自增锁,并发差
  • = 1(consecutive):批量插入时持有,简单插入用轻量锁
  • = 2(interleaved,8.0 默认):完全不用表级自增锁,自增值由 mutex 分配——并发最好但 binlog 同步可能"乱序"(ROW 模式无影响)
SHOW VARIABLES LIKE 'innodb_autoinc_lock_mode';
-- 8.0 默认 = 2,生产推荐

三、行级锁概览 — InnoDB 的强项

行级锁是 InnoDB 的核心优势——MyISAM 不支持,是它被淘汰的关键原因。

InnoDB 行级锁的三种格式

  • Record Lock:锁单条记录——精确锁定某一行
  • Gap Lock:锁记录之间的"间隙"——防止新数据插入到这个范围
  • Next-Key Lock:Record + Gap 的组合——锁一个"前开后闭"区间

行锁的"两种模式":

  • 共享锁(S Lock)SELECT ... LOCK IN SHARE MODE 或 8.0+ 的 FOR SHARE,允许其他事务读但不能写
  • 排他锁(X Lock)SELECT ... FOR UPDATEUPDATEDELETEINSERT,阻塞其他事务的读写

InnoDB 行锁的特点

  1. 锁建立在索引上——没有索引的查询会退化为表锁(实际是把所有行都锁了)
  2. 锁不会在事务中立即释放——必须等 COMMIT 或 ROLLBACK 才释放(这是为什么长事务占用大量锁资源)
  3. 死锁检测innodb_deadlock_detect=ON,默认开):自动检测死锁并回滚代价小的事务

行锁格式(Record / Gap / Next-Key)的细节是下一篇的主题——它决定了"锁住多少范围",直接关系并发度。这里先建立一个印象:行锁不是"锁住一行"那么简单——RR 隔离级别下,一条 SELECT ... WHERE id BETWEEN 10 AND 20 FOR UPDATE 可能锁住 10 条记录 + 9 个间隙 + 一个 Next-Key。

锁的获取与释放时机

四、三层锁的选择策略

理解了三层粒度,再说生产中"加什么锁"的策略——这是工程师真正要做的决定。

备份场景

  • 全 InnoDB + 一致性快照 → mysqldump --single-transaction(无锁)
  • 混合引擎或纯 MyISAM → FTWRL(必须接受短暂阻塞)

DDL 场景

  • 简单 DDL(如加索引)→ pt-online-schema-change / gh-ost(不阻塞 DML)
  • 复杂 DDL → 业务低峰 + 监控 MDL 队列

业务事务场景

  • 默认走 MVCC 快照读 → 不加锁
  • 需要当前读 → FOR UPDATE(X 锁)或 FOR SHARE(S 锁)
  • 严格控制并发 → 显式加锁 + 控制事务边界

常见误区

  1. "加了索引就不会锁太多" —— 不准确,RR 隔离级别下范围查询会加间隙锁,即使索引很精确
  2. "FOR UPDATE 性能差,不要用" —— 错,该用的地方必须用,丢失更新比性能问题严重得多
  3. "锁会自动释放" —— 不准确,必须等事务结束才释放——长事务 = 大量锁占用

锁选择决策树

回头看那个半夜备份故障——根因是当时还有 MyISAM 表,必须用 FTWRL,而 FTWRL 跟长查询冲突。修复方法:把所有 MyISAM 迁到 InnoDB,改用 mysqldump --single-transaction,备份完全无锁。

锁真正教会我的,不是"行锁比表锁好",而是锁是粒度、代价、并发性的三角权衡——粒度越细并发越好但管理成本越高,粒度越粗管理简单但并发差。InnoDB 的设计智慧全在这个三角里——用 MVCC 解决"读不加锁"、用行锁解决"写不互斥"、用间隙锁解决"防幻读"。

下一篇讲行锁的三种格式——Record / Gap / Next-Key,看 InnoDB 怎么用最小代价锁住合适的范围。


关于十三Tech 资深服务端研发,AI 实践者,专注分享真实可落地的技术经验。 相信 AI 是程序员的最佳搭档。

联系方式:569893882@qq.com GitHub:@TriTechAI