← 返回文章

MySQL 锁

发布于 2026/10/17

1. 完整回答

MySQL 的锁主要用于控制并发事务对数据的访问,避免多个事务同时修改数据造成冲突,从而保证数据一致性和事务隔离。

以常用的 InnoDB 存储引擎为例,锁可以从两个角度理解。

首先是锁的粒度,主要包括表级锁和行级锁。表级锁影响整张表,并发能力相对较低;行级锁只锁定相关的索引记录,并发能力更高。

其次是锁的模式,主要分为共享锁和排他锁。共享锁允许多个事务同时读取受保护的数据,排他锁用于控制独占修改,两者存在互斥关系。

InnoDB 的行锁主要通过索引实现,具体又包括记录锁、间隙锁和临键锁。

记录锁锁定已有的索引记录;间隙锁限制其他事务向索引间隙插入数据;临键锁结合了前两者,可以同时保护记录和索引范围,主要用于解决加锁读场景中的幻读问题。

在 MySQL 默认的可重复读隔离级别下,普通 SELECT 通常通过 MVCC 进行快照读,不需要加行锁;UPDATE、DELETE 和 SELECT ... FOR UPDATE 等操作需要根据实际访问的索引范围加锁。

实际使用中,索引和事务执行时间对锁的影响比较大。如果 SQL 没有使用合适的索引,可能扫描并锁住大量记录;如果事务长时间不提交,也容易造成锁等待甚至死锁。

所以在库存扣减、订单状态更新等并发场景中,通常会结合合理索引、短事务和行级锁保证数据正确性,同时尽量缩小锁范围,提高并发性能。

2. 核心原理与关键概念

2.1 InnoDB 的加锁流程

InnoDB 加锁主要围绕索引、隔离级别和事务展开。

步骤 A:确定访问范围

MySQL 根据 SQL 条件和执行计划选择索引,确定需要扫描的记录或范围。

步骤 B:确定锁类型

InnoDB 根据操作类型、索引条件和事务隔离级别决定锁:

  • 普通 SELECT:在读已提交和可重复读级别下,通常使用 MVCC 快照读。

  • SELECT ... FOR SHARE:对相关记录加共享锁。

  • SELECT ... FOR UPDATE、UPDATE、DELETE:通常需要排他锁。

  • 范围加锁:在可重复读级别下,可能涉及间隙锁和临键锁。

步骤 C:获取锁并处理冲突

需要行锁时,事务会先获取对应的表级意向锁,再获取相关索引记录上的锁。

如果其他事务已经持有不兼容的锁,当前事务通常需要等待;出现死锁时,InnoDB 可以主动检测并回滚其中一个事务。

步骤 D:释放锁

事务提交或回滚后,释放持有的事务级数据锁,等待的事务才能继续获取相关锁。

整体流程:

确定索引访问范围 → 判断锁类型 → 获取意向锁和数据锁 → 处理锁冲突 → 提交或回滚释放锁

2.2 关键锁类型

锁类型 核心作用 典型场景
表级锁 控制整张表的并发访问 显式表锁操作
记录锁(Record Lock) 锁定已有的索引记录 主键等值更新
间隙锁(Gap Lock) 阻止其他事务向指定索引间隙插入记录 范围加锁
临键锁(Next-Key Lock) 记录锁与前方间隙锁的组合 可重复读下的范围查询加锁
意向锁(IS、IX) 在表级标识事务准备获取的行锁类型 行级加锁前的协调
插入意向锁 表示事务准备向索引间隙插入记录 并发 INSERT
元数据锁(MDL) 保护表结构,协调数据操作与结构变更 SELECT、UPDATE、ALTER TABLE

此外,InnoDB 还存在用于自增值分配的 AUTO-INC 锁,其使用方式受到 innodb_autoinc_lock_mode 影响。

2.3 共享锁和排他锁的兼容关系

当前持有的锁 请求共享锁 S 请求排他锁 X
共享锁 S 兼容 冲突
排他锁 X 冲突 冲突

共享锁主要用于保护读取期间的数据不被其他事务修改;排他锁主要用于保证同一数据不会被多个事务同时修改。

这里的读锁指显式加锁读取。普通快照读通常可以直接读取历史版本,不受这些行锁的常规互斥限制。

3. 高频面试追问

3.1 普通 SELECT 会加锁吗?快照读和当前读有什么区别?

在 InnoDB 的读已提交和可重复读隔离级别下,普通 SELECT 通常不加行锁,而是使用 MVCC 进行快照读。

快照读通过 Read View 判断哪些数据版本可见,再结合 Undo Log 获取符合条件的历史版本,所以读取时通常不会阻塞其他事务修改数据。

当前读则需要读取当前可用的数据版本,并通过加锁协调并发修改。

常见的当前读包括:

  • SELECT ... FOR UPDATE

  • SELECT ... FOR SHARE

  • UPDATE

  • DELETE

例如,一个事务正在修改用户余额但还没有提交,另一个事务执行普通 SELECT,通常仍然可以读取符合快照规则的旧余额;如果执行 SELECT ... FOR UPDATE,则可能等待前一个事务释放排他锁。

另外,普通 SELECT 虽然通常不加行锁,但仍会获取必要的元数据锁。

3.2 SELECT FOR UPDATE 和 FOR SHARE 有什么区别?

核心区别是锁模式不同,FOR UPDATE 使用排他锁,FOR SHARE 使用共享锁。

FOR SHARE 允许其他事务对相同记录继续获取共享锁,但会阻止其他事务获取冲突的排他锁。

FOR UPDATE 则会对相关记录获取排他锁,阻止其他事务执行冲突的加锁读取或修改。

例如,业务需要先读取库存,再根据库存判断是否更新,就可以在事务中使用 SELECT ... FOR UPDATE 锁定对应记录。

这样可以避免读取库存后、真正修改前,其他事务抢先修改同一条库存记录。

需要注意,显式事务中的加锁读取才具有持续保护后续操作的意义。如果使用自动提交模式,语句结束后事务就会提交,锁也随之释放。

3.3 为什么说 InnoDB 的行锁依赖索引?索引失效会锁全表吗?

InnoDB 的记录锁实际上锁定的是索引记录,SQL 使用什么索引、扫描多大范围,会直接影响加锁范围。

例如:

UPDATE user SET age = 20 WHERE id = 10;

如果 id 是主键,并且查询通过主键准确定位记录,通常只需要锁定对应的索引记录。

如果使用没有合适索引的字段作为条件,MySQL 可能需要扫描大量记录。扫描过程中,InnoDB 可能对大量索引记录及相关间隙加锁。

因此,即使 SQL 最终只更新一条数据,也可能影响其他很多事务。

这里有两个关键点:

第一,索引不合适可能造成大范围行锁,并不意味着 InnoDB 自动把行锁升级成表锁。

第二,锁范围取决于实际执行计划,不能只根据 SQL 中是否出现索引字段判断。

实际排查时,可以通过 EXPLAIN 查看索引访问方式,再结合锁监控确认加锁情况。

3.4 记录锁、间隙锁和临键锁有什么区别?为什么需要间隙锁?

记录锁保护已有记录,间隙锁限制新记录插入,临键锁同时保护记录和前面的间隙。

例如,某个索引已有三个值:

10、20、30

如果对索引值 20 加记录锁,主要保护值为 20 的索引记录。

如果对区间 (10,20) 加间隙锁,其他事务就不能在这个间隙插入 15,但仍可以对已有记录执行不冲突的操作。

如果对 20 加临键锁,锁定范围通常表示为:

(10,20]

这样既保护了记录 20,也限制在 10 和 20 之间插入新记录。

间隙锁存在的主要原因是:只锁住已有记录,无法阻止其他事务插入符合查询条件的新记录。

在可重复读隔离级别下,InnoDB 使用临键锁等机制保护加锁读取的索引范围,减少幻读问题。

继续追问:间隙锁之间会互相冲突吗?

一般不会。普通间隙锁主要用于阻止插入,同一个间隙上的共享间隙锁和排他间隙锁可以共存。

但是插入意向锁可能与已有间隙锁冲突,导致插入事务等待。

3.5 唯一索引和普通索引的加锁方式有什么区别?

在可重复读隔离级别下,唯一索引等值查询如果准确命中已有记录,通常只需要记录锁;普通索引查询或范围查询通常涉及更大的锁定范围。

原因是唯一索引能够保证匹配记录最多只有一条,因此准确命中时,只需保护对应记录。

普通索引允许重复值,等值查询可能匹配多条记录,还需要考虑这些记录之间及附近的索引间隙。

例如:

  • 主键 id = 10 且记录存在:通常只需要记录锁。

  • 普通索引 age = 20:可能涉及多条记录及相关间隙。

  • 范围查询 id > 10:在可重复读级别下,通常需要对扫描范围使用临键锁或间隙锁。

继续追问:唯一索引等值查询一定不会加间隙锁吗?

不一定。如果查询的唯一值不存在,在可重复读级别下进行加锁读取,仍可能锁住对应的索引间隙,防止其他事务插入该值。

所以需要区分记录是否存在、索引是否唯一,以及实际执行的查询条件。

3.6 可重复读和读已提交的加锁有什么区别?如何解决幻读?

主要区别是快照生成规则和间隙锁的使用方式不同。

在**读已提交(READ COMMITTED)**级别下,每次一致性读取通常都会建立新的 Read View,因此同一个事务多次普通查询,可能看到其他事务新提交的数据。

在加锁方面,读已提交对一般的索引搜索和扫描通常不使用间隙锁,但外键检查和重复键检查仍可能使用。

在**可重复读(REPEATABLE READ)**级别下,同一事务中的普通一致性读取通常使用首次一致性读建立的快照,因此多次执行相同查询,通常可以得到一致的结果。

对于当前读,InnoDB 可以通过临键锁和间隙锁限制其他事务向已锁定范围插入记录。

因此,可重复读主要通过两种机制处理幻读:

  • 快照读: MVCC 保持读取视图稳定。

  • 当前读: 通过范围加锁限制新记录插入。

需要注意,快照读和当前读的可见性规则不同。两者混合使用时,当前读可能看到快照读看不到的新提交记录,因此不能认为可重复读下所有读取都共享完全相同的结果。

3.7 意向锁和插入意向锁有什么区别?为什么需要意向锁?

两者的作用和粒度不同。意向锁属于表级锁,用于协调表锁和行锁;插入意向锁属于特殊的间隙锁,用于协调并发插入。

InnoDB 的意向锁包括:

  • IS: 表示事务准备对表中的某些记录加共享锁。

  • IX: 表示事务准备对表中的某些记录加排他锁。

例如,事务准备通过 SELECT ... FOR UPDATE 锁定某条记录,会先获取表级 IX 锁。

这样其他事务如果需要获取整张表的排他锁,就能通过意向锁判断存在冲突,不需要逐条检查所有行锁。

不同事务之间的 IX 锁通常是兼容的,所以多个事务仍然可以修改同一张表中的不同记录。

插入意向锁则用于插入过程。

例如,索引中存在 10 和 20,两个事务分别准备插入 12 和 15。只要没有其他锁冲突,这两个插入通常可以并发执行,不需要因为处于同一个间隙就相互等待。

3.8 MySQL 为什么会发生死锁?如何解决?

死锁通常是多个事务持有对方需要的锁,形成循环等待,导致事务都无法继续执行。

例如有两条记录 A 和 B:

事务 1:

锁定 A → 尝试锁定 B → 等待事务 2

事务 2:

锁定 B → 尝试锁定 A → 等待事务 1

两个事务相互等待,就形成了死锁。

InnoDB 默认启用死锁检测,发现死锁后,会选择其中一个事务进行回滚,让其他事务继续执行。

实际处理主要从几个方面入手:

  1. 让多个事务按照相同顺序访问和更新数据,减少循环等待。

  2. 尽量缩短事务执行时间,避免事务中包含耗时操作。

  3. 为查询和更新条件建立合理索引,减少扫描和加锁范围。

  4. 在应用层捕获死锁异常,对整个失败事务进行有限次数重试。

继续追问:死锁和锁等待超时有什么区别?

死锁是事务之间形成循环等待;锁等待超时则是某个事务等待锁的时间超过限制,即使没有循环等待也可能发生。

默认情况下,InnoDB 检测到死锁会回滚被选中的整个事务;发生行锁等待超时时,通常只回滚当前失败语句,事务可能仍然处于活动状态。

因此,处理锁等待超时时,还需要明确事务是否继续执行或主动回滚,避免遗留长事务。

3.9 为什么 ALTER TABLE 有时候会一直阻塞?

通常与 MySQL 的元数据锁 MDL 有关。

元数据锁用于保护表结构,避免一个事务正在使用某张表时,另一个事务直接修改表结构,造成数据和元数据不一致。

例如,事务 A 执行了:

BEGIN; SELECT * FROM user;

虽然普通查询通常不会加行锁,但事务仍会持有这张表的元数据锁,直到事务结束。

如果此时事务 B 执行:

ALTER TABLE user ...

修改表结构需要获取相应的元数据锁。如果与事务 A 已持有的锁冲突,就必须等待事务 A 提交或回滚。

而等待中的 DDL 还可能导致后续访问该表的 SQL 排队,放大阻塞影响。

所以生产环境中执行 DDL 时,需要关注是否存在长事务,以及 DDL 的锁等待情况。

3.10 实际项目中如何使用锁防止库存超卖?如何排查锁等待?

库存扣减可以直接使用带条件的原子更新,让 InnoDB 在数据库层面保证并发修改的正确性。

例如:

UPDATE product
SET stock = stock - 1
WHERE id = 100 AND stock > 0;

这条 SQL 同时完成库存判断和扣减。

如果多个事务并发更新相同的商品记录,InnoDB 会通过排他锁协调修改,后续事务需要等待冲突锁释放后再判断更新条件。

业务通过受影响行数判断扣减是否成功,就可以避免并发扣减导致库存变成负数。

这种方案适合单条记录的原子扣减。如果还需要同时创建订单、更新支付状态等,就要通过事务保证多个操作的一致性。

同时,需要确保商品 ID 有合适的索引,并尽量缩短事务时间。对于极高并发的热点库存,还可能需要结合限流、队列等方式降低数据库竞争。

继续追问:线上出现大量锁等待,如何排查?

通常按照下面的流程处理:

定位阻塞事务 → 查看等待关系 → 分析执行 SQL → 检查索引和事务持续时间 → 优化或处理阻塞源

具体可以使用:

  • SHOW ENGINE INNODB STATUS:查看最近一次死锁信息。

  • information_schema.INNODB_TRX:查看活动事务及其状态。

  • performance_schema.data_locks:查看已持有和正在请求的数据锁。

  • performance_schema.data_lock_waits:查看锁等待与阻塞关系。

  • performance_schema.metadata_locks:排查元数据锁等待。

  • EXPLAIN:分析 SQL 是否扫描了过多记录。

如果锁等待集中在少量热点行,就需要考虑减少事务持锁时间、优化更新方式,或者在业务层进行削峰处理。

4. 易混淆概念对比

4.1 表级锁与行级锁

对比项 表级锁 行级锁
锁定粒度 整张表 索引记录及相关范围
并发能力 通常较低 通常较高
管理开销 相对较小 相对较大
典型场景 显式锁表 InnoDB 数据更新

4.2 记录锁、间隙锁与临键锁

对比项 记录锁 间隙锁 临键锁
锁定对象 已有索引记录 索引记录之间的间隙 索引记录及其前方间隙
阻止修改已有记录 根据共享或排他模式决定 不直接阻止 根据共享或排他模式决定
阻止间隙内插入 通常不阻止 阻止 阻止
主要作用 保护已有记录 限制插入 同时保护记录和范围

4.3 意向锁与插入意向锁

对比项 意向锁 插入意向锁
锁粒度 表级 索引间隙
主要作用 协调表锁与行锁 协调并发插入
常见类型 IS、IX 插入意向间隙锁
核心特点 不同事务的 IX 锁通常兼容 不同位置的插入通常可以并发

4.4 快照读与当前读

对比项 快照读 当前读
读取版本 符合 Read View 的可见版本 当前可用版本
实现方式 MVCC 加锁协调并发
行锁 通常不加 根据操作获取相关行锁
典型 SQL 普通 SELECT SELECT ... FOR UPDATE、UPDATE

4.5 读已提交与可重复读

对比项 读已提交(RC) 可重复读(RR)
Read View 每次一致性读通常重新创建 同一事务通常复用首次一致性读的快照
普通 SELECT 可能看到新提交的数据 通常保持读取结果稳定
间隙锁 一般搜索和扫描中基本不使用,保留必要例外 范围加锁时常使用
并发特点 范围插入限制相对较少 范围加锁可能产生更多竞争

4.6 死锁与锁等待超时

对比项 死锁 锁等待超时
发生原因 多个事务循环等待 等待锁超过限制时间
是否必须循环等待 是 否
默认处理 检测后回滚选中的整个事务 回滚当前失败语句
主要优化方向 统一加锁顺序、缩短事务 排查阻塞源、优化索引和事务

5. 总结

MySQL 的锁主要用于控制并发事务对数据的访问,保证数据一致性和事务隔离。InnoDB 通过基于索引的行级锁实现细粒度并发控制,主要包括记录锁、间隙锁和临键锁,并通过意向锁协调表锁与行锁。在默认可重复读隔离级别下,普通查询通常通过 MVCC 实现快照读,加锁读取则通过记录锁和范围锁控制并发修改。实际应用中,锁的范围与索引、隔离级别和执行计划密切相关,因此需要合理设计索引、缩短事务时间,并处理可能出现的锁等待和死锁问题。