MySQL 事务的实现原理详解

MySQL 事务的实现原理详解

重点围绕 InnoDB 存储引擎,因为真正支持事务的是 InnoDB。


一、事务与 ACID 简单回顾

事务(Transaction):一组要么全部成功、要么全部失败的 SQL 操作,是数据库并发控制的基本单位。

ACID 四个特性:

  1. 原子性(Atomicity)

    • 要么全部执行,要么全部回滚。
    • 失败时,数据库能恢复到事务开始前的状态。
  2. 一致性(Consistency)

    • 事务前后,数据库从一个"合法状态"到另一个"合法状态"。
    • 约束(主键、唯一约束、外键等)不会被破坏。
  3. 隔离性(Isolation)

    • 多个事务并发执行时,相互之间的可见性如何控制。
    • 对应四种隔离级别:RU、RC、RR、Serializable。
  4. 持久性(Durability)

    • 提交后的修改即使宕机也不会丢失。
    • 靠的是各种日志和刷盘机制。

二、InnoDB 事务的大框架

InnoDB 为了实现 ACID,核心用到了这几样东西:

  1. Redo Log(重做日志) ------ 保证持久性
  2. Undo Log(回滚日志) ------ 保证原子性 + MVCC 的基础
  3. 锁(Lock)系统 ------ 行级锁实现隔离性的一部分
  4. MVCC(多版本并发控制) ------ 实现 RC / RR 隔离级别下的"快照读"
  5. 两阶段提交(Redo Log + Binlog) ------ 保证崩溃恢复 & 主从复制的一致性

整体可以理解为:

"写操作先记日志(WAL 思想),通过日志 + 版本控制 + 锁来维持 ACID。"


三、原子性:Undo Log 如何工作

原子性 = 要么全做,要么全不做 。InnoDB 通过 Undo Log 实现"回滚能力"。

1. Undo Log 是什么

  • Undo Log 记录的是:如何把新数据恢复成旧数据
  • 例如:UPDATE t SET age = 18 WHERE id = 1;
    • Undo 里会记录:原来的 age 是多少。

2. Undo Log 的作用

  1. 事务回滚

    • 事务失败或主动 ROLLBACK 时,InnoDB 按 Undo Log 逆向操作,把数据恢复成修改前的版本。
  2. MVCC 的版本链基础

    • 每行记录除了当前值外,还通过 Undo Log 把历史版本串成一条"版本链"。
    • 这样"快照读"就可以读到旧版本,做到"读到事务开始时的视图"。

3. Undo Log 的类型

  • Insert Undo Log :对 INSERT 操作产生。

    • 事务未提交前,需要它来回滚"插入"。
    • 提交后这类 Undo 其实可以被丢弃(因为没人再需要看到"插入之前"的"没有这条记录"的版本)。
  • Update Undo Log :包括 UPDATEDELETE

    • 既用于回滚,也用于给其他事务提供历史版本(MVCC)。
    • 提交后不能立刻删除,要等没有事务再需要这些旧版本。

四、持久性:Redo Log 与 WAL 思想

持久性 = 提交之后不能丢

1. Redo Log 是什么

  • Redo 记录的是:对"数据页"的物理修改,类似于"操作步骤"。
  • 比如:修改了哪一个表空间、哪一页、哪个偏移量、改成什么值。

2. 为什么需要 Redo Log

直接把页面写回磁盘成本很高:

  • 随机写多、IO 慢;
  • 如果宕机在一半,页面处于中间状态。

所以 InnoDB 采用 WAL(Write-Ahead Logging)

  1. 先写 Redo Log(顺序写) 到日志文件/缓冲。
  2. 再异步把数据页刷回磁盘。

只要 Redo Log 写成功,就认为事务"持久化"了。

3. 提交时发生了什么

一个写事务大概流程:

  1. 修改内存中的 Buffer Pool 页(脏页)。
  2. 生成 Redo Log 写入 Redo Log Buffer。
  3. 事务 COMMIT 时,必须保证相关 Redo 至少刷新到磁盘 (或者根据 innodb_flush_log_at_trx_commit 的策略)。
  4. 真正的数据页晚点刷也没关系,宕机时可以靠 Redo Log"重做"回来。

4. 崩溃恢复时 Redo 的作用

  • 启动时:扫描 Redo Log,从最后一次一致性检查点开始重做,确保所有已经提交的事务都物理落盘。

五、隔离性:锁 + MVCC 的组合拳

隔离性主要通过两部分实现:

  1. 锁机制(Locking) ------ 控制对"同一行或范围"的并发修改
  2. MVCC(多版本并发控制) ------ 让读操作尽量不用加锁,直接读"快照"

1. 锁的种类(InnoDB 行级锁)

常见锁:

  • 记录锁(Record Lock):锁住某一条记录。
  • 间隙锁(Gap Lock):锁住两个值之间的"间隙",不锁已有记录。
  • Next-Key Lock:记录锁 + 间隙锁,锁住"某条记录以及它前面的间隙",用于防止幻读。
  • 意向锁(Intention Lock):表级锁,用来快速判断"某个表上是否有人加过行锁"。

思路:在高并发下,通过尽可能细粒度的行锁 + 间隙锁控制写冲突。

2. MVCC 的基本结构

InnoDB 每行记录内部有几个隐藏字段:

  • trx_id:最近一次修改该行的事务 id。
  • roll_pointer:指向 Undo Log 中该行的前一个版本。

于是:

  • 当前行 = 最新版本。
  • 沿着 roll_pointer 可以找到历史版本。
  • 不同事务按照自己的"快照规则",去选择某个版本来读。

这样:

  • 快照读(普通 SELECT):不加锁,通过版本链读取旧数据版本。
  • 当前读(SELECT ... FOR UPDATE / UPDATE / DELETE):要加锁,读的是当前最新版本。

六、四种隔离级别是如何实现的

MySQL 提供 4 种隔离级别:

  1. 读未提交(READ UNCOMMITTED,RU)
  2. 读已提交(READ COMMITTED,RC)
  3. 可重复读(REPEATABLE READ,RR)------ InnoDB 默认
  4. 串行化(SERIALIZABLE)

对应现象:脏读、不可重复读、幻读。

1. READ UNCOMMITTED

  • 读操作可以看到"未提交事务"的修改。
  • 实现上基本不使用 MVCC 快照,直接读最新版本。
  • 问题多,一般不用。

2. READ COMMITTED

  • 每条 SQL 开始时生成一个新的"快照"。
  • 同一个事务内,不同的 SELECT 可能读到不同版本的数据(不可重复读)。
  • 实现上:
    • 通过 MVCC,按照当前语句执行时的活跃事务列表过滤版本。

3. REPEATABLE READ(InnoDB 默认)

  • 事务开始时生成一致性视图(快照)。
  • 整个事务生命周期内,快照不变:
    • 多次 SELECT 结果一样(可重复读)。
  • 幻读问题:
    • 对于快照读,通过 MVCC 的版本规则避免看到其他事务新插入的数据。
    • 对于当前读(加锁的范围查询),通过 Next-Key Lock(记录锁 + 间隙锁)防止"幻影记录"插入。

4. SERIALIZABLE

  • 所有读都是加锁读,相当于"串行执行"。
  • 并发性最差,但隔离性最高。

七、一致性:规则 + 日志 + 约束

一致性是结果上的"正确性",主要来源于:

  1. 应用层逻辑:业务本身必须写对。
  2. 数据库约束:主键、外键、唯一约束、触发器等。
  3. 事务隔离:防止并发行为破坏规则。
  4. 日志与恢复机制:崩溃后回放 Redo / 回滚 Undo,确保不会得到"半条更新"的中间状态。

简化理解:

原子性 + 隔离性 + 持久性 + 约束 = 一致性。


八、Redo Log 与 Binlog 的两阶段提交

为了保证 MySQL Server 层的 BinlogInnoDB 的 Redo Log 一致,InnoDB 采用"两阶段提交":

  1. 阶段一:准备阶段(Prepare)

    • 写入 Redo Log(状态标记为 prepare)。
  2. 阶段二:提交阶段(Commit)

    • 写 Binlog。
    • 把 Redo Log 状态改为 commit

崩溃恢复时:

  • 看到 Redo Log 处于 prepare 状态,判断 Binlog 是否完整,决定是提交还是回滚。

这样就保证:

  • 不会出现 binlog 里有记录但 InnoDB 没有提交,或者反过来的情况。
  • 主从复制、崩溃恢复结果一致。

九、锁与两阶段锁协议

InnoDB 行锁遵循 两阶段锁协议(2PL)

  1. 在执行 SQL 过程中,不断 加锁,直到事务结束。
  2. 事务提交或回滚时,一次性释放所有锁

这会导致一些现象:

  • 一条后面的 SQL 所加的锁,可能会让前面已经执行的 SQL 也被锁住资源(因为都属于一个事务)。
  • 死锁:多个事务互相等待对方的锁。
    • InnoDB 会检测死锁(构建 wait-for 图),选出代价最小的事务回滚。

十、崩溃恢复的完整过程(简化版)

假设实例宕机,再启动:

  1. 加载数据字典 & 基本信息
  2. 扫描 Redo Log ,从 checkpoint 开始:
    • 对于已经 commit 的事务但数据没落盘的,进行 重做
  3. 回滚未提交事务
    • 利用 Undo Log,把它们做过的修改挨个撤销。

最终:

  • 所有已提交的事务都"看起来像是完全执行完了";
  • 所有未提交的事务"像是从未发生过"。
  • 原子性 + 持久性 得到了保证。

十一、整体总结(帮助你脑中形成一张图)

可以把 MySQL(InnoDB)的事务实现想象成一套"流水线":

  1. 写前日志(WAL)
    • 修改先写 Redo Log,再写数据页,保证崩溃可恢复(持久性)。
  2. 回滚日志(Undo Log)
    • 记录旧版本,支持回滚(原子性)+ 快照读(MVCC)。
  3. MVCC + 锁
    • 快照读用 MVCC,当前读用行锁 + 间隙锁,共同实现隔离性。
  4. 两阶段提交
    • Redo Log + Binlog 的两阶段提交,确保存储引擎和 Server 层的一致性。
  5. 约束 + 规则
    • 加上主键、唯一约束、外键以及业务逻辑,保证数据语义上的一致性。

掌握上面这些,你就已经从"会用事务"升级到"能从实现原理思考问题"的阶段了。

相关推荐
Codeking__17 小时前
Redis的value类型介绍——zset
数据库·redis·缓存
muddjsv17 小时前
SQLite3 核心命令全解析 (从入门到精通)
数据库
難釋懷17 小时前
认识NoSQL
数据库·nosql
亿坊电商17 小时前
利于SEO优化的CMS系统都有哪些特点?
前端·数据库
阿阿阿安17 小时前
MySQL(一)数据库风险操作场景总结
数据库·mysql
计算机程序设计小李同学17 小时前
平价药店销售与管理系统
java·mysql·spring·spring cloud·ssm
心丑姑娘18 小时前
使用ClickHouse时的劣质SQL样例
数据库·sql·clickhouse
什么都不会的Tristan18 小时前
redis篇
数据库·redis·缓存
only°夏至besos18 小时前
MySQL 运维实战:常见问题排查与解决方案
运维·数据库·mysql
液态不合群18 小时前
并发,并行与异步
数据库