跳到主要内容

数据库锁机制

锁用来协调并发写以及当前读对数据正确性的要求。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 处理思路​

  1. 固定加锁顺序(如始终按主键从小到大锁多行)。
  2. 缩小事务、减少一次锁定的行数。
  3. 用合适索引,避免扫描并锁住无关行。
  4. 捕获死锁错误后有限次重试(幂等很重要)。
  5. 分析日志: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. 排查清单​

  1. 是否长事务? 先缩短事务边界。
  2. 是否锁了过多行? 看执行计划是否全表/大 range。
  3. 隔离级别是否 RR 且大量范围当前读? 间隙锁是否符合预期。
  4. 是否死锁循环? 查 INNODB STATUS 与应用日志。
  5. 锁等待超时? 区分「业务慢」与「被别的事务堵住」。

7. 与全文的串联​

索引决定「扫哪些行、锁哪些间隙」
事务决定「修改何时对别人可见、何时释放锁」
隔离级别决定「快照读看到什么、当前读加什么锁」

回到专题入口:数据库总览。
索引线:索引总览。
事务线:事务基础 · 隔离级别。