面试必备:MySQL 锁机制深度解析
为什么需要锁?
多事务并发时,锁用来协调对同一数据的访问,保证隔离性和一致性。
事务 A 事务 B
─────────────────────────────────────────
SELECT balance SELECT balance
← 1000 ← 1000
UPDATE balance = 1100
UPDATE balance = 900
← 1000-1000=900
← 1000+1000=2000 ← 错误!丢失了 A 的更新
MySQL 锁分类
按粒度分:
├── 表锁(Table Lock)
├── 行锁(Row Lock)
└── 页面锁(Page Lock,InnoDB 不常用)
按类型分:
├── 共享锁(S Lock)
├── 排他锁(X Lock)
├── 意向锁(IX、IS)
└── Record Lock、Gap Lock、Next-Key Lock
按特性分:
├── 乐观锁
└── 悲观锁
共享锁 vs 排他锁
共享锁(S Lock)
允许事务读取数据,其他事务也可以加共享锁读取。
-- 加共享锁
SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE;
-- 或
SELECT * FROM user WHERE id = 1 FOR SHARE; -- MySQL 8.0+
-- 其他事务也可以:
SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE; -- ✓ 可以
UPDATE user SET name = 'new' WHERE id = 1; -- ✗ 阻塞(排他锁冲突)
排他锁(X Lock)
允许事务读写数据,其他事务不能加任何锁。
-- 加排他锁
SELECT * FROM user WHERE id = 1 FOR UPDATE;
-- 或
UPDATE user SET name = 'new' WHERE id = 1; -- 自动加排他锁
-- 其他事务:
SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE; -- ✗ 阻塞
UPDATE user SET name = 'another' WHERE id = 1; -- ✗ 阻塞
| 锁组合 | 兼容 |
|---|---|
| S + S | ✓ 兼容 |
| S + X | ✗ 不兼容 |
| X + X | ✗ 不兼容 |
行锁 vs 表锁
InnoDB 行锁
只锁住选中的行,并发性能高。
-- 行锁示例
SELECT * FROM user WHERE id = 1 FOR UPDATE; -- 只锁 id=1 这一行
UPDATE user SET name = 'new' WHERE id = 2; -- id=2 可以正常修改(不阻塞)
行锁特点:
- 开销大,加锁慢
- 锁定粒度小
- 死锁概率高
- 支持 MVCC
MyISAM 表锁
锁住整张表,并发性能低。
LOCK TABLES user WRITE; -- 排他表锁
UNLOCK TABLES;
表锁特点:
- 开销小,加锁快
- 锁定粒度大
- 无死锁
- 不支持事务
InnoDB 的行锁算法
1. Record Lock(记录锁)
锁住某一条索引记录。
SELECT * FROM user WHERE id = 1 FOR UPDATE;
-- 锁住 id=1 的索引记录
2. Gap Lock(间隙锁)
锁住索引记录之间的间隙,防止插入。
SELECT * FROM user WHERE id BETWEEN 1 AND 10 FOR UPDATE;
-- 锁住 (1, 10) 区间
-- 其他事务无法插入 id 在 1~10 之间的记录
作用:防止幻读。
3. Next-Key Lock(临键锁)
Record Lock + Gap Lock 的组合。
索引:1, 5, 10, 15, 20
SELECT * FROM user WHERE id = 5 FOR UPDATE;
-- 锁住:
-- Record Lock: id=5
-- Gap Lock: (1, 5) 和 (5, 10)
-- Next-Key Lock: (-∞, 5] 和 (5, 10]
4. Insert Intention Lock(插入意向锁)
INSERT 操作在等待时产生的锁。
-- 事务 A
INSERT INTO user VALUES (5); -- 等待 Gap Lock
-- 事务 B
INSERT INTO user VALUES (5); -- 也等待 Gap Lock
-- 两者都产生 Insert Intention Lock,互不阻塞
意向锁(Intention Lock)
InnoDB 为了在表锁和行锁之间建立桥梁。
-- 事务 A:获取行锁
SELECT * FROM user WHERE id = 1 FOR UPDATE; -- id=1 行被锁定
-- 事务 B:尝试获取表锁
LOCK TABLES user WRITE; -- 需要检查是否有行锁
-- InnoDB 检查:存在 IX 意向锁吗?
-- IX:有意获取行排他锁
-- IS:有意获取行共享锁
| 锁类型 | 说明 |
|---|---|
| IS(意向共享锁) | 事务有意对表中的行加共享锁 |
| IX(意向排他锁) | 事务有意对表中的行加排他锁 |
兼容关系:
| X | IX | S | IS | |
|---|---|---|---|---|
| X | ✗ | ✗ | ✗ | ✗ |
| IX | ✗ | ✓ | ✗ | ✓ |
| S | ✗ | ✗ | ✓ | ✓ |
| IS | ✗ | ✓ | ✓ | ✓ |
乐观锁 vs 悲观锁
悲观锁
假设并发冲突,先加锁再操作。
-- 悲观锁
SELECT * FROM user WHERE id = 1 FOR UPDATE;
-- 修改
UPDATE user SET balance = balance - 100 WHERE id = 1;
COMMIT;
乐观锁
假设冲突少,先操作再检查版本。
-- 乐观锁:用版本号
SELECT id, balance, version FROM user WHERE id = 1;
-- 结果:id=1, balance=1000, version=1
UPDATE user
SET balance = balance - 100, version = version + 1
WHERE id = 1 AND version = 1; -- 检查版本
-- 如果 version 不是 1,说明被其他事务修改了,失败
-- 失败后重试(乐观锁失败处理)
死锁(Dead Lock)
什么是死锁?
两个或多个事务相互等待对方持有的锁,形成循环等待。
事务 A 事务 B
─────────────────────────────────────────
BEGIN;
UPDATE user SET name='A' WHERE id=1; -- 锁住 id=1
BEGIN;
UPDATE user SET name='B' WHERE id=2; -- 锁住 id=2
UPDATE user SET name='B' WHERE id=1; -- 等待 A 的 id=1
UPDATE user SET name='A' WHERE id=2; -- 等待 B 的 id=2
────────────────────
死锁!
InnoDB 死锁处理
InnoDB 自动检测死锁,选择回滚最小事务(undo log 少)。
检测到死锁
↓
选择回滚代价最小的事务
↓
回滚该事务,释放锁
↓
其他事务继续执行
如何避免死锁?
-- 1. 固定顺序访问资源
UPDATE user SET ... WHERE id=1; -- 先 id=1
UPDATE user SET ... WHERE id=2; -- 再 id=2
-- 事务 B 也按同样顺序 → 不会死锁
-- 2. 减少锁持有时间
-- 不要在事务中做太多事情
-- 3. 使用低隔离级别
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 4. 添加合理索引
-- 减少锁的范围
锁相关命令
-- 查看当前锁等待
SHOW ENGINE INNODB STATUS;
-- 查看锁信息
SELECT * FROM information_schema.INNODB_LOCKS;
-- 查看锁等待
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
-- 查看事务
SELECT * FROM information_schema.INNODB_TRX;
-- 杀掉阻塞进程
KILL trx_mysql_thread_id;
面试高频问题
Q1:InnoDB 和 MyISAM 的锁有什么区别?
| 特性 | InnoDB | MyISAM |
|---|---|---|
| 锁粒度 | 行锁 + 表锁 | 表锁 |
| 事务支持 | 支持 | 不支持 |
| 锁类型 | 共享/排他/意向 | 共享/排他 |
| 死锁 | 可能(自动检测) | 不可能 |
| MVCC | 支持 | 不支持 |
| 索引 | 聚簇索引 | 非聚簇 |
Q2:什么时候会锁全表?
-- 1. 不走索引
UPDATE user SET name='new' WHERE name='old'; -- name 没有索引
-- 2. 使用 LIKE 模糊匹配
SELECT * FROM user WHERE name LIKE '%张%' FOR UPDATE; -- 可能锁全表
-- 3. 锁表操作
LOCK TABLES user WRITE;
Q3:RR 隔离级别如何避免幻读?
- 快照读:MVCC 保证每次快照读看到的数据一致
- 当前读:Next-Key Lock 锁住索引范围
Q4:如何排查死锁?
-- 1. 查看死锁日志
SHOW ENGINE INNODB STATUS;
-- 2. 查看最近死锁
SHOW FULL PROCESSLIST;
总结
锁分类:
├─ 按粒度:行锁、表锁、页面锁
├─ 按类型:共享锁(S)、排他锁(X)
└─ 按算法:Record Lock、Gap Lock、Next-Key Lock
InnoDB 锁特点:
├─ 行锁 + 意向锁
├─ 支持事务
├─ MVCC + Next-Key Lock
└─ 自动死锁检测
避免死锁:
├─ 固定顺序访问
├─ 减少锁持有时间
├─ 合理使用索引
└─ 降低隔离级别
乐观锁 vs 悲观锁:
├─ 悲观锁:先加锁后操作
└─ 乐观锁:先操作后检查(版本号/CAS)