目录
🔹 3. 临键锁(Next-Key Lock) = 记录锁 + 间隙锁
前言:为什么需要锁?
想象一个银行转账场景:
用户A向用户B转账100元。
事务1:A账户减100元
事务2:B账户加100元
如果这两个操作同时被多个线程执行,而没有锁的保护,可能会出现:
- A扣了两次钱,但B只收到一次 → 数据不一致!
锁(Lock) 就是MySQL用来保证并发安全和数据一致性的核心机制。它确保在并发环境下,多个事务对同一数据的访问是有序、可控的。
🔐 一、MySQL锁的分类:全局视角
MySQL的锁体系非常复杂,我们从几个维度来理解:
📌 1. 按粒度划分(最重要!)
| 锁类型 | 说明 | 加锁开销 | 并发度 | 使用场景 |
|---|---|---|---|---|
| 全局锁 | 锁住整个实例 FLUSH TABLES WITH READ LOCK | 小 | 极低 | 备份(mysqldump) |
| 表级锁 | 锁住整张表 LOCK TABLES ... WRITE | 小 | 低 | MyISAM引擎、DDL操作 |
| ✅ 行级锁 | 锁住单行或多行数据 | 大 | 高 | InnoDB核心,支持高并发 |
💡 重点:InnoDB引擎主要使用行级锁,这是它支持高并发的关键。
📌 2. 按模式划分(行锁的核心)
| 锁模式 | 英文 | 兼容性 | 说明 |
|---|---|---|---|
| 共享锁(S锁) | Shared Lock | ✅ S锁 | 读锁,多个事务可同时读 |
| 排他锁(X锁) | Exclusive Lock | ❌ 任何锁 | 写锁,阻塞其他读写 |
| 意向共享锁(IS) | Intention Shared | - | 表级锁,表示“打算加S锁” |
| 意向排他锁(IX) | Intention Exclusive | - | 表级锁,表示“打算加X锁” |
🔑 核心规则:
- S锁与S锁兼容:多个事务可同时读同一行。
- S锁与X锁互斥:读时不能写,写时不能读。
- X锁与X锁互斥:写时不能写(避免脏写)。
🧱 二、InnoDB行锁的实现:记录锁、间隙锁、临键锁
InnoDB的行锁不是简单的“锁一行”,而是基于索引和B+树结构实现的复杂机制。
🔹 1. 记录锁(Record Lock)
- 作用:锁定索引记录本身。
- 示例:
-- 假设id是主键 SELECT * FROM users WHERE id = 1 FOR UPDATE; -- 对id=1的记录加X锁
🔹 2. 间隙锁(Gap Lock)
- 作用:锁定索引记录之间的间隙,防止幻读(Phantom Read)。
- 目的:阻止其他事务在间隙中插入新记录。
- 示例:
-- users表中id=5, 10, 15 -- 间隙:(-∞,5), (5,10), (10,15), (15,+∞) SELECT * FROM users WHERE id BETWEEN 6 AND 9 FOR UPDATE; -- 会锁定间隙 (5,10),阻止插入id=7,8的记录
🔹 3. 临键锁(Next-Key Lock) = 记录锁 + 间隙锁
- 作用:锁定记录本身 + 记录之前的间隙。
- 范围:
(gap_left, record] - 默认行为:InnoDB在可重复读(REPEATABLE READ) 隔离级别下,默认使用临键锁防止幻读。
🌰 案例:锁定id=10的记录(临键锁)
- 锁住间隙:
(5,10)- 锁住记录:
id=10- 范围:
(5,10]
🔄 三、锁的实践:如何加锁?
✅ 1. 显式加锁(悲观锁)
-- 1. 共享锁(S锁):读锁
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE; -- MySQL 8.0.1之前
-- 或
SELECT * FROM users WHERE id = 1 FOR SHARE; -- MySQL 8.0.1+
-- 2. 排他锁(X锁):写锁
SELECT * FROM users WHERE id = 1 FOR UPDATE;
⚠️ 注意:
FOR UPDATE不仅锁住查询行,还会锁住相关索引(包括二级索引),防止其他事务修改。
✅ 2. 隐式加锁(由SQL语句自动触发)
| SQL语句 | 自动加的锁 |
|---|---|
INSERT | 在插入行上加X锁 |
UPDATE | 在修改行上加X锁 |
DELETE | 在删除行上加X锁 |
SELECT(普通) | 不加锁(快照读) |
SELECT ... FOR UPDATE | X锁 |
SELECT ... LOCK IN SHARE MODE | S锁 |
🧪 四、实战案例:转账场景中的锁应用
🏦 需求:实现安全的银行转账
-- 事务1:A转账100给B
START TRANSACTION;
-- 1. 锁住A账户(防止余额被修改)
SELECT balance FROM accounts WHERE user_id = 'A' FOR UPDATE;
-- 2. 检查余额是否足够
-- 3. A账户减100
UPDATE accounts SET balance = balance - 100 WHERE user_id = 'A';
-- 4. 锁住B账户
SELECT balance FROM accounts WHERE user_id = 'B' FOR UPDATE;
-- 5. B账户加100
UPDATE accounts SET balance = balance + 100 WHERE user_id = 'B';
COMMIT;
✅ 关键点:
- 使用
FOR UPDATE确保在事务结束前,A、B账户余额不会被其他事务修改。- 避免了“脏读”、“不可重复读”和“幻读”。
💣 五、死锁(Deadlock):并发的“致命”陷阱
❌ 什么是死锁?
两个或多个事务互相等待对方释放锁,导致所有事务都无法继续。
-- 事务1
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE user_id = 'A'; -- 锁住A
UPDATE accounts SET balance = balance + 100 WHERE user_id = 'B'; -- 等待B(被事务2锁住)
-- 事务2
START TRANSACTION;
UPDATE accounts SET balance = balance - 50 WHERE user_id = 'B'; -- 锁住B
UPDATE accounts SET balance = balance + 50 WHERE user_id = 'A'; -- 等待A(被事务1锁住)
👉 结果:事务1等B,事务2等A → 死锁!
🛠️ MySQL如何解决死锁?
- 死锁检测:InnoDB有一个死锁检测器,会定期检查锁等待图。
- 自动回滚:一旦发现死锁,会选择一个代价较小的事务进行回滚(
ROLLBACK),释放其锁,让其他事务继续。 - 报错:被回滚的事务会收到错误:
Deadlock found when trying to get lock; try restarting transaction
💡 最佳实践:
- 尽量按相同顺序访问资源(如先A后B)。
- 减少事务大小,尽快提交。
- 捕获死锁异常,实现自动重试机制。
📊 六、查看锁信息(DBA必备技能)
-- 1. 查看当前正在运行的事务
SELECT * FROM information_schema.INNODB_TRX\G
-- 2. 查看锁等待情况
SELECT * FROM information_schema.INNODB_LOCKS; -- 旧版本
-- MySQL 8.0+ 使用:
SELECT * FROM performance_schema.data_locks;
-- 3. 查看锁等待的详细信息
SELECT * FROM performance_schema.data_lock_waits;
-- 4. 查看死锁日志(需开启)
SHOW ENGINE INNODB STATUS\G
-- 在输出中查找 "LATEST DETECTED DEADLOCK" 部分
🎯 总结:锁机制核心要点
| 概念 | 关键点 |
|---|---|
| 锁粒度 | 行锁 > 表锁 > 全局锁(InnoDB主打行锁) |
| 锁模式 | S锁(读)、X锁(写)、IS/IX(意向锁) |
| 行锁实现 | 记录锁 + 间隙锁 + 临键锁(防幻读) |
| 加锁方式 | FOR UPDATE(X锁)、FOR SHARE(S锁) |
| 死锁处理 | 检测 + 回滚 + 重试 |
| 最佳实践 | 小事务、同序访问、及时提交 |

2573

被折叠的 条评论
为什么被折叠?



