MySQL 面试题(三):事务、MVCC 与锁

所属系列:MySQL 面试题 · 第 3 / 4 篇

事务与锁是最容易因为“背结论”而答错的部分。回答时要先区分快照读和当前读,再说明隔离级别、索引条件和锁范围。

ACID 分别由什么保证

  • 原子性(Atomicity):undo log 等机制支持回滚;
  • 一致性(Consistency):由事务机制、约束和业务规则共同保证;
  • 隔离性(Isolation):MVCC 与锁共同实现;
  • 持久性(Durability):redo log、刷盘策略和存储系统共同保证。

“一致性完全由数据库自动保证”是不准确的。余额不能为负、订单状态不可逆等业务约束仍需要应用和数据库共同表达。

四种隔离级别

隔离级别 脏读 不可重复读 幻读
读未提交(RU) 可能 可能 可能
读已提交(RC) 避免 可能 可能
可重复读(RR) 避免 避免 快照读通常避免;当前读依赖锁
串行化(Serializable) 避免 避免 避免

InnoDB 默认是可重复读(RR)。不能简单说“RR 彻底解决所有幻读”,因为普通 SELECT 的快照读与 SELECT ... FOR UPDATE 的当前读机制不同。

什么是 MVCC

多版本并发控制(MVCC)让读操作在很多情况下不用阻塞写操作。InnoDB 通过隐藏事务信息、undo log 版本链和 Read View 判断某个版本是否可见。

RC 和 RR 的 Read View 区别

  • RC:通常每条一致性读语句创建新的 Read View;
  • RR:通常事务第一次一致性读时创建 Read View,并在事务内复用。

因此 RC 下两次查询可能看到其他事务已经提交的新值,而 RR 下通常保持一致。

快照读和当前读

普通查询通常是快照读:

SELECT * FROM orders WHERE id = 1;

需要读取最新版本并加锁的是当前读:

SELECT * FROM orders WHERE id = 1 FOR UPDATE;
SELECT * FROM orders WHERE id = 1 FOR SHARE;
UPDATE orders SET status = 'paid' WHERE id = 1;
DELETE FROM orders WHERE id = 1;

旧语法 LOCK IN SHARE MODE 仍可能见到,但 MySQL 8 更推荐 FOR SHARE

行锁真的只锁一行吗

InnoDB 的行锁本质上锁索引记录。锁范围取决于:

  • 使用了哪个索引;
  • 条件是唯一等值、普通等值还是范围;
  • 隔离级别;
  • 记录是否存在;
  • 优化器的执行计划。

如果更新条件没有可用索引,可能扫描并锁住大量记录,表现得像“锁表”。

记录锁、间隙锁和临键锁

  • 记录锁(Record Lock):锁住索引记录;
  • 间隙锁(Gap Lock):锁住索引记录之间的间隙,主要阻止插入;
  • 临键锁(Next-Key Lock):记录锁与其前方间隙的组合。

在 RR 下,范围当前读可能使用临键锁避免其他事务插入范围内的新记录。

假设索引值为 10, 20, 30

SELECT *
FROM orders
WHERE amount >= 20 AND amount < 30
FOR UPDATE;

具体锁范围不能只靠 SQL 文本猜测,应结合索引、执行计划和 performance_schema.data_locks 查看。

乐观锁和悲观锁

悲观锁直接依赖数据库锁:

START TRANSACTION;
SELECT stock FROM products WHERE id = 1 FOR UPDATE;
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;

乐观锁通常使用版本号:

UPDATE products
SET stock = stock - 1,
    version = version + 1
WHERE id = ?
  AND stock > 0
  AND version = ?;

应用必须检查受影响行数;更新 0 行意味着冲突或库存不足,不能当成成功。

死锁是怎么发生的

事务 A:

UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

事务 B 以相反顺序访问:

UPDATE accounts SET balance = balance - 50 WHERE id = 2;
UPDATE accounts SET balance = balance + 50 WHERE id = 1;

两者可能形成循环等待。InnoDB 会检测死锁并回滚代价较小的事务之一。

如何减少死锁

  • 事务按固定顺序访问资源;
  • 缩短事务,不在持锁期间调用远程接口;
  • 为过滤条件建立合适索引;
  • 一次锁定需要的记录;
  • 对死锁错误做有限次数、带退避的重试;
  • 避免把锁等待超时当作死锁处理。

如何查看锁和死锁

MySQL 8 推荐:

SELECT * FROM performance_schema.data_locks;
SELECT * FROM performance_schema.data_lock_waits;
SHOW ENGINE INNODB STATUS\G

INFORMATION_SCHEMA.INNODB_LOCKSINNODB_LOCK_WAITS 在较新的 MySQL 版本中已经被 Performance Schema 对应表替代。

redo log、undo log 和 binlog

  • redo log:InnoDB 物理日志,主要用于崩溃恢复;
  • undo log:保存旧版本,支持事务回滚和 MVCC;
  • binlog:Server 层逻辑日志,用于复制和时间点恢复。

事务提交时需要协调 redo log 与 binlog,避免出现“数据库已提交但复制日志缺失”或相反状态。

面试速答

SELECT 会加锁吗

普通一致性读通常不加行锁;FOR UPDATEFOR SHARE、串行化隔离级别以及部分特殊场景会加锁。

锁等待超时和死锁一样吗

不一样。死锁是循环等待,InnoDB 可以检测并主动回滚事务;锁等待超时是等待超过 innodb_lock_wait_timeout

长事务有什么危害

会长期持锁、阻碍 undo 清理、扩大版本链、增加复制延迟与故障恢复成本。

下一篇将把这些机制用于慢 SQL、线上抖动和大表变更排查。