数据库锁

锁有哪些类型:全局锁、表级锁、行级锁、共享锁、排他锁、记录锁、间隙锁等。

  1. 全局锁
    全局锁会限制整个数据库实例中的部分操作,典型命令是:
    FLUSH TABLES WITH READ LOCK;
  2. 表级锁
    常见类型包括:
  • 表锁:LOCK TABLES 显式加锁;MyISAM 等存储引擎也大量使用表级锁。
  • 意向锁:InnoDB 的 IS、IX,用于表示事务准备在表中的记录上加共享锁或排他锁。
  • 元数据锁(MDL):保护表结构等元数据,防止并发 DDL 与相关 SQL 发生不安全的冲突。
  • AUTO-INC 锁:用于协调自增值分配,具体机制取决于 MySQL 配置和语句类型。
    例如:
    ALTER TABLE orders ADD COLUMN remark VARCHAR(100);

这类 DDL 可能需要获取 MDL。若有长期事务持有相关元数据锁,DDL 就可能一直等待;后续访问该表的 SQL 也可能受到影响。

优化思路: 上线 DDL 前检查长事务,选择合适的在线变更方式,并监控 MDL 等待。
3. 行级锁
InnoDB 的行级锁通常围绕索引记录实现,而不是简单地对 SQL 逻辑上涉及的每一行加锁。

例如:
UPDATE account
SET balance = balance - 100
WHERE id = 1;

如果 id 是主键,这条 SQL 通常只需要锁定对应的索引记录及相关必要资源,竞争范围较小。

如果条件没有合适索引,执行器可能需要扫描大量记录,导致更多记录被检查、锁定,或者在扫描期间产生更大范围的锁竞争。

优化核心:让 SQL 通过合适的索引定位尽可能少的记录,并缩短持锁时间。

主要用途是某些需要全局一致性视图的备份场景。

  • 产生:执行全局读锁命令。
  • 影响:限制相关表的写操作;具体行为还受其他锁和操作类型影响。
  • 优化:优先使用支持一致性快照的在线备份方案,减少全局锁持有时间。

按锁定范围分类:Record、Gap、Next-Key
这是排查 InnoDB 死锁和锁等待时最重要的一部分。InnoDB 行级锁实际锁定的是索引记录及其相关范围。

MySQL开发者区
+1

  1. 记录锁(Record Lock)
    锁定某条索引记录,不锁定其前面的间隙。

例如:
CREATE TABLE account (
id BIGINT PRIMARY KEY,
balance DECIMAL(10, 2)
) ENGINE = InnoDB;

SELECT *
FROM account
WHERE id = 100
FOR UPDATE;

假设 id = 100 存在,且使用主键进行唯一值查找,在常见情况下只需要对该记录加排他记录锁。

此时:

  • 其他事务不能直接修改或删除这条记录。
  • 其他事务仍可以操作不冲突的其他记录。
  • 普通一致性读通常仍能通过 MVCC 读取符合快照规则的版本。
    常见场景: 按主键更新用户、订单、账户等单条数据。
  1. 间隙锁(Gap Lock)
    锁定索引记录之间的间隙,主要用于阻止其他事务向特定范围插入新记录。

假设索引中存在以下值:
10、20、30

事务 A 执行:
START TRANSACTION;

SELECT *
FROM account
WHERE id BETWEEN 10 AND 20
FOR UPDATE;

在默认 REPEATABLE READ 隔离级别下,范围锁定可能涉及 Next-Key Lock 和边界间隙。事务 B 如果尝试插入范围内的新记录,可能因为间隙锁而等待。
INSERT INTO account (id, balance)
VALUES (15, 100);

这里的关键不是 id = 15 原本有没有记录,而是它是否落入了被锁定的索引间隙。

间隙锁的主要目的,是防止范围内出现影响并发事务正确性的幻读相关插入行为。
3. Next-Key Lock(临键锁)
Next-Key Lock = Record Lock + Gap Lock。

也就是:

  • 锁住某条索引记录。
  • 同时锁住该记录之前的索引间隙。
    假设索引值为 10、20、30,那么针对记录 20 的一个 Next-Key Lock,可以覆盖 (10, 20]。
    三种锁的区别
    Record Lock
    10

20

30

只锁定记录 20。
Gap Lock
10

间隙 (10, 20)

20

阻止在对应间隙内插入新记录,但不单纯因为 Gap Lock 就禁止对边界记录本身进行所有操作。
Next-Key Lock
10

(10, 20]

30

同时锁定间隙和记录 20。
示意图用于理解锁定范围。实际锁范围取决于索引、查询条件、执行计划、隔离级别和记录是否存在。

  1. 插入意向锁(Insert Intention Lock)
    事务插入新记录前,InnoDB 会获取插入意向间隙锁。

它不是简单地说“同一个间隙只能有一个事务插入”。例如,索引值为 10、20 时,两个事务分别插入 12 和 15,在没有其他冲突锁的情况下,可以并发推进。

但是,如果其他事务持有覆盖该位置的冲突间隙锁或 Next-Key Lock,插入就可能等待。

锁为什么产生:SQL 语句、事务隔离级别、索引使用情况、锁的获取顺序等。

锁怎么优化:通过执行计划、事务分析、锁等待信息和业务代码定位根因,而不是简单地给 SQL 加索引或缩短超时时间。
第一步:获取死锁现场

MySQL 可以执行:
SHOW ENGINE INNODB STATUS;

重点查看 LATEST DETECTED DEADLOCK,分析:

  • 涉及哪些事务、表和索引。
  • 每个事务执行了什么 SQL。
  • 每个事务持有哪些锁、正在等待什么锁。
  • 哪个事务最终被选为牺牲者并回滚。
    注意:该命令主要展示最近一次被记录的死锁,不代表全部历史死锁。

第二步:定位具体 SQL 和锁冲突

不要只看表名,还要看:

  • index PRIMARY 或其他二级索引。
  • lock_mode、locks rec but not gap 等锁信息。
  • RECORD LOCKS 对应的索引记录。
  • HOLDS THE LOCK(S) 和 WAITING FOR THIS LOCK TO BE GRANTED。
    判断是记录锁、间隙锁、Next-Key Lock,还是不同索引之间的锁冲突。

第三步:反查业务代码调用链

从 SQL 找到对应的 Mapper、Service、事务入口,梳理完整执行顺序:

  • 哪些方法开启了事务?
  • 事务中执行了哪些 SQL?
  • 是否涉及多个表、多个索引?
  • 是否存在循环更新、批量处理?
  • 是否调用了远程接口,导致事务持续时间过长?
    特别注意:死锁 SQL 不一定是根因,往往是多个事务执行顺序交叉造成的。

第四步:分析死锁形成原因

重点检查以下问题:

  • 加锁顺序不一致:事务 A 先锁表记录 X,再锁 Y;事务 B 先锁 Y,再锁 X。
  • 事务过大:一次事务修改大量记录,持锁时间长、锁冲突概率高。
  • 索引不合理:查询没有合适索引,扫描和加锁的记录范围扩大。
  • 范围查询冲突:在特定隔离级别下,范围扫描可能涉及间隙锁或 Next-Key Lock。
  • 索引与业务操作交叉:更新二级索引字段、唯一键冲突检查等引发额外锁竞争。
  • 并发访问热点:多个事务同时更新同一批数据。

第五步:制定治理方案

根据根因选择措施,而不是一律加锁:

  1. 统一多条 SQL、多个资源的加锁顺序。
  2. 补充合适的索引,缩小扫描和锁定范围。
  3. 拆分大事务,缩短事务持锁时间。
  4. 避免事务中执行耗时的远程调用、文件处理等操作。
  5. 对批量更新按统一顺序处理记录。
  6. 对高频热点业务考虑串行化、队列化或乐观锁等方案。

第六步:验证与防止复发

  • 使用并发测试复现原有冲突场景。
  • 检查死锁频率是否下降。
  • 观察 SQL 耗时、锁等待时间和事务持续时间。
  • 检查应用是否正确处理死锁异常并重试。
    MySQL 检测到死锁后,通常会回滚其中一个事务。应用需要对可重试的事务进行有限次数重试,并确保重试不会导致重复扣款、重复写入等业务问题。
posted @ 2026-10-20 13:04  程序小小猿  阅读(2)  评论(0)    收藏  举报