先说结论

InnoDB 的锁要结合隔离级别、索引和 SQL 的实际访问范围来看。排查死锁时,先找互相等待的事务和加锁顺序,再通过缩短事务、补索引或统一访问顺序解决。

锁的维度

从兼容性看,共享锁允许多个事务读取同一资源,排他锁与其他共享或排他锁冲突。普通快照读通常不加记录共享锁,SELECT ... FOR SHAREFOR UPDATE 属于锁定读。

从作用范围看,InnoDB 常见锁包括:

  • Record Lock:锁住某条索引记录。
  • Gap Lock:锁住两个索引值之间的间隙,阻止插入,不锁定已有记录本身。
  • Next-Key Lock:记录锁与其前方间隙的组合。
  • Insert Intention Lock:插入前对目标间隙表达插入意图,不同位置的插入通常可并发。
  • Intention Lock:表级意向锁,表示事务准备在表中某些行加共享或排他锁,帮助快速判断表锁兼容性。

行锁为什么与索引有关

InnoDB 通过索引定位并锁定记录。若更新条件缺少合适索引,执行器可能扫描大量索引记录,并在过程中锁住远超预期的数据。即使最终只更新一行,也可能造成大范围阻塞。

UPDATE orders SET status = 'CLOSED'
WHERE external_no = 'X20260721001';

external_no 没有索引,数据库需要扫描更多记录。线上写操作条件必须确认索引与选择性,而不是只看受影响行数。

唯一索引等值查询的锁范围

当使用唯一索引精确命中已存在记录时,通常可以退化为记录锁,不必锁住整个间隙。若查询的唯一键不存在,为防止其他事务插入满足条件的记录,仍可能锁住目标间隙。

锁范围受 MySQL 版本、隔离级别、索引是否唯一、条件是否等值、记录是否存在和实际执行计划影响。分析时把这些条件说清楚,不要拿一个固定范围套所有场景。

RC 与 RR 的差异

Repeatable Read 为了支持当前读的范围一致性,会更广泛使用 Gap Lock 和 Next-Key Lock。Read Committed 通常减少间隙锁使用,从而提高并发,但同一事务两次读取可能看到不同已提交版本。

切换隔离级别不是单纯性能开关。必须确认业务是否依赖重复读语义、唯一性检查以及现有并发控制逻辑。

死锁的四个条件

死锁通常满足互斥、占有且等待、不可剥夺、循环等待。数据库中最常见的形态是两个事务以相反顺序持有并请求资源:

事务 A:锁订单 1 → 等待订单 2
事务 B:锁订单 2 → 等待订单 1

InnoDB 会检测等待图中的环,并选择一个事务回滚以打破死锁。被回滚不代表数据库故障,应用应对死锁错误做有限、带退避且幂等的重试。

一个典型死锁案例

-- 事务 A
UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;

-- 事务 B
UPDATE account SET balance = balance - 50 WHERE id = 2;
UPDATE account SET balance = balance + 50 WHERE id = 1;

两个转账事务按不同账户顺序加锁。解决方法是统一按较小账户 ID 到较大账户 ID 加锁。统一顺序消除循环等待,同时事务仍要保持短小。

死锁与锁等待超时的区别

死锁存在等待环,数据库通常能快速检测并回滚一个事务。锁等待超时可能只是某事务长时间持锁,并不存在环;等待方达到 innodb_lock_wait_timeout 后失败。两者排查方向相关但不完全相同。

线上排查步骤

  1. 保存应用错误中的事务、SQL、参数和时间点。
  2. 查看 SHOW ENGINE INNODB STATUS 的最近死锁信息。
  3. 使用 Performance Schema 的事务、数据锁和锁等待视图还原持有者与等待者。
  4. 查看事务开始时间和当前语句,警惕已经空闲但未提交的会话。
  5. 对照执行计划确认扫描与加锁范围。
  6. 检查代码中多表、多行访问顺序是否一致。

不要只盯着最后报错的 SQL。它可能只是等待方,真正问题是另一个事务更早执行的语句或未及时提交。

降低死锁和阻塞的方法

  • 按固定顺序访问表与记录。
  • 通过高选择性索引缩小扫描和加锁范围。
  • 减小事务,每次只包含必须原子提交的数据库操作。
  • 不在事务中做 HTTP 调用、消息等待或人工交互。
  • 大批量更新按主键分批,避免一次锁住过多记录。
  • 使用合理超时,让失败快速暴露;重试必须有限且幂等。

常见问题

追问 1:普通 SELECT 会加行锁吗?

InnoDB 的普通一致性读通常通过 MVCC 读取快照,不加记录锁。显式 FOR UPDATEFOR SHARE 或写操作属于当前读,需要相应锁。

追问 2:没有索引的 UPDATE 是不是锁表?

技术上通常仍是对扫描到的索引记录加锁,不等同于显式表锁;但因为可能扫描并锁住大量记录,效果上会严重阻塞并发,常被口语化为“像锁表”。

追问 3:发生死锁应该把超时时间调大吗?

调大锁等待超时不能消除等待环。应统一资源顺序、缩小锁范围和事务,并对被选为牺牲者的事务做安全重试。

追问 4:间隙锁锁住的是不存在的记录吗?

更准确地说,它锁定索引值之间的范围,阻止其他事务向该范围插入,而不是锁住一个虚构行。不同事务的间隙锁在某些情况下可以兼容。

追问 5:如何快速发现长事务?

查看 InnoDB 事务视图、会话状态与事务开始时间,并结合 Undo 历史长度、锁等待和应用链路。连接处于 Sleep 不代表没有打开事务。