MySQL 的行级锁到底是怎么加的?

2026-05-02阅读 0热度 0
行级锁 MySQL

InnoDB锁机制的核心原理

在剖析具体加锁行为前,必须厘清两个底层逻辑:数据库锁定的目标是索引,而加锁的基本单元是next-key lock。这个单元由记录锁(锁定索引项本身)和间隙锁(锁定索引项之间的区间)组合而成。两者在区间定义上有本质区别:next-key lock采用左开右闭区间,而间隙锁采用左开右开区间。

但锁的形态并非固定。当仅凭记录锁或间隙锁就足以隔离事务、防止幻读时,next-key lock会“退化”为更精确的锁类型。掌握这些退化条件,是精准控制InnoDB并发行为的关键。

为便于后续推演,我们定义一张示例表:id列为主键(唯一索引),age列为普通二级索引(非唯一),name列为无索引列。初始数据状态如下:

一、唯一索引等值查询的锁行为

基于唯一索引进行等值查询时,加锁策略会根据目标记录是否存在而动态调整。

核心规则可概括为:若目标记录存在,引擎在索引树精确定位后,会将next-key lock退化为仅锁定该行的记录锁。若目标记录不存在,引擎会定位到大于查询值的第一条记录,并将其上的next-key lock退化为间隙锁

1. 记录存在时的锁退化

假设事务A执行以下查询,且表中存在id=1的记录。

select * from user where id = 1 for update;

此时,事务A将在id=1的主键索引记录上施加X型(排他)记录锁。

任何其他事务对id=1记录的更新或删除操作都将被阻塞。退化的逻辑在于:在此场景下,记录锁已足够保证隔离性。由于唯一索引约束,其他事务无法插入新的id=1记录,也无法修改现有记录,从而确保了查询结果集的一致性。

2. 记录不存在时的锁退化

若事务A查询一条不存在的记录:

select * from user where id = 2 for update;

事务A会在id=5的主键索引记录上施加一个间隙锁,锁定区间(1, 5)。

这将阻塞任何插入id值为2、3、4的新事务。选择间隙锁而非next-key lock是出于性能考量:若锁定(1,5]区间,id=5的记录将被阻塞,影响其更新。但无论id=5是否变更,事务A的查询结果始终为空。因此,仅防止插入的间隙锁是实现隔离的最优解。

二、唯一索引范围查询的锁策略

范围查询的加锁逻辑更为精细。基本原则是:扫描过程中遇到的每一条索引记录都会先附加next-key lock,随后根据特定条件判断是否退化。

具体规则可归纳为:

  1. 对于大于等于查询,由于包含等值条件,若等值记录存在,则该记录上的next-key lock退化为记录锁。
  2. 对于小于小于等于查询,退化行为取决于条件值对应记录的存在性:
    • 若该记录不存在,扫描终止时,终止记录上的next-key lock退化为间隙锁。
    • 若该记录存在,对于“小于”查询,终止记录的next-key lock退化为间隙锁;对于“小于等于”查询,则保持next-key lock。

1. 大于(等于)范围查询分析

(1) 大于查询的锁范围

假设事务A执行:

select * from user where id > 15 for update;

事务A将在主键索引上施加两个next-key lock:

  • 在id=20记录上,施加(15, 20]的next-key lock。该锁防止id=20被修改,并阻止id在16至19区间的新记录插入。
  • 在虚拟的“上确界”记录上,施加(20, +∞]的next-key lock,防止插入任何id大于20的记录。

此处无锁退化。要保证“id>15”结果集的稳定性,必须同时防止范围内记录被修改和新记录插入,next-key lock恰好满足此双重需求。

(2) 大于等于查询的锁退化

假设事务A执行:

select * from user where id >= 15 for update;

事务A将施加三把锁:

  • 在id=15记录上,因等值查询命中,next-key lock退化为记录锁,仅锁定该行。
  • 在id=20记录上,施加(15, 20]的next-key lock。
  • 在“上确界”记录上,施加(20, +∞]的next-key lock。

可见,id=15上的锁发生了退化,其逻辑与前述“唯一索引等值查询”完全一致。

2. 小于(等于)范围查询分析

(1) 条件值记录不存在时的锁退化

查询id < 6,且id=6记录不存在。

select * from user where id < 6 for update;

加锁情况如下:

  • 在id=1上加(-∞, 1]的next-key lock。
  • 在id=5上加(1, 5]的next-key lock。
  • 在id=10上加(5, 10)的间隙锁。注意,扫描到第一条不满足条件的记录(id=10)时,next-key lock退化了。

这印证了前述规则:当条件值记录不存在时,终止扫描的记录其next-key lock退化为间隙锁。

(2) 小于等于查询且记录存在时的锁保持

查询id <= 5,且id=5存在。

select * from user where id <= 5 for update;

加锁情况如下:

  • 在id=1上加(-∞, 1]的next-key lock。
  • 在id=5上加(1, 5]的next-key lock。

此处id=5上的锁未退化。原因在于:对于“id<=5”查询,id=5本身属于结果集。若退化为间隙锁,其他事务可删除id=5,导致前后查询结果集不一致,引发幻读。因此必须用next-key lock锁定。

(3) 小于查询且记录存在时的锁退化

查询id < 10,且id=10存在(但不满足条件)。

select * from user where id < 10 for update;

加锁情况与第(1)种情况一致:

  • 在id=1上加(-∞, 1]的next-key lock。
  • 在id=5上加(1, 5]的next-key lock。
  • 在id=10上加(5, 10)的间隙锁

由于id=10是第一条不满足条件的记录,且查询为“小于”条件,其next-key lock退化为间隙锁。

三、非唯一索引等值查询的锁机制

非唯一索引等值查询涉及主键与二级索引的双重锁定,逻辑更为复杂。加锁时会对两者同时操作,但对主键索引仅锁定满足条件的记录。

核心规则同样分为两种情况:

  • 记录存在:由于索引非唯一,查询过程实为扫描,直至找到第一条不满足条件的记录。扫描过程中,对所有命中的二级索引记录加next-key lock,并将第一条不满足条件记录的next-key lock退化为间隙锁。同时,为所有满足条件记录的主键索引加记录锁。
  • 记录不存在:扫描到第一条不满足条件的记录后,将其next-key lock退化为间隙锁。由于无命中记录,故不对任何主键索引加锁

1. 记录不存在的间隙锁

假设事务A查询age = 25(表中无此记录)。

select * from user where age = 25 for update;

引擎将扫描至第一条age > 25的记录,即age=39。随后,在该记录的二级索引上,next-key lock退化为间隙锁,锁定区间(22, 39)。

这将阻塞插入age值在23至38之间的新记录。该间隙锁的核心目的是防止幻读——避免在事务期间插入age=25的记录。

需注意一个关键细节:插入age=22或age=39的记录是否被阻塞,取决于插入记录的主键id值。因为二级索引的定位依据是“索引值+主键值”。插入操作能否成功,取决于其目标位置的下一条记录是否被间隙锁锁定。

  • 插入age=22:若插入(id=3, age=22),其下一条记录为(id=10, age=22),该记录未被锁,可插入。若插入(id=12, age=22),其下一条记录为(id=20, age=39),该记录被间隙锁覆盖,插入被阻塞。
  • 插入age=39:若插入(id=3, age=39),其下一条记录为(id=20, age=39),被锁,插入阻塞。若插入(id=21, age=39),其下一条记录不存在,无锁,可插入。

因此,间隙锁锁定的是一个逻辑区间,但具体插入操作是否被阻塞,取决于其试图插入的精确“位置”。

2. 记录存在的扫描加锁

假设事务A查询age = 22(表中存在多条记录)。

select * from user where age = 22 for update;

加锁过程为顺序扫描:

  1. 定位第一条age=22的记录(id=10),对其二级索引加(21, 22]的next-key lock,并对主键索引id=10加记录锁。
  2. 继续扫描,找到下一条age=39的记录(首个不满足条件记录),将其二级索引上的next-key lock退化为间隙锁(22, 39)。
  3. 停止扫描。

最终加锁状态如下:

  • 主键索引:仅对id=10施加记录锁。
  • 二级索引
    • 在age=22记录上,施加(21, 22]的next-key lock。
    • 在age=39记录上,施加(22, 39)的间隙锁。

这些锁协同工作,有效防止了幻读:记录锁保护了已存在的age=22记录不被修改;二级索引锁则阻止了在查询区间内插入新的age=22记录。

同样,插入age=21或22的记录是否成功,也取决于其主键id值,原理同上。

四、非唯一索引范围查询的锁策略

非唯一索引的范围查询规则反而简化:next-key lock不发生退化。即对扫描到的每一条二级索引记录,均施加next-key lock。

以以下查询为例:

select * from user where age >= 22  for update;

加锁情况如下:

  • 主键索引:对id=10和id=20这两条满足条件的记录施加记录锁。
  • 二级索引
    • age=22: 施加(21, 22]的next-key lock。
    • age=39: 施加(22, 39]的next-key lock。
    • 上确界记录: 施加(39, +∞]的next-key lock。

这里存在一个关键差异:为何age=22这个等值命中的记录,其next-key lock没有像唯一索引那样退化为记录锁?根本原因在于age字段的非唯一性。若仅加记录锁,只能防止该特定记录被修改,无法阻止其他事务插入全新的age=22记录。这将导致前后两次范围查询的结果集可能不同,从而产生幻读。因此,必须使用next-key lock来同时封锁插入操作。

五、无索引查询的锁风险

最后讨论最需警惕的场景。若锁定读查询(如SELECT ... FOR UPDATE)未使用索引列作为条件,或未走索引扫描而引发全表扫描,后果极为严重:每一条记录的索引上都会被附加next-key lock。这实质上锁定了整个表。

需特别注意,不仅是SELECT ... FOR UPDATEUPDATEDELETE语句若条件无法利用索引,同样会触发全表扫描及全表加锁。

因此,在生产环境中执行任何带锁性质的语句前,必须审查其执行计划,确保查询利用了合适的索引。全表扫描引发的锁表问题,对数据库并发性能的冲击是灾难性的。即便语句包含索引,优化器也可能选择全表扫描,但若无索引,则全表扫描必然发生,这一点必须铭记于心。

免责声明

本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。

相关阅读

更多
欢迎回来 登录或注册后,可保存提示词和历史记录
登录后可同步收藏、历史记录和常用模板
注册即表示同意服务条款与隐私政策