Easton Notes
八股MySQL 面试题

MySQL 事务与锁

MySQL 事务 ACID、并发异常、隔离级别、MVCC、幻读与 InnoDB 锁机制高频面试题。

什么是事务?ACID 分别如何实现?

回答重点

事务把一组操作作为一个逻辑单元执行。ACID 包含:

  • 原子性(Atomicity):事务中的操作要么全部成功,要么全部回滚。InnoDB 主要利用 undo log 保存回滚所需的信息,并由事务管理机制保证提交或回滚。
  • 一致性(Consistency):事务前后都满足数据完整性约束和业务规则。它是事务最终要达到的结果,需要原子性、隔离性、持久性、数据库约束和正确的业务逻辑共同保证。
  • 隔离性(Isolation):并发事务之间的中间状态按隔离级别受到控制。InnoDB 主要通过 MVCC 和锁实现。
  • 持久性(Durability):事务成功提交后,即使进程或机器异常重启,结果也能恢复。InnoDB 主要依靠 redo log、WAL 和崩溃恢复实现;实际持久化强度还受刷盘参数和底层存储能力影响。

不要把“一致性”简单归因于某一种日志。数据库可以保证唯一键、外键等约束,但账户总额之类的业务不变量仍要由正确的事务边界和业务逻辑维护。

并发事务可能产生哪些异常?

回答重点

  • 脏读:读到了其他事务尚未提交的修改;如果对方回滚,读到的数据就从未真正生效。
  • 不可重复读:同一事务前后读取同一行,结果不同,通常是其他事务在两次读取之间提交了更新或删除。
  • 幻读:同一事务按相同范围条件查询,前后得到的记录集合不同,通常是其他事务提交了插入或删除。
  • 丢失更新:两个事务基于同一旧值计算并写回,后一次写入覆盖前一次结果。

脏读、不可重复读和幻读描述的是读取现象;丢失更新往往是业务上的“读、计算、写”竞争,不一定仅靠提高隔离级别就能消除。常见解决方式是锁定读、原子更新或带版本号的乐观锁:

UPDATE account
SET balance = balance - 100
WHERE id = 1 AND balance >= 100;

MySQL 有哪些事务隔离级别?

回答重点

SQL 标准定义了四个隔离级别,隔离程度从低到高为:

隔离级别脏读不可重复读幻读
读未提交(READ UNCOMMITTED)可能可能可能
读已提交(READ COMMITTED)避免可能可能
可重复读(REPEATABLE READ)避免避免标准模型下仍可能
串行化(SERIALIZABLE)避免避免避免

InnoDB 对可重复读做了额外增强:普通快照读通过 MVCC 复用 Read View,锁定读和写操作通过 next-key lock 保护扫描范围,因此在常见访问模式下也能避免幻读。

多数 MySQL 5.7、8.0 安装的默认隔离级别是可重复读,但它可以在全局、会话或事务级别修改,不能把默认值当成不可变规则。应以当前实例的配置为准:

SELECT @@GLOBAL.transaction_isolation, @@SESSION.transaction_isolation;

快照读和当前读有什么区别?

回答重点

  • 快照读,也叫一致性非锁定读:普通 SELECT 通常根据 Read View 读取可见的历史版本,不给读取到的记录加行锁。
  • 当前读,也叫锁定读:读取符合条件的最新可见记录,并对相关索引记录或范围加锁。SELECT ... FOR UPDATE、SELECT ... FOR SHARE、UPDATE 和 DELETE 都需要当前读。
START TRANSACTION;

SELECT * FROM orders WHERE id = 10;
SELECT * FROM orders WHERE id = 10 FOR UPDATE;

COMMIT;

SELECT ... FOR SHARE 是较新版本的写法,旧版本常见 LOCK IN SHARE MODE。锁的具体范围由隔离级别、索引、查询条件和执行计划共同决定,而不是由 SQL 文本单独决定。

在串行化隔离级别下,普通 SELECT 的行为也可能转为加共享锁的读取;具体表现还与是否处于显式事务、自动提交设置和版本有关。

Read View 和 MVCC 是如何工作的?

回答重点

InnoDB 聚簇索引记录包含两个与 MVCC 密切相关的隐藏字段:

  • DB_TRX_ID:最后一次插入或修改该记录版本的事务 ID。
  • DB_ROLL_PTR:指向 undo log 中更旧版本的回滚指针。

多次更新会通过 DB_ROLL_PTR 形成版本链。Read View 保存创建快照时的事务状态,核心信息可概括为:

  • m_ids:创建 Read View 时仍活跃的事务 ID 集合。
  • min_trx_id:活跃事务 ID 的最小值。
  • max_trx_id:创建 Read View 时下一个将被分配的事务 ID,不是 m_ids 的最大值。
  • creator_trx_id:创建该 Read View 的事务 ID。

读取一个版本时,先判断它是否由当前事务产生;自己的修改对自己可见。对于其他事务生成的版本:

  1. trx_id < min_trx_id,说明生成版本的事务在快照创建前已提交,版本可见。
  2. trx_id >= max_trx_id,说明事务在快照创建后才启动,版本不可见。
  3. min_trx_id <= trx_id < max_trx_id 时,如果 trx_id 在 m_ids 中,说明当时尚未提交,版本不可见;否则版本可见。
  4. 当前版本不可见时,沿 DB_ROLL_PTR 查找更旧版本,直到找到可见版本或确认记录不存在。

MVCC 主要优化一致性读,让读操作不必和写操作互相阻塞。它不能替代写锁,也不负责解决所有业务竞争。

读已提交和可重复读的 Read View 有什么区别?

回答重点

  • 读已提交:每次一致性读语句开始时创建新的 Read View,因此能看到其他事务在上一条语句之后提交的数据。
  • 可重复读:事务中的第一次一致性读创建 Read View,后续一致性读通常复用它,因此多次读取结果保持一致。

仅执行 BEGIN 或 START TRANSACTION 通常不会立即创建 Read View。需要在事务开始时建立快照,可以使用:

START TRANSACTION WITH CONSISTENT SNAPSHOT;

当前读不使用旧 Read View 返回历史版本,而是读取最新可见版本并加锁,所以同一事务混用快照读和当前读时,可能观察到不同的数据集合。

InnoDB 的可重复读是否彻底解决了幻读?

回答重点

不能脱离读取方式直接回答“是”或“否”:

  • 对连续的普通 SELECT,可重复读复用 Read View,其他事务后来提交的新记录不可见,因此避免了快照读中的幻读。
  • 对锁定读和 UPDATE、DELETE,InnoDB 通常使用记录锁、间隙锁或 next-key lock 保护扫描范围,阻止其他事务在范围内插入,从而避免当前读中的幻读。
  • 如果先做快照读,其他事务随后插入并提交,再由本事务更新该记录,本事务之后可能看到自己修改过的最新版本。此时前后结果集合发生变化,说明不能把“可重复读绝不会出现幻读”当成无条件结论。

此外,读已提交下通常不会为普通范围扫描保留防幻读的间隙锁;串行化则通过更强的锁定约束并发。

如何用锁定读避免幻读?

回答重点

事务需要先锁定一个范围,再基于该范围执行后续判断和写入:

START TRANSACTION;

SELECT *
FROM account
WHERE balance >= 1000000
FOR UPDATE;

-- 根据查询结果继续执行业务写入
COMMIT;

在 InnoDB 可重复读下,如果执行计划使用合适的索引,范围锁定读通常会建立 next-key lock,阻止其他事务向已扫描范围插入记录。

注意以下边界:

  • 必须把检查和后续操作放在同一个事务中。
  • 索引决定扫描范围。缺少合适索引可能导致锁范围大幅扩大。
  • 精确命中唯一索引时,可能只需要记录锁;唯一键不存在时则可能锁住对应间隙。
  • 读已提交、可重复读和串行化的加锁行为不同,应结合当前版本与隔离级别验证。

一条 UPDATE 语句是原子的吗?

回答重点

对 InnoDB 来说,单条 UPDATE 具有事务原子性。自动提交开启时,这条语句本身构成一个事务;处于显式事务中时,它会随整个事务一起提交或回滚。

InnoDB 在执行过程中通过行锁隔离冲突写入,通过 undo log 保存回滚信息,并通过 redo log 支持崩溃恢复。语句失败时,本条语句已经完成的修改会回滚;事务失败或主动回滚时,则按事务边界撤销。

原子性只覆盖数据库事务内的操作。发送消息、调用第三方接口等外部副作用不会自动与 MySQL 提交保持原子,需要事务消息、Outbox 或其他一致性方案。

长事务有什么危害?

回答重点

  • 长时间持有行锁和 MDL,增加锁等待、死锁和 DDL 阻塞概率。
  • 持续占用旧 Read View,使 purge 不能及时清理仍可能被访问的 undo 版本,导致 undo 空间和历史链增长。
  • 事务修改越多,回滚成本、日志量和崩溃恢复压力越大。
  • 大事务集中写入 binlog,可能造成副本应用延迟。
  • 长时间占用连接和服务端资源,出现故障时影响范围更大。

治理方式包括缩短事务边界、避免在事务内执行网络调用和人工等待、拆分可独立提交的批量操作,并监控事务时长、锁等待与 undo history。拆分前要确认不会破坏业务原子性。

MySQL 中有哪些层级的锁?

回答重点

按影响范围可以概括为:

  • 全局锁:影响整个实例的读写状态,典型命令是 FLUSH TABLES WITH READ LOCK。
  • 表级锁:包括显式表锁、元数据锁(MDL)、InnoDB 意向锁和 AUTO-INC 相关锁机制。
  • 行级锁:InnoDB 在索引记录上实现记录锁、间隙锁、next-key lock 和插入意向锁。

不同存储引擎能力不同。例如 InnoDB 以行级锁和 MVCC 为核心,MyISAM 主要使用表锁。日常所说的“InnoDB 行锁”实际锁的是索引记录或索引区间;如果查询没有合适索引,扫描和锁定范围可能接近全表。

全局锁和表级锁分别有什么作用?

回答重点

全局读锁

FLUSH TABLES WITH READ LOCK;
-- 执行需要全局只读视图的操作
UNLOCK TABLES;

它会让实例进入全局只读状态,写数据和修改表结构等操作会被阻塞,常用于特定备份场景。对于全部使用 InnoDB 的实例,一致性快照备份通常比长时间持有全局锁影响更小,但快照不能自动解决所有非事务表和并发 DDL 问题。

显式表锁

LOCK TABLES 可以给整张表加读锁或写锁,粒度大、并发度低,一般只在特定存储引擎或运维场景使用。

元数据锁(MDL)

访问表时由 MySQL 自动获取。DML 和查询通常持有共享性质的 MDL,DDL 需要排他性质的 MDL,锁一般持续到事务结束。长事务可能阻塞 DDL;排队中的 DDL 还可能让后续请求继续等待。

意向锁

InnoDB 在加行级共享锁或排他锁前,会先在表级加 IS 或 IX。它用于让表锁快速判断表内是否存在冲突的行锁,不是为了阻塞其他正常的行级操作。

AUTO-INC 锁

用于协调自增值分配。具体持有时机和并发行为受 innodb_autoinc_lock_mode、插入类型及 MySQL 版本影响,不应笼统地认为所有自增插入都会锁表到事务结束。

InnoDB 的共享锁和排他锁有什么区别?

回答重点

  • 共享锁(S 锁):允许其他事务继续获取同一记录的 S 锁,但阻止 X 锁。
  • 排他锁(X 锁):阻止其他事务在同一记录上获取 S 锁或 X 锁。

兼容关系如下:

已持有 \ 请求S 锁X 锁
S 锁兼容冲突
X 锁冲突冲突

普通快照读通常不加 S 锁。锁定读、更新和删除取得的锁通常持有到事务提交或回滚,而不只是持有到单条 SQL 结束。

记录锁、间隙锁和 next-key lock 有什么区别?

回答重点

  • 记录锁(Record Lock):锁住某个索引记录,例如唯一索引等值查询命中现有记录时常见。
  • 间隙锁(Gap Lock):锁住两个索引记录之间的开区间,不锁记录本身,主要用于阻止其他事务在该间隙插入。
  • next-key lock:记录锁与其前方间隙锁的组合,通常表示为左开右闭区间,例如 (10, 20]。
  • 插入意向锁(Insert Intention Lock):插入前在目标间隙申请的特殊间隙锁。插入不同位置的事务通常可以并发,但会与覆盖该位置的间隙锁冲突。

间隙锁主要用于 InnoDB 可重复读下的范围保护。在读已提交下,普通搜索和扫描通常关闭间隙锁,但外键检查、重复键检查等场景仍可能使用它,因此不要写成“读已提交绝对没有间隙锁”。

间隙锁的重点是阻止插入,并不完全遵循普通 S/X 记录锁的兼容规则;多个事务可以在同一间隙持有不同形式的 gap lock。

InnoDB 如何确定一条 SQL 的锁范围?

回答重点

锁范围首先由实际执行计划扫描的索引区间决定,再受隔离级别、查询条件、索引唯一性和记录是否存在影响:

  • 唯一索引等值查询命中记录时,通常退化为记录锁。
  • 唯一索引等值查询未命中时,在可重复读下通常需要锁住目标值所在间隙。
  • 非唯一索引等值查询可能锁住所有匹配记录以及相关间隙。
  • 范围查询在可重复读下通常形成一组 next-key lock,边界是否退化要看实际数据和优化规则。
  • 通过二级索引更新时,除了二级索引记录,还可能锁住对应的聚簇索引记录。
  • 没有可用索引时,InnoDB 可能扫描并锁定大量索引记录和区间,效果接近阻塞整张表,但底层仍不等同于主动获取了一个表级 X 锁。

因此,不能只根据 WHERE id < 10 就断言锁一定是 (-∞, 10)。应结合表中现有键值、执行计划、隔离级别和 MySQL 版本分析,必要时在测试环境查询 performance_schema.data_locks 验证。

两个事务同时执行 UPDATE 一定会阻塞吗?

回答重点

不一定,关键看实际锁集合是否冲突:

  • 同时更新同一主键记录时,第一个事务持有 X 记录锁,第二个通常进入锁等待。
  • 使用同一索引更新互不重叠的记录或范围时,通常可以并发。
  • 即使 WHERE 条件看似不重叠,也可能因二级索引维护、外键检查、唯一键检查或范围间隙重叠而阻塞。
  • 条件列没有合适索引时,执行器可能扫描并锁住大量记录,使本来不相关的更新互相等待。

锁最终在事务结束时释放。工程上应让事务尽快提交,并为更新条件建立能缩小扫描范围的索引。

如何排查和处理死锁?

回答重点

死锁是多个事务形成循环等待。InnoDB 检测到死锁后通常选择一个代价较小的事务回滚,让其他事务继续执行;如果没有形成环,只是长时间等待,则由锁等待超时控制。

常用排查入口:

SHOW ENGINE INNODB STATUS;

SELECT *
FROM performance_schema.data_lock_waits;

performance_schema 中锁表的可用字段和表名与 MySQL 版本有关,应以当前实例为准。治理重点是:

  • 多个业务流程按相同顺序访问表和记录。
  • 缩短事务,避免在持锁期间做慢查询或远程调用。
  • 建立合适索引,减少扫描和加锁范围。
  • 控制批量更新规模。
  • 捕获死锁错误并重试整个事务,同时保证操作幂等。

仅调大锁等待超时不能消除死锁,反而可能延长故障暴露时间。

评论

On this page