MySQL 事务机制深度解析:从 ACID 到底层实现

MySQL 事务机制深度解析:从 ACID 到底层实现

MySQL 的事务机制主要由 InnoDB 存储引擎 实现,核心围绕 ACID 四大特性 ,通过 日志系统(redo log、undo log)锁机制MVCC(多版本并发控制) 共同协作完成。以下将系统拆解其实现原理。

一、事务的四大特性(ACID)与底层实现对应关系

事务是一组原子性的 SQL 操作,要么全部成功,要么全部失败。ACID 是事务的核心准则,其实现依赖 InnoDB 的不同机制:

特性 含义 底层实现机制
原子性(Atomicity) 事务是不可分割的最小单元,所有操作要么全部提交,要么全部回滚 undo log(回滚日志):记录数据修改前的逻辑状态,用于回滚时恢复数据
一致性(Consistency) 事务执行前后,数据从一个合法状态转换到另一个合法状态(如约束、索引完整性) 原子性、隔离性、持久性的共同结果,同时依赖数据库约束(如主键、外键)
隔离性(Isolation) 并发事务之间互不干扰,执行过程对其他事务透明 锁机制 (行锁、表锁、间隙锁)+ MVCC(多版本并发控制)
持久性(Durability) 事务一旦提交,对数据的修改永久生效,即使系统崩溃也不丢失 redo log(重做日志):记录数据页的物理修改,用于崩溃恢复

二、核心日志系统:redo log 与 undo log

日志是 InnoDB 实现事务的基石,redo log 保证持久性 ,undo log 保证原子性,二者通过"两阶段提交"协作。

1. redo log(重做日志)

作用
  • 崩溃恢复:当 MySQL 宕机时,通过 redo log 恢复未刷盘的缓冲池数据,保证事务提交后数据不丢失。
  • WAL 机制(Write-Ahead Logging):先写日志,再刷磁盘,减少随机 I/O(日志是顺序 I/O)。
写入时机

redo log 的写入分为两个阶段:

  1. redo log buffer:事务执行过程中,修改数据先写入内存中的日志缓冲区(默认 16MB)。
  2. 刷盘(fsync) :根据 innodb_flush_log_at_trx_commit 参数策略刷入磁盘:
    • 0:每秒刷盘一次,事务提交时不主动刷盘(性能最高,但宕机丢 1 秒数据)。
    • 1:事务提交时立即刷盘(默认,保证持久性,性能中等)。
    • 2:事务提交时写入操作系统缓存,每秒刷盘一次(性能较好,宕机丢操作系统缓存数据)。
物理日志格式

redo log 是物理日志,记录"哪个数据页的哪个偏移量做了什么修改",例如:

"表空间 ID 为 10,页号为 100,偏移量 500 处,将字节从 0x01 改为 0x02"。

2. undo log(回滚日志)

作用
  • 事务回滚 :当事务执行失败或调用 ROLLBACK 时,通过 undo log 恢复数据到修改前的状态(保证原子性)。
  • MVCC 版本链:为多版本并发控制提供历史数据版本(实现快照读)。
写入时机

修改数据之前,先将数据的原始状态写入 undo log(逻辑日志)。例如:

  • 执行 UPDATE t SET name='B' WHERE id=1 前,先记录 undo log:id=1 的 name 原来是 'A'
逻辑日志格式

undo log 是逻辑日志,记录"如何撤销当前操作",分为两类:

  • insert undo log :针对 INSERT 操作,事务提交后可直接删除(仅用于回滚)。
  • update undo log :针对 UPDATE/DELETE 操作,事务提交后需保留(用于 MVCC,由 purge 线程异步清理)。

3. redo log 与 undo log 的区别与协作

核心区别
维度 redo log undo log
日志类型 物理日志(数据页修改) 逻辑日志(数据历史状态)
作用 崩溃恢复,保证持久性 事务回滚 + MVCC,保证原子性
写入时机 修改数据后(先写 buffer,再刷盘) 修改数据前(先写 undo,再改数据)
空间管理 循环写入(固定大小,默认 48MB) 追加写入(存储在回滚段中)
协作:两阶段提交(2PC)

为了保证 redo log(InnoDB 层)与 binlog(Server 层,用于主从复制、归档)的一致性,事务提交采用两阶段提交

  1. Prepare 阶段
    • redo log 刷盘,标记事务状态为 PREPARED(已准备提交)。
  2. Commit 阶段
    • 写入 binlog 并刷盘(Server 层)。
    • redo log 标记为 COMMITTED(事务正式提交)。

崩溃恢复逻辑

  • redo logCOMMITTED:直接提交事务。
  • redo logPREPARED:检查 binlog 是否存在,存在则提交,不存在则回滚。

三、并发控制:锁机制与 MVCC

隔离性的实现依赖"锁"解决当前读 的并发问题,依赖"MVCC"解决快照读的并发问题,二者结合实现高并发下的数据隔离。

1. 锁机制

InnoDB 支持表锁行锁 ,核心是行锁(粒度细,并发高)。

行锁的类型
  • 共享锁(S 锁) :读锁,允许其他事务加 S 锁,但不允许加 X 锁(SELECT ... LOCK IN SHARE MODE)。
  • 排他锁(X 锁) :写锁,不允许其他事务加任何锁(UPDATE/DELETE/INSERTSELECT ... FOR UPDATE)。
间隙锁(Gap Lock)与 Next-Key Lock

为了解决幻读 (同一事务内两次当前读结果不一致),InnoDB 在 REPEATABLE READ(RR) 隔离级别下引入:

  • 间隙锁:锁定索引记录之间的"间隙"(不包含记录本身),防止其他事务插入新数据。
  • Next-Key Lock行锁 + 间隙锁 ,锁定"前开后闭区间"(如索引值为 1、3、5,则锁定区间 (-∞,1](1,3](3,5](5,+∞)),彻底避免幻读。
锁的作用场景
  • 当前读 :读取的是最新数据(如 SELECT ... FOR UPDATEUPDATEDELETE),需加锁保证并发安全。
  • 快照读 :读取的是历史版本(如普通 SELECT),通过 MVCC 实现,无需加锁。

2. MVCC(多版本并发控制)

MVCC 通过数据版本链Read View(读视图) 实现"快照读",让并发事务之间互不干扰,提升读性能。

核心组件
(1)隐藏字段

InnoDB 为每行数据添加 3 个隐藏字段:

  • DB_TRX_ID:最后一次修改该行的事务 ID(6 字节)。
  • DB_ROLL_PTR:回滚指针(7 字节),指向 undo log 中的历史版本。
  • DB_ROW_ID:隐藏主键(6 字节),若表无主键则自动生成。
(2)版本链

每次修改数据时,都会生成一个新版本,并通过 DB_ROLL_PTR 连接到 undo log 中的旧版本,形成版本链(链表头是最新版本,链表尾是最旧版本)。

例如:

  1. 事务 A(ID=10)插入一行数据:DB_TRX_ID=10DB_ROLL_PTR=null
  2. 事务 B(ID=20)修改该行:生成新版本,DB_TRX_ID=20DB_ROLL_PTR 指向事务 A 的版本(undo log)。
  3. 事务 C(ID=30)再次修改:生成新版本,DB_TRX_ID=30DB_ROLL_PTR 指向事务 B 的版本。
(3)Read View(读视图)

当事务执行快照读 (普通 SELECT)时,生成一个 Read View,用于判断版本链中哪个版本对当前事务可见

Read View 包含 4 个核心信息:

  • m_ids:生成 Read View 时,活跃的事务 ID 列表(未提交的事务)。
  • min_trx_idm_ids 中的最小事务 ID。
  • max_trx_id:生成 Read View 时,系统下一个要分配的事务 ID(m_ids 最大值 +1)。
  • creator_trx_id:当前事务的 ID。
(4)可见性判断规则

遍历版本链,从最新版本开始,依次判断:

  1. 若版本的 DB_TRX_ID == creator_trx_id:是当前事务自己修改的,可见
  2. 若版本的 DB_TRX_ID < min_trx_id:该版本在生成 Read View 前已提交,可见
  3. 若版本的 DB_TRX_ID >= max_trx_id:该版本在生成 Read View 后才开启,不可见
  4. min_trx_id <= DB_TRX_ID < max_trx_id:检查 DB_TRX_ID 是否在 m_ids 中:
    • 若在:该版本是活跃事务修改的(未提交),不可见
    • 若不在:该版本已提交,可见

若当前版本不可见,通过 DB_ROLL_PTR 找到上一个版本,重复判断,直到找到可见版本或遍历完版本链。

四、事务执行流程:一条更新语句的完整旅程

BEGIN; UPDATE t SET name='B' WHERE id=1; COMMIT; 为例,拆解事务从开始到提交的完整过程:

步骤 1:开始事务

执行 BEGINSTART TRANSACTION,InnoDB 为事务分配唯一的 事务 ID(trx_id),但此时并未真正开始(延迟到第一条 SQL 执行)。

步骤 2:读取数据页

执行 UPDATE 时,先检查缓冲池(Buffer Pool) 中是否存在 id=1 的数据页:

  • 若存在:直接读取缓冲池中的页。
  • 若不存在:从磁盘读取数据页到缓冲池(产生随机 I/O)。

步骤 3:修改数据页

在缓冲池中修改数据页(将 nameA 改为 B),此时缓冲池中的页变为脏页(与磁盘不一致)。

步骤 4:记录 undo log

在修改数据前,先将原始数据(name=A)写入 undo log(逻辑日志),并更新数据行的 DB_ROLL_PTR 指向该 undo log,形成版本链。

步骤 5:记录 redo log

将数据页的物理修改写入 redo log buffer(内存),此时 redo log 记录的是"哪个页的哪个偏移量做了什么修改"。

步骤 6:提交事务(两阶段提交)

阶段 1:Prepare
  • redo log buffer 刷盘(根据 innodb_flush_log_at_trx_commit 参数),标记事务状态为 PREPARED
阶段 2:Commit
  • 写入 binlog(Server 层,逻辑日志,记录 SQL 语句或行变更)并刷盘。
  • redo log 标记为 COMMITTED,事务正式提交。

步骤 7:后台刷脏页

事务提交后,缓冲池中的脏页由后台线程(Master Thread、Page Cleaner Thread)异步刷入磁盘(减少随机 I/O,提升性能)。

回滚流程(若事务失败)

若执行 ROLLBACK 或事务崩溃:

  1. 读取 undo log,根据版本链恢复数据到修改前的状态。
  2. 记录 redo log(回滚操作也需 redo log 保证持久性)。
  3. 释放锁,清理 undo log(insert undo log 直接删除,update undo log 由 purge 线程异步清理)。

五、默认隔离级别(REPEATABLE READ)的实现

MySQL 默认隔离级别是 REPEATABLE READ(RR,可重复读) ,通过 MVCCNext-Key Lock 共同实现,解决了不可重复读幻读问题。

1. 解决不可重复读(快照读)

不可重复读:同一事务内,两次快照读结果不一致(其他事务修改了数据)。

实现方式

  • RR 隔离级别下,Read View 是在事务第一次执行快照读时生成的(而非每次快照读都生成)。
  • 后续所有快照读都使用同一个 Read View,因此只能看到生成 Read View 前已提交的事务修改,保证了可重复读。

2. 解决幻读(当前读)

幻读:同一事务内,两次当前读结果不一致(其他事务插入了新数据)。

实现方式

  • 对于快照读:通过 MVCC 版本链,只能看到 Read View 生成前的数据,新插入的数据不可见,因此不会出现幻读。
  • 对于当前读 (如 SELECT ... FOR UPDATEUPDATE):通过 Next-Key Lock(行锁 + 间隙锁)锁定查询范围,防止其他事务插入新数据,彻底避免幻读。

示例:RR 隔离级别下的幻读防护

假设有表 t,索引 id 有值 1、3、5:

  1. 事务 A 执行 SELECT * FROM t WHERE id BETWEEN 2 AND 4 FOR UPDATE;(当前读)。
  2. InnoDB 对区间 (1,3](3,5] 加 Next-Key Lock(锁定 3 及其前后间隙)。
  3. 事务 B 尝试 INSERT INTO t (id) VALUES (2);,因间隙 (1,3) 被锁定,插入被阻塞。
  4. 事务 A 再次执行当前读,结果与第一次一致,无幻读。

总结

MySQL 事务机制的核心是:

  • 原子性:undo log 回滚。
  • 持久性:redo log 崩溃恢复。
  • 隔离性:MVCC(快照读)+ 锁(当前读)。
  • 一致性:三者共同保证。

其中,redo log 与 undo log 是事务的"左膀右臂",MVCC 是高并发的关键,而 Next-Key Lock 则彻底解决了 RR 隔离级别下的幻读问题。这套机制既保证了数据安全,又实现了高性能并发,是 InnoDB 成为主流存储引擎的核心原因。

相关推荐
程序员夏末2 小时前
【MySQL | 第二篇】 MVCC的底层实现(多版本并发控制)
数据库·sql·mysql
庞轩px2 小时前
线程池核心参数与拒绝策略深度解析
java·jvm·数据库
xcLeigh2 小时前
Oracle 迁移深度复盘:多数据库选型决策全解析
大数据·数据库·sql·oracle·数据迁移·数据管理
王仲肖2 小时前
PostgreSQL pageinspect 插件深度解析
数据库·postgresql
云边有个稻草人2 小时前
【MySQL】第十四节—事务:从基础概念到隔离性理论与实践 | 详解
数据库·mysql·事务·隔离级别·事务的隔离性·事务提交方式
干啥啥不行,秃头第一名2 小时前
Python深度学习入门:TensorFlow 2.0/Keras实战
jvm·数据库·python
FL4m3Y4n2 小时前
redis的主从同步与对象模型
数据库·redis·缓存