数据库锁机制
锁用来协调并发写以及当前读对数据正确性的要求。InnoDB 以行级锁为主,并配合意向锁、间隙锁等实现范围安全。锁加得越宽、持有越久,并发越差、死锁概率越高。
1. 锁的粒度与意向锁
| 粒度 | 说明 |
|---|---|
| 表锁 | 锁整张表;DDL、部分场景会用到 |
| 行锁 | 锁具体索引记录;InnoDB DML 主力 |
| 意向锁(Intention Lock) | 表级「预告」:表示事务即将/正在持有行锁,便于快速判断表锁是否冲突 |
常见行锁模式:
- 共享锁(S):读锁;
SELECT ... LOCK IN SHARE MODE/FOR SHARE - 排他锁(X):写锁;
SELECT ... FOR UPDATE、UPDATE/DELETE等
兼容性(简化):S 与 S 兼容;S 与 X、X 与 X 不兼容。
2. 记录锁、间隙锁、临键锁
在 RR 隔离级别下,InnoDB 为防止范围上的幻读等问题,引入间隙相关锁(RC 下间隙锁更少):
| 类型 | 锁住什么 | 作用 |
|---|---|---|
| 记录锁(Record Lock) | 索引上的一条记录 | 防止别人改/删这一行 |
| 间隙锁(Gap Lock) | 索引记录之间的间隙 | 防止别人在间隙中插入 |
| 临键锁(Next-Key Lock) | 记录锁 + 该记录前的间隙 | 记录锁与间隙锁的组合,RR 下常见 |
索引有序值: ..., 10, 20, 30, ...
^间隙^ ^间隙^
临键思想:锁住 (10, 20] 这类区间,避免幻影插入
要点:
- 锁是加在索引上的;没有合适索引时,可能退化为更粗的扫描与更多锁。
- 唯一索引等值查询命中行时,行为与非唯一索引、范围查询不同(锁范围更小或更精确)。
- 「插入意图锁」用于插入时与间隙锁的协调。
这也是为什么 索引优化 与锁息息相关:查询走错索引或全表,锁范围会爆炸。
3. 谁会加锁?加多久?
| 操作 | 典型加锁 |
|---|---|
普通 SELECT(快照读) | 通常不加行锁 |
SELECT ... FOR UPDATE | 对扫描到的行加 X 锁(及可能的间隙) |
UPDATE / DELETE | 对定位到的行加 X 锁 |
INSERT | 插入行上的锁;可能与间隙锁冲突 |
COMMIT / ROLLBACK | 释放本事务持有的锁 |
原则:越晚加锁、越早提交,锁持有时间越短,吞吐越好。
4. 死锁
4.1 典型形态
事务 A: 锁住行 1 → 等待行 2
事务 B: 锁住行 2 → 等待行 1
→ 环路等待 → InnoDB 死锁检测 → 回滚代价较小的一方
业务层也可能因「先锁订单再锁用户」与「先锁用户再锁订单」顺序不一致而死锁。
4.2 处理思路
- 固定加锁顺序(如始终按主键从小到大锁多行)。
- 缩小事务、减少一次锁定的行数。
- 用合适索引,避免扫描并锁住无关行。
- 捕获死锁错误后有限次重试(幂等很重要)。
- 分析日志:
SHOW ENGINE INNODB STATUS\G中的LATEST DETECTED DEADLOCK。
-- 可选:观察锁等待
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
-- 性能_schema / sys 库中也有锁等待相关视图(版本相关)
5. 乐观锁与悲观锁(应用层说法)
| 方式 | 做法 | 适合 |
|---|---|---|
| 悲观锁 | 读时就 FOR UPDATE,认为冲突多 | 冲突率高的库存扣减等 |
| 乐观锁 | 带 version 字段更新:UPDATE ... WHERE id=? AND version=? | 冲突少、读多写少 |
-- 乐观锁示例
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 10 AND stock >= 1;
-- 影响行数为 0 则表示冲突或库存不足,业务层重试或失败
二者可与数据库行锁配合,不是互斥概念。
6. 排查清单
- 是否长事务? 先缩短事务边界。
- 是否锁了过多行? 看执行计划是否全表/大 range。
- 隔离级别是否 RR 且大量范围当前读? 间隙锁是否符合预期。
- 是否死锁循环? 查
INNODB STATUS与应用日志。 - 锁等待超时? 区分「业务慢」与「被别的事务堵住」。
7. 与全文的串联
索引决定「扫哪些行、锁哪些间隙」
事务决定「修改何时对别人可见、何时释放锁」
隔离级别决定「快照读看到什么、当前读加什么锁」