锁的分类
锁
├── 全局锁(FTWRL)
├── 表级锁
│ ├── 数据锁(lock tables)
│ └── 元数据锁(MDL)
└── 行级锁
├── 共享锁(S 锁 / 读锁)
└── 排他锁(X 锁 / 写锁)
| 引擎 | 默认锁粒度 | 是否支持事务 |
|---|---|---|
| MyISAM | 表级 | ✗ |
| InnoDB | 行级(+ 表级) | ✓ |
读锁 / 写锁的兼容性
- 读锁(S):别的事务可以读,不能写
- 写锁(X):别的事务既不能读也不能写(InnoDB 借助 MVCC 快照读可读)
- 多个读锁可兼容,其他组合均不兼容
全局锁:FTWRL
FLUSH TABLES WITH READ LOCK;
整个库进入只读状态,主要用于保证数据备份的一致性(一般在备库、从库使用)。
表级锁
数据锁(lock tables)
LOCK TABLES t_user READ; -- 其它会话只能读,不能写
LOCK TABLES t_user WRITE; -- 其它会话不能读也不能写
UNLOCK TABLES;
元数据锁(MDL,MetaData Lock)
元数据 = 表结构、字段、数据类型、索引等。
- 事务访问数据时,自动加 MDL 读锁
- 事务修改表结构时(如
ALTER TABLE),自动加 MDL 写锁 - 锁按队列处理:先到先服务
- 典型坑:长事务持有 MDL 读锁时,
ALTER TABLE被阻塞;后续所有访问该表的查询/写入也跟着排队——容易引发整库挂起!
行级锁
共享锁(S 锁 / 读锁)
SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE;
排他锁(X 锁 / 写锁)
SELECT * FROM user WHERE id = 1 FOR UPDATE;
UPDATE / DELETE / INSERT 隐式加 X 锁。
MVCC 多版本并发控制

InnoDB 在每行数据后面保存了 3 个隐藏列:
DB_TRX_ID:最近一次修改该行的事务 IDDB_ROLL_PTR:回滚指针,指向 undo log 中的旧版本DB_ROW_ID:行 ID(无主键时作为隐藏主键)
事务通过 ReadView(活跃事务 ID 集合) 沿版本链找到第一个对自己可见的版本——这就是非锁定一致性读。
当前读 vs 快照读
| 类型 | 语句 | 读取版本 | 是否加锁 |
|---|---|---|---|
| 当前读 | SELECT ... LOCK IN SHARE MODE | 最新 | 加 S 锁 |
| 当前读 | SELECT ... FOR UPDATE | 最新 | 加 X 锁 |
| 当前读 | UPDATE / DELETE / INSERT | 最新 | 加 X 锁 |
| 快照读 | 普通 SELECT | 历史版本(MVCC) | 不加锁 |
- Read Committed 隔离级别:每次
SELECT都生成新的 ReadView - Repeatable Read 隔离级别:事务第一次
SELECT生成 ReadView,后续复用
间隙锁(Gap Lock)与幻读
幻读问题
RR 隔离级别下,同一事务两次执行 SELECT * FROM user WHERE age > 10,两次结果行数不同(中间被别的事务插入了 age=15 的数据)。
间隙锁的引入
示例表:

辅助索引 age 形成的”区间”:

InnoDB 把 age 索引划分为:
- 行数据:
10和30 - 间隙:
(-∞, 10)、(10, 30)、(30, +∞)
间隙锁锁的是区间本身,不允许其他事务在间隙内 INSERT。
Next-Key Lock
Next-Key Lock = 行锁(Record Lock)+ 间隙锁(Gap Lock)
-- 锁定间隙 (10, 20),不会命中已有数据
SELECT * FROM user WHERE age = 11;
特性:
- 唯一索引等值查询(完全命中)→ 只用行锁,忽略间隙锁
- 普通索引查询 → 必加间隙锁(因为取值可能重复)
- 范围查询扫描过的区间 → 全部加锁(包括间隙)
-- name 字段无索引的 UPDATE:扫描整个表,所有间隙都会被锁
UPDATE user SET age = 20 WHERE name = 'a';
⚠️ 死锁高发场景:用无索引字段做条件更新,所有间隙都会被锁,并发时极易死锁。生产中应保证更新条件走索引。
下一篇深入 InnoDB 的”日志系统”:redo log(崩溃恢复)+ undo log(事务回滚/MVCC)+ binlog(主从复制)三者的关系与两阶段提交。