ACID 是什么
- 原子性:事务中的操作全部成功或全部回滚,主要依赖 Undo Log。
- 一致性:事务执行前后业务约束保持成立,是前三项共同服务的目标。
- 隔离性:并发事务互相隔离,通过 MVCC 和锁实现。
- 持久性:事务提交后的数据可以恢复,主要依赖 Redo Log 和刷盘机制。
四种隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| Read Uncommitted | 可能 | 可能 | 可能 |
| Read Committed | 避免 | 可能 | 可能 |
| Repeatable Read | 避免 | 避免 | 需结合具体读方式分析 |
| Serializable | 避免 | 避免 | 避免 |
InnoDB 默认通常是 Repeatable Read,但部署环境可以修改,应用不应凭印象假设。
MVCC 如何工作
InnoDB 的聚簇索引记录包含隐藏事务信息。修改记录时,旧版本通过 Undo Log 形成版本链。Read View 保存活跃事务范围,查询根据可见性规则沿版本链找到自己能看到的版本。
在 Read Committed 下,一般每条一致性读语句创建新的 Read View;在 Repeatable Read 下,事务内首次一致性读建立的视图通常会被后续一致性读复用,因此能够重复读取相同版本。
快照读与当前读
普通 SELECT 通常是快照读,读取符合 Read View 的历史版本。SELECT ... FOR UPDATE、UPDATE、DELETE 属于当前读,需要读取最新记录并加锁。
这也是理解“RR 是否完全解决幻读”的关键:快照读依靠一致性视图,当前读还会结合记录锁、间隙锁或 Next-Key Lock 控制范围内的并发写入。
锁与索引的关系
InnoDB 的行锁实质上加在索引记录上。更新条件没有合适索引时,可能扫描并锁定大量记录,显著扩大影响范围。间隙锁锁定的是索引区间而不是某条实际记录,主要用于防止范围内插入。
长事务为什么危险
长事务会持续持有锁和旧 Read View,导致 Undo 版本无法及时清理,增加存储和查询成本,也提高死锁与主从延迟风险。事务中不应执行远程调用、等待用户输入等不可控操作。
实战建议
- 事务尽量短小,只包围必须保证一致性的数据库操作。
- 按固定顺序访问资源,降低死锁概率。
- 捕获死锁或锁等待超时后,只对幂等操作进行有限重试。
- 用
EXPLAIN检查索引,避免无谓扩大锁范围。 - 明确业务能接受的隔离级别,不要盲目追求最高隔离。
核心考点清单
- Undo Log 支撑回滚与历史版本,Redo Log 支撑崩溃恢复。
- Read View 决定版本可见性,RC 和 RR 创建视图的时机不同。
- 普通 SELECT 常为快照读,
FOR UPDATE、UPDATE、DELETE 属于当前读。 - InnoDB 行锁加在索引记录上,无合适索引可能扩大扫描与锁定范围。
高频追问与参考回答
追问:MVCC 能避免所有锁吗?
不能。它主要优化一致性读;当前读、更新和唯一性检查仍需要锁,DDL 与元数据访问也有自己的协调机制。
追问:RR 下还有幻读吗?
快照读通过一致性视图避免前后结果变化,当前读结合 Next-Key Lock 控制范围插入。混用快照读和当前读时仍需准确分析语义,不能只背“完全解决”。
机制全景图
下面把「MySQL 事务隔离级别与 MVCC 原理」从输入到结果压缩成一条可复述的主链路。面试时先用图建立全局坐标,再进入局部实现,能避免只背零散结论。
flowchart LR
A["事务分配标识"]
A --> B["更新生成新版本与 undo"]
B --> C["读取建立 Read View"]
C --> D["沿版本链判断可见性"]
D --> E["提交后 Purge 清理历史"]
完整链路:从输入到结果
沿着「事务分配标识 → 更新生成新版本与 undo → 读取建立 Read View → 沿版本链判断可见性 → 提交后 Purge 清理历史」观察输入、状态与输出,下面每个阶段都对应一个可以在源码、日志或系统表中验证的位置。
1. 事务分配标识
InnoDB 事务开始与首次一致性读的时机受隔离级别影响,Read View 不一定在 begin 时创建。
2. 更新生成新版本与 undo
更新记录写入新值,并通过隐藏事务 ID 与 roll pointer 连接 undo 中的旧版本。
3. 读取建立 Read View
Read View 保存活跃事务范围,用于判断某版本在当前快照是否可见。
4. 沿版本链判断可见性
不可见时沿 undo 版本链回溯;长事务会让链条变长并增加读放大。
5. 提交后 Purge 清理历史
历史版本只有在不再被任何活跃快照需要时才能 Purge,长事务会阻塞回收。
源码与实现定位
| 入口 | 阅读重点 |
|---|---|
| storage/innobase/read/read0read.cc | Read View 可见性判断 |
| SHOW ENGINE INNODB STATUS | History List Length 与事务 |
源码或系统表应按上表顺序追踪:先确认入口实际走到哪条路径,再用运行时数据验证,而不是仅凭类名或配置推测。
参数配置与可复现实验
SELECT * FROM information_schema.innodb_trx
ORDER BY trx_started;
保持一个长 RR 快照同时持续更新,观察 undo 历史、版本链读耗时和 Purge 恢复。
验证步骤与预期结果
1. 固定输入和基线
先在没有故障注入的环境执行上述配置,固定数据规模、并发度、运行时版本和预热时间。以「最长事务」为主基线,记录值应满足「在线请求秒级」;同时保存 活跃事务时长、History List Length,使后续变化能够回到同一时间轴比较。
2. 从实现入口确认路径
在「storage/innobase/read/read0read.cc」确认请求确实进入「Read View 可见性判断」对应的实现,再沿「SHOW ENGINE INNODB STATUS」观察「History List Length 与事务」。如果入口路径都未命中,就不应继续调整下游参数,而应先检查调用条件、版本或路由是否与假设一致。
3. 注入本文特有的失败模式
优先复现「把快照读误认为完全无锁事务」,并把单一变量逐级放大,直到「最长事务」越过「>60s 示例」。随后再分别验证「长事务阻塞 Purge」和「更新后又把当前读和快照读结果混用」,三类故障分开执行,避免多个变量同时变化而无法归因。
4. 执行止损和根因修复
第一轮只应用「终止异常长事务」,确认它能控制影响范围;第二轮应用「报表拆短批次/副本」,验证核心链路恢复;最后落实「监控 trx_started 和 History List」,消除同类问题再次出现的条件。每一步都保留变更前后数据,不用“感觉变快了”替代测量。
5. 通过退出条件
实验只有同时满足三项才算通过:「最长事务」回到「在线请求秒级」、「History List」回到「稳态可回落」、「版本链读取」回到「接近当前版本」,并且业务结果差异为零。若性能恢复但结果不一致,仍应视为失败;若指标恢复后很快再次越线,则说明只完成了临时止损,没有消除根因。
量化基线
| 指标 | 样例基线/口径 | 风险线 | 结论 |
|---|---|---|---|
| 最长事务 | 在线请求秒级 | >60s 示例 | 长快照 |
| History List | 稳态可回落 | 持续单调涨 | Purge 被阻 |
| 版本链读取 | 接近当前版本 | 读延迟随时间涨 | undo 链过长 |
这些数值是实验口径或示例告警线,不是可复制到所有系统的固定答案;上线阈值应由本系统稳态、峰值和故障演练共同确定。
事故复盘:报表长事务导致 undo 膨胀
只读报表保持事务数小时,线上更新持续产生 undo,History List Length 不断上升,磁盘和查询延迟恶化。把报表拆成短快照批次并在只读副本执行后,Purge 才能追上。
| 失败模式 | 首要证据 | 第一处置动作 |
|---|---|---|
| 把快照读误认为完全无锁事务 | 活跃事务时长 | 终止异常长事务 |
| 长事务阻塞 Purge | History List Length | 报表拆短批次/副本 |
| 更新后又把当前读和快照读结果混用 | undo 表空间 | 监控 trx_started 和 History List |
发布与回滚检查点
- 发布前:确认「storage/innobase/read/read0read.cc」对应实现和上述配置在目标版本仍然有效,并保存「最长事务」基线。
- 灰度中:同时观察 活跃事务时长、History List Length、undo 表空间;任一指标越过表中风险线,就停止继续扩量。
- 回滚时:先执行「终止异常长事务」控制影响,再回退代码或参数;涉及持久状态时必须额外核对结果差异。
- 发布后:至少覆盖一个完整峰值周期,确认「把快照读误认为完全无锁事务」没有再次出现,才关闭变更观察窗口。
方案对比与选型
| 方案 | 更适合的场景 | 主要收益 | 代价与边界 |
|---|---|---|---|
| READ COMMITTED | 允许同事务两次读看到新提交 | 版本链压力较低、语义直观 | 不可重复读 |
| REPEATABLE READ | 事务内需要一致快照 | InnoDB 默认且配合 Next-Key Lock | 长事务持有旧快照更久 |
| SERIALIZABLE/显式锁读 | 关键不变量需强串行 | 语义最强 | 锁竞争与吞吐成本高 |
选型至少带上 数据规模、选择性、读写比、事务长度和峰值并发,并用上面的量化基线验证;未知数据应明确为待测假设。
设计边界与工程取舍
MVCC 解决读写并发的版本可见性,不替代唯一约束和业务不变量;写写冲突仍需锁,当前读也会参与锁定。
工程落地遵循:正确性由约束和事务兜底,性能优化必须用执行计划与测量验证。回答时直接引用「storage/innobase/read/read0read.cc」、配置实验和事故数据,比复述固定模板更有说服力。