MySQL InnoDB事务acid特性的原理和隔离级别的实现原理

InnoDB存储引擎

InnoDB存储结构

表空间

则每张表都会有一个表空间(xxx.ibd),一个mysql实例可以对应多个表空间

  • 系统表空间
    • 存储数据字典(表结构定义、索引信息等)、Change Buffer、Doublewrite Buffer
    • undo log,默认在此可更改到独立表空间
    • 默认存储在ibdata1文件中
  • 独立表空间
    • 每个表单独对应一个.ibd文件(存储表数据和索引)
  • 通用表空间
    • 存储多个表的数据和索引
  • 临时表空间
    • 临时表数据CREATE TEMPORARY TABLE
    • 排序和聚合操作的临时数据ORDER BYGROUP BY
    • JOIN多表连接的临时数据
  • Undo 表空间
    • 存储 Undo Log(默认位于系统表空间,可分离)

  • 数据段:B+树叶子节点
  • 索引段:B+树非叶子结点
  • 回滚段:管理undo log

  • 连续分配的最小单元 (1区 = 64个连续页 = 1MB也就是)
  • 作用:减少随机 I/O(预分配连续空间),避免大量小页零散分布

  • 磁盘IO最小单元 (默认 16KB

  • InnoDB 存储引擎数据是按行进行存放的

InnoDB的内存架构

核心组件

InnoDB 内存架构 缓冲池 Buffer Pool 日志缓冲区 Log Buffer 自适应哈希索引 AHI 更改缓冲区 Change Buffer

缓冲池Buffer Pool
  • 作用:缓存磁盘数据页,减少磁盘IO操作
  • LRU算法(最近最少使用)

是 否 新数据页 插入到旧子列表头部 停留时间 > 1s? 移动到新子列表头部 保持原位 频繁访问页保持在前端 短期访问页快速淘汰

  • 分区管理
    • 新子列表 (37%):频繁访问的热数据
    • 旧子列表 (63%):新加载的冷数据
  • 在专用服务器上,通常将多达**80%**的物理内存分配给缓冲池
日志缓冲区Log Buffer
  • 用来保存要写入到磁盘中的log日志数据(redo log 、undo log), 默认大小为 16MB,日志缓冲区的日志会定期刷新到磁盘中。如果需要更新、插入或删除许多行的事 务,增加日志缓冲区的大小可以减少磁盘 I/O。
  • 参数:
    • innodb_log_buffer_size:缓冲区大小
    • innodb_flush_log_at_trx_commit:日志刷新到磁盘时机,取值主要包含以下三个:
      • 1: 日志在每次事务提交时写入并刷新到磁盘,默认值。
      • 0: 每秒将日志写入并刷新到磁盘一次。
      • 2: 日志在每次事务提交后写入,并每秒刷新到磁盘一次。
更改缓冲区Change Buffer

针对非唯一二级索引,在执行DML语句时,如果这些语句不在Buffer Pool中,不会直接操作磁盘进行修改,而是先将数据变更存在Change Buffer中,在未来数据读取时,将数据合并到Buffer Pool中,再将合并后的数据刷新到磁盘中。

自适应哈希索引

自适应hash索引,用于优化对Buffer Pool数据的查询。MySQL的innoDB引擎中虽然没有直接支持 hash索引,但是给我们提供了一个功能就是这个自适应hash索引。因为前面我们讲到过,hash索引在 进行等值匹配时,一般性能是要高于B+树的,因为hash索引一般只需要一次IO即可,而B+树,可能需 要几次匹配,所以hash索引的效率要高,但是hash索引又不适合做范围查询、模糊匹配等。 InnoDB存储引擎会监控对表上各索引页的查询,如果观察到在特定的条件下hash索引可以提升速度, 则建立hash索引,称之为自适应hash索引。

MVCC

多版本并发控制 ,是数据库实现高并发访问的核心技术,维护一个数据的多个版本,使得MySQL能在RR和RC级别不使用锁机制的情况下实现非阻塞读,同时保证事务的隔离性。

RU读取最新的数据版本,除事务回滚用到undo log不涉及MVCC快照读。

S将所有读操作隐式转换为当前读(FOR SHARE),同样不涉及快照读。

MVCC 核心组成

组件 作用
Undo Log 存储数据历史版本链
----------------
Read View 事务开启时生成的"数据可见性快照"
-----------------
表中隐藏列 记录事务版本信息
DB_TRX_ID 最近修改/插入该数据的事务ID,最后一次修改该记录的事务ID
DB_ROLL_PTR 指向 Undo Log 的指针(用于回溯历史版本),指向上一个版本

undo log

回滚日志 ,是一种逻辑日志但记录的数据修改前的物理行数据值。是InnoDB引擎中实现事务原子性一致性MVCC的重要机制。记录事务对数据的修改操作,用于事务回滚时提供撤销修改的数据依据,或在快照读时提供历史版本数据。

undo log类型
  • Insert undo log(插入回滚日志) :仅用于记录 INSERT 操作。
    • 记录内容:插入的完整行数据 (包括所有字段值)。
    • 原因:插入的记录在事务提交前,仅对当前事务可见,其他事务无法访问。若事务回滚,只需通过 undo log 定位到这些插入的行,直接删除即可(反向操作是 "删除插入的行",而 undo log 记录行数据是为了精准定位要删除的记录)。
  • Update undo log(更新回滚日志) :用于记录 UPDATEDELETE 操作(注:InnoDB 中 DELETE 本质是标记删除,也属于特殊的更新)。
    • 记录内容:被修改行的旧版本数据 (包括所有字段值),而非抽象的 "反向操作逻辑"。
    • 原因:更新 / 删除操作会改变行的已有数据,回滚时需要恢复到修改前的状态。例如,若将 age=20 改为 age=30,undo log 会记录 age=20(旧值)以及未修改的字段数据,回滚时直接用旧值覆盖新值即可;若删除一行,undo log 会记录该行删除前的完整数据,回滚时重新插入该数据(恢复删除)。
存储方式
  • 存储在 InnoDB 的undo 表空间

  • 按 "段"管理,每个事务会分配一个或多个 undo log 段。

核心作用
  • 事务回滚

    • 当事务回滚ROLLBACK或数据库崩溃,InnoDB通过undo log实现对数据修改的撤销,恢复到事务开始时的状态。
    • 示例
    sql 复制代码
    BEGIN;
    UPDATE users SET balance = 100 WHERE id = 1;  -- 记录undo log(旧值balance=50)
    DELETE FROM orders WHERE id = 10;  -- 记录undo log(旧记录完整信息)
    ROLLBACK;  -- 执行undo log:balance恢复为50,orders表恢复id=10的记录
  • MVCC支持

    • 快照读(普通SELECT)时,InnoDB通过undo log获取数据的历史版本,确保事务执行过程中看到的是事务开始时的一致性视图,不会受其他事务影响,避免了脏读、不可重复读、幻读。
事务回滚支持
  • 事务开始:分配undo log空间。
  • 修改操作 :每次执行写操作,将旧的数据版本写入undo log。
    • 例如:UPDATE t SET a=2 WHERE id=1(原 a=1),undo log 记录(id=1, a=1)
  • 事务提交
    • INSERT undo log:直接标记为可删除。
    • UPDATE/DELETE undo log:保留,供其他事务的快照读使用,由 purge 线程后续清理。
  • 事务回滚:反向执行 undo log 中的操作(如将 a=2 恢复为 a=1), v 彻底撤销事务影响。
MVCC支持
  • 配合每行数据隐藏列DB_TRX_ID(记录最后一次修改的事务ID)和DB_ROLL_PTR(指向undo log的指针)。
  • 当事务需要获取对应的数据版本时,通过DB_ROLL_PTR遍历undo log获取符合当前事务可见性的版本。
版本链

版本链是快照读(普通SELECT)实现一致性视图的核心。

版本链的每个节点对应事务对某行的 一次修改,而非一个事务的多次修改 。版本链是通过行记录的 roll_ptr 指针和 undo log 记录的 prev 指针串联形成的,每一次修改都会生成一个新的 undo log 节点。

示例

一张表的原始数据为:

id age name DB_TRX_ID DB_ROLL_PTR
30 30 A30 1 null

四个并发事务同时访问这张表

事务2 事务3 事务4 事务5
开始事务 开始事务 开始事务 开始事务
修改age=3(id=30) 查询id=30的记录
提交事务
修改name=A3(id=30)
查询id=30的记录
提交事务
修改age=10(id=30)
查询id=30的记录
查询id=30的记录
提交事务

当事务2执行修改时,创建最新的版本(age=3),旧数据会记录在undo log日志,形成下图版本链:

当事务3执行修改操作时,创建新的版本(name=A3),旧数据(age=3,非整行数据)会记录在undo log,新版本DB_ROLL_PTR指向修改前旧版本:

当事务3执行修改操作时,创建新的数据版本,旧数据(age=3)记录在undo log,新数据版本DB_ROLL_PTR指向修改前的旧版本:

不同事务或相同事务对同一记录进行修改,会导致该记录的undo log形成一条不同版本的版本链表,链表头部是最新的数据版本,尾部是最早的数据版本。

Read View

一致性视图,在事务开始时创建,记录了事务启动时活跃事务状态。通过比对Read View中的参数和undo log中数据版本的事务ID,可以判断事务在某时间点能看到的数据版本范围,是事务内一致性读的关键。

Read View的组成

Read View包含四个核心字段

字段 含义
m_ids 当前活跃的事务ID集合
min_trx_id 最小活跃事务ID
max_trx_id 即将分配的事务ID,即当前最大事务ID+1
creator_trx_id 创建当前Read View的事务ID

活跃事务:指Read View创建时还未提交的事务。

创建Read View的时机

不同的隔离级别下创建Read View的时机也不同:

  • RC:在事务每次快照读时创建。
  • RR:在事务第一次快照读时创建,后续快照读复用当前Read View。
判断可见性

当事务访问某一行数据时,会遍历其undo log版本链,找到该事务可见的数据版本,trx_id是undo log版本链中的DB_trx_id(创建该版本的事务ID)。

判断规则

条件 是否可见 说明
trx_id == creator_trx_id 可见 该版本是当前事务自己修改的
trx_id < min_trx_id 可见 该版本在Read View创建前就已提交
trx_id >= max_trx_id 不可见 该版本在Read View创建后才创建
trx_id ∈ m_ids 不可见 该版本由Read View创建时未提交的事务修改
trx_id ∉ m_idsmin_trx_id ≤ trx_id < max_trx_id 可见 该版本在Read View创建时已提交

隔离级别的实现原理

事务隔离级别的实现是MVCC和锁机制配合的结果。

涉及到的核心机制

机制 作用 适用的隔离级别
MVCC(undo log+ReadView) 实现非阻塞读快照读),通过版本链提供一致性视图 RC,RR
临键锁(间隙锁+记录锁) 锁定索引间隙和记录,防止插入和修改,解决幻读脏写 RR,S
间隙锁 锁定索引间隙,防止插入,避免幻读 RR,S
行锁(记录锁) 锁定单行索引记录,避免写冲突(脏写) 所有写操作
undolog 事务回滚
读未提交RU

核心特性 :直接读取最新的数据(包括未提交的数据变化)脏读,所以RU的实现不依赖ReadView

  • undo log的表现
    • 读操作直接访问最新的数据版本(包括未提交的修改)。
    • undo log仅用于事务回滚。
  • 锁机制
    • 写操作加排它锁(X锁) ,持锁至事务结束,避免脏写
      • 不会阻止该行的读操作,读操作不会加锁,排它锁只阻塞尝试获取锁的操作。
    • 读操作(包括当前读)不加锁,导致脏读
读已提交RC

核心特性:避免脏读、不可重复读问题、幻读问题、读已提交的最新数据版本。

  • MVCC :
    • 每次快照读创建新的ReadView,保证每次读取的都是最新的已提交版本,快照读在undo log版本链找到事务可见的数据版本(当前快照读时最新已提交的数据版本)。
    • 因为使用ReadView,利用ReadView中关于ReadView创建时的参数(m_ids等)与undolog版本链的事务ID参数(DB_trx_ID)比对,能避免读到活跃事务修改的数据版本,以此避免脏读问题。
    • 会因为每次快照读都创建新的ReadView,每个Readview可见的数据版本可能不同,造成不可重复读的问题。
  • 锁机制
    • 当前读加记录锁 ,持锁至事务结束,锁定当前行,避免其他事物修改该行数据,造成脏写
    • 不加间隙锁 ,会出现幻读
可重复读RR
  • MVCC:

    • 第一次快照读时创建ReadView,该事务内所有快照读会在共用该ReadView在undo log版本链上找到事务可见的数据版本(事务开始时已提交的数据版本),避免脏读和不可重复读
    • 只使用MVCC快照读读取固定的一个数据版本,不会出现幻读问题。
  • 锁机制

    • 当前读使用临键锁,防止幻读和脏写
    • 只使用当前读,或第一次读操作是当前读,会对查询的数据范围加临键锁,即便之后在锁范围内再使用快照读也不会出现幻读问题。但是如果之后的快照读不在锁定范围并且又使用当前读暴露了其他事务的修改,也会出现不可重复读和幻读。
  • 仍存在的幻读问题

    • 快照读当前读混合读

      • 由快照读读取事务开始时的数据版本变成读取最新版本的当前读,且中间有其他事务修改该数据。
      sql 复制代码
      -- 事务 A (RR)
      BEGIN;
      -- 快照读:基于 MVCC 首次 Read View
      SELECT * FROM users WHERE age > 20; -- 返回 2 行 (id=30,40)
      
      -- 事务 B 插入并提交:INSERT INTO users(age) VALUES(25); -- id=50
      
      -- 当前读:直接读取最新数据(绕过 MVCC)
      SELECT * FROM users WHERE age > 20 FOR UPDATE; -- 返回 3 行 (id=30,40,50)
      COMMIT;
      • 快照读更新操作引发数据可见(隐式当前读与快照读混用)

        • 更新使其他事务插入的行可见
        sql 复制代码
        -- 事务A (RR)
        BEGIN;
        SELECT * FROM users WHERE age>20; -- 快照读:返回id=30 (age=30)
        
        -- 事务B:INSERT INTO users(age) VALUES(25); COMMIT;
        
        UPDATE users SET status=1 WHERE age>20; -- 当前读:更新id=30和id=新行
        SELECT * FROM users WHERE age>20;       -- 看到id=30和id=新行
        • 事务与其他事务更新不同列
        sql 复制代码
        -- 事务A (RR)
        BEGIN;
        SELECT * FROM users WHERE id=1; -- 看到(name='A', age=20)
        
        -- 事务B:UPDATE users SET name='B' WHERE id=1; COMMIT;
        
        -- 事务A更新不同列
        UPDATE users SET age=21 WHERE id=1; -- 当前读:基于(name='B', age=20)更新
        SELECT * FROM users WHERE id=1;     -- 看到(name='B', age=21)
        • 其他事务删除数据
        sql 复制代码
        -- 事务A (RR)
        BEGIN;
        SELECT * FROM users WHERE id=1; -- 看到数据
        
        -- 事务B:DELETE FROM users WHERE id=1; COMMIT;
        
        UPDATE users SET age=21 WHERE id=1; -- 0 rows affected(数据已不存在)
        SELECT * FROM users WHERE id=1;     -- 无结果
        • 本事务与其他事务修改不同行
        sql 复制代码
        -- 初始数据:id=1, col1=100, col2=200
        -- 事务A (RR)          | 事务B
        ----------------------|-------------------
        BEGIN;                |
        SELECT col1 FROM t;   | BEGIN;
        --> 100               |
                              | UPDATE t SET col2=300;
                              | COMMIT;
        UPDATE t SET col1=150;|
        SELECT * FROM t;      |
        --> col1=150, col2=300|
    • 原因分析

      • RR 通过事务开始时固定的 ReadView 确保快照读避免不可重复读和幻读。但更新操作(隐式当前读)会绕过 ReadView 直接读取最新数据版本,继承其他事务的修改(包括插入/删除/更新),并将修改后的数据以本事务 ID 写入新版本。这导致:
        1. 若其他事务插入新行且匹配更新条件 → 幻读
        2. 若其他事务更新同一行 → 不可重复读
        3. 若其他事务删除行且尝试更新该行 → 行消失(不可重复读)
    • 解决办法

      • 读操作使用加锁读,也是串行化的解决方案吗,但业务中可考虑上述情况是否会出现。
串行化S
  • MVCC :
    • 禁止快照读,所有读装换为当前读。
  • 锁机制
    • 将普通读操作隐式加**SELECT ... FOR SHARE(共享锁)**。
    • 每次读操作都会对查询范围内的数据行和间隙加临键锁,彻底避免幻读不可重复读

事务原理

Undo Log回滚

前像版本
  • 事务回滚要将数据恢复到前像版本,而前像版本指的是数据行隐藏字段DB_ROLL_PTR指向的undo log版本链的直接前驱版本,从最新的修改开始执行create_trx_id是当前事务id的版本链的反向逻辑就能恢复行数据版本
  • DB_ROLL_PTR指向的版本链中的版本一定是在该版本创建时已提交的事务修改的,mysql的写操作是隐式加锁读(当前读),对同一数据行的写操作事务一定是串行执行的
  • 除了可用于回滚的直接前驱版本,也就是更早版本,依然存在是MVCC给其他未提交且可见此版本的事务用于快照读的。

不同类型的 Undo Log 中旧版本的存储内容和回滚操作

操作类型 存储内容 回滚操作
UPDATE 被修改前行数据的完整版本(含所有字段旧值) 用 undo log 中记录的旧值覆盖当前行数据,恢复 DB_TRX_IDDB_ROLL_PTR 为修改前的状态,撤销字段更新。
DELETE 整行数据的完整版本(含所有字段旧值,相当于特殊更新的旧状态) 清除行的删除标记(DELETE_BIT),用 undo log 中的旧值恢复行数据可见性,DB_TRX_IDDB_ROLL_PTR 回退到删除前的版本。
INSERT 新插入行的完整主键信息(主键值及元数据) 根据主键定位到插入的行,执行物理删除(因插入行未提交,其他事务不可见,删除后无残留)。
回滚核心流程:逆向遍历 undo log 并执行反向操作

回滚过程会从事务的最后一个修改操作开始,逆向遍历事务的 undo log 链表,逐个对每个操作执行 "反向逻辑",直到所有修改被撤销。具体步骤如下:

步骤 1:定位事务的 undo log 链表

InnoDB 通过事务 ID 找到该事务对应的 undo log 链表,链表的 "头节点" 是事务最后一次修改生成的 undo log 记录,"尾节点" 是事务第一次修改生成的 undo log 记录。

步骤 2:从最后一个修改开始逆向处理

回滚按 "逆序" 处理每个 undo log 记录(即先撤销最后执行的操作,再撤销倒数第二个,以此类推),确保数据恢复的正确性。以下按操作类型分述:

场景 1:撤销 INSERT 操作(基于 Insert undo log)

  • undo log 内容:记录了插入行的完整数据(含主键)。
  • 反向操作:根据 undo log 中的主键定位到插入的行,直接删除该行(因为插入的行在事务提交前仅对当前事务可见,删除后其他事务无法感知)。
  • 示例 :事务内执行 INSERT INTO user VALUES (1, '张三'),回滚时通过 Insert undo log 找到 id=1 的行,执行删除。

场景 2:撤销 UPDATE 操作(基于 Update undo log)

  • undo log 内容:记录了被修改行的完整旧版本数据(修改前的所有字段值)。
  • 反向操作 :根据 undo log 中的主键定位到数据行,用旧版本数据覆盖当前版本(即恢复 DB_TRX_ID 为旧版本的事务 ID,DB_ROLL_PTR 指向旧版本的前驱 undo log)。
  • 示例 :事务内先执行 UPDATE user SET age=30 WHERE id=1(原 age=20),回滚时通过 Update undo log 找到 id=1 的行,将 age 恢复为 20,DB_ROLL_PTR 指向修改前的旧版本 undo log。

场景 3:撤销 DELETE 操作(基于 Update undo log)

  • undo log 内容:记录了被删除行的完整旧版本数据(删除前的所有字段值)。
  • 反向操作 :根据 undo log 中的主键定位到被标记删除的行,恢复其数据为旧版本(清除删除标记 delete_flag),并更新 DB_TRX_IDDB_ROLL_PTR 为旧版本信息。
  • 示例 :事务内执行 DELETE FROM user WHERE id=1,回滚时通过 Update undo log 找到 id=1 的行,恢复其数据(取消删除标记),使其可见性恢复到删除前的状态。
初始状态
复制代码
账户表 (accounts)
+----+-------+---------+
| id | name  | balance |
+----+-------+---------+
| 1  | Alice | 1000.00 |
| 2  | Bob   |  500.00 |
+----+-------+---------+
事务操作序列
sql 复制代码
BEGIN;  -- 事务A开始
-- 操作1:Alice转出100
UPDATE accounts SET balance = 900.00 WHERE id = 1;
-- 操作2:Bob转入100
UPDATE accounts SET balance = 600.00 WHERE id = 2;
数据页未持久化

初始状态
脏页 脏页 Buffer Pool Page1: id=1, balance=900 Page2: id=2, balance=600 Undo Log 记录1: id=1, old_balance=1000 记录2: id=2, old_balance=500

回滚流程
回滚线程 Undo Log Buffer Pool 1. 读取操作1前像: id=1, balance=1000 2. 恢复Page1: balance=1000 3. 读取操作2前像: id=2, balance=500 4. 恢复Page2: balance=500 清除脏页标记 回滚线程 Undo Log Buffer Pool

结果

  • 内存数据恢复为每项修改数据的前像
  • 磁盘数据保持 (无需操作)
  • 无磁盘 I/O 发生

数据页全部持久化

初始状态

sql 复制代码
操作1持久化:Page1已刷盘 → id=1, balance=900
操作2持久化:Page2已刷盘 → id=2, balance=600

Undo Log:
  记录1: id=1, old_balance=1000
  记录2: id=2, old_balance=500

回滚流程
回滚线程 Undo Log Buffer Pool Redo Log 后台线程 磁盘 1. 读取操作1前像: balance=1000 2. 恢复Page1内存: balance=1000 3. 生成回滚1的Redo 4. 读取操作2前像: balance=500 5. 恢复Page2内存: balance=500 6. 生成回滚2的Redo 刷写两个回滚Redo 刷Page1 (balance=1000) 刷Page2 (balance=500) loop [异步刷盘] 回滚线程 Undo Log Buffer Pool Redo Log 后台线程 磁盘

关键步骤详解

  1. 内存回滚
    • 立即将内存中的数据恢复为前像值
    • 缓冲池标记为脏页(因为与磁盘不一致)
  2. Redo Log 保护
    • 保证回滚操作本身的持久性
  3. 数据页刷盘
    • 后台线程将恢复后的数据刷到磁盘
    • 刷盘过程依然通过 Doublewrite 防止页断裂
部分数据页持久化

初始状态

sql 复制代码
操作1持久化:Page1已刷盘 → id=1, balance=900
操作2未持久化:Page2在内存 → id=2, balance=600 (脏页)

Undo Log:
  记录1: id=1, old_balance=1000
  记录2: id=2, old_balance=500

回滚流程
回滚线程 Undo Log Buffer Pool Redo Log 后台线程 磁盘 1. 读取操作1前像: id=1, balance=1000 2. 恢复Page1内存: balance=1000 3. 生成回滚1的Redo记录 4. 读取操作2前像: id=2, balance=500 5. 恢复Page2内存: balance=500 刷写回滚Redo记录 刷Page1 (balance=1000) loop [异步刷盘] 回滚线程 Undo Log Buffer Pool Redo Log 后台线程 磁盘

关键步骤详解

  1. 内存回滚
    • 不论是否持久化都将内存中的数据恢复为前像值
    • 已经持久化的数据页将缓冲池标记为脏页(因为与磁盘不一致)
    • 未持久化的数据页将脏页标识去除(与磁盘数据一致,无需刷盘)
  2. Redo Log 保护
    • 保证回滚操作本身的持久性
  3. 数据页刷盘
    • 后台线程将恢复后的数据刷到磁盘
    • 刷盘过程依然通过 Doublewrite 防止页断裂

Redo Log

  • 重做日志 ,记录的是事务提交时数据页的物理修改,在刷新脏页到磁盘中发生错误时或数据库崩溃时,用于数据恢复 ,以实现事务持久性

  • 组成Redo Log Buffer (重做日志缓冲)和Redo Log File(重做日志文件),前者在内存中,后者在磁盘中

  • 脏页 :在执行事务的增删改操作时会先对内存中的Buffer Pool缓冲池进行修改,如果缓冲池中不存在则会由后台线程将数据从磁盘中读出存放在缓冲池中,并对数据进行修改,修改后的数据页就称为脏页(与磁盘中数据不一致)。

  • Redo Log解决的问题:后台线程会在一定时机将脏页刷新到磁盘中,但刷新不是实时的,如果事务已提交并返回成功,但是如果在未成功刷盘时出错或崩溃

    • 导致已提交的事务丢失,事务的持久性就未能保证。
    • 未提交的事务的部分数据页被刷新到磁盘中,导致数据不一致
WAL日志先行
  • 日志先行,所有的数据页修改前必须先将对应的修改记录写入到日志,并保证日志落盘以保证事务的持久性。

  • 工作流程

    • 事务执行阶段

      事务在修改数据页时会同步生成redo log记录(包括表空间ID、页号、偏移量、修改值等物理信息)

      • 物理逻辑日志:记录页级别的物理修改,而非 SQL 语句
      • 实时生成:每条数据修改都立即产生日志
      • 内存缓冲:日志暂存内存,未直接落盘
      • 设计目的:利用内存缓冲避免每次修改都触发磁盘 I/O,大幅提升事务执行效率。

    事务 Buffer Pool Log Buffer 修改数据页(产生脏页) 生成Redo记录(物理逻辑日志) 记录格式: 表空间ID, 页号, 偏移量, 修改值 事务 Buffer Pool Log Buffer

    • 事务提交阶段
      • 事务提交时,根据innodb_flush_log_at_trx_commit参数决定日志的刷盘策略。
        • 策略1(默认安全):立即将 Log Buffer 中的日志刷到磁盘文件
        • 策略2(平衡):仅写入操作系统缓存
        • 策略0(高性能):依赖后台线程异步刷盘
      • 保证事务提交时,相关redo log至少进入操作系统持久化层,满足事务的持久化要求。

    innodb_flush_log_at_trx_commit=1 =2 =0 事务提交 刷盘策略 立即刷Redo Log到磁盘 写OS缓存 等待后台线程刷盘 返回提交成功

    • 后台处理阶段
      • 当日志文件写满 75% 时,触发 Checkpoint(检查点)
      • 将内存中最早的脏页刷入磁盘
      • 更新系统表空间中的 checkpoint_lsn
      • 回收已刷盘日志的存储空间

    日志写满75% 触发Checkpoint 刷脏页到磁盘 推进checkpoint_lsn 回收旧日志空间

    • 崩溃恢复阶段
      • 定位系统表空间中的checkpoint_lsn(最近一次刷盘成功的点)
      • 从LSN开始扫描Redo Log文件
      • Redo重做 :重新应用Redo Log中的所有日志记录,恢复数据页状态。
        • 注意:Redo重做操作并不是直接去修改磁盘上的数据页,而是将redolog记录的修改应用到缓冲池中对应的数据页上。如果缓冲池中没有对应的数据页,则从磁盘读取到缓冲池,然后在缓冲池中应用Redo Log的修改。
      • Undo回滚 :根据Undo Log回滚所有未提交事务的修改(这些事务无法继续完成,回滚保证一致性)。
        • 未持久化修改:恢复内存数据前像,与磁盘数据一致,去除脏页标识。
        • 已持久化修改:恢复内存数据前像,与磁盘数据不一致,标记脏页,添加回滚Redo,刷盘后将数据恢复值前像版本。

    Crash Redo BufferPool Undo Disk 数据库重启 读取checkpoint_lsn 扫描Redo Log 重放日志(LSN1001) 重放日志(LSN1002) 数据页变为事务后状态 进入Undo阶段 读取Undo Log 回滚id=1 (1000.00) 回滚id=2 (500.00) 刷回原始状态 清理事务状态 Crash Redo BufferPool Undo Disk

    • 如果不应用redo log,那么想保证事务的持久性,就要在事务提交时,将所有被该事务修改的脏页同步到磁盘中,这些脏页可能在磁盘中分散的位置,所以同步操作会涉及到大量的随机磁盘IO
    • WAL日志先行的机制下,读数据页的修改会以日志形式记录在redo log buffer,在事务提交时再将日志持久化到redo log文件中,而写入redolog文件的操作是追加写 ,只是一种高效的顺序写IO
    • 在redolog持久化到磁盘后,事务的持久性就已经被保证,即使数据库崩溃也可以依靠redo重放来恢复修改,所以缓冲池中脏页的刷盘就可以是
      • 延迟的
        • 降低提交延迟,用户能更快得到提交成功的响应,
        • 增加合并机会,让后续可能对一个页的修改在缓冲池中合并,最终只刷一次盘。
      • 批量的
        • 分摊磁盘IO开销,一次磁盘IO的时间成本被分摊到了多个数据页上,平均每个页的IO成本降低。
        • 分摊系统调用开销,一次系统调用的成本被多个数据页分摊。
      • 可优化的
        • 操作系统IO调度器,会尝试对批量的请求进行排序(如类似电梯算法 - SCAN或C-SCAN),使磁头移动路径更短,减少随机磁盘IO的性能损耗。

    WAL将随机数据修改转化为顺序日志写入,避免每次修改都触发磁盘 I/O,大幅提升事务执行效率,并且延迟刷盘可以增加脏页修改合并机会。

事务原理实现

原子性 (Atomicity): "要么全做,要么全不做"

  • 核心机制: Undo Log
  • 实现过程:
    • 执行任何修改(INSERT/UPDATE/DELETE ,先在 Undo Log 中记录修改前的数据状态(旧值或反向操作逻辑)。(注意:写入 Undo Log 本身也是一个修改,会被 Redo Log 记录以保证 Undo Log 的持久性)
    • 修改内存中的数据页(产生脏页)。
    • 提交 (Commit):
      • 生成包含 COMMIT 标记的 Redo Log 记录并 强制刷盘 (fsync)(此时持久性已保证)
      • 脏页异步刷盘。
    • 回滚 (Rollback) / 失败:
      • 引擎根据 Undo Log 中的记录,执行逻辑逆操作(如 DELETE 的逆操作是 INSERTUPDATE 是恢复旧值),将数据恢复到事务开始前的状态。
    • 关键点: Undo Log 提供了将事务所有修改"撤销"回去的能力。无论提交还是回滚,事务内的操作被视为一个不可分割的整体。Redo Log 保证了 Undo Log 操作本身的可靠性。

一致性 (Consistency): "数据库总是从一个一致状态转换到另一个一致状态"

  • 核心机制: ACID 共同目标 + 数据库约束 + 应用逻辑
  • 实现过程:
    • 原子性 确保事务边界内的转换是原子的,不会停留在中间不一致状态。
    • 隔离性 防止并发事务看到彼此未完成的不一致修改。
    • 持久性 确保提交的状态是永久的,不会因崩溃丢失导致状态回退。
    • 数据库约束 (主键、外键、唯一、非空、CHECK) :在事务执行过程中(通常在语句级或事务提交时)进行校验。违反约束的操作会被拒绝,触发回滚(依赖 Undo Log)。
    • 应用逻辑:业务规则需要开发者在事务代码中正确实现。
    • 关键点: A、I、D 是实现 C 的基础手段。Undo Log 在回滚违反约束的操作、MVCC 在提供一致性读视图上都对一致性有直接贡献。

隔离性 (Isolation): "并发执行的事务相互隔离,感觉像串行执行"

  • 核心机制: 锁机制 + MVCC (基于 Undo Log)
  • 实现过程:
    • 写-写冲突 (核心:锁机制):
      • 当一个事务要修改某数据项时,必须先获得相应的锁(如行锁、X锁)。
      • 其他事务试图修改同一数据项时会被阻塞(或根据隔离级别报错),直到锁释放。这保证了同一时间只有一个事务能修改特定数据,防止数据被并发写破坏。
      • 例如(Repeatable Read):事务A修改行R时加X锁,事务B尝试修改R会被阻塞直到A提交/回滚释放锁。
    • 读-写冲突 (核心:MVCC + Undo Log):
      • MVCC 基础: 每行数据包含隐藏字段 DB_TRX_ID(最后修改它的事务ID)和 DB_ROLL_PTR(指向该行在 Undo Log 中旧版本记录的指针),形成数据行的版本链
      • 快照读 (非锁定读): 当读操作发生时(在 RC 或 RR 级别下):
        • 系统根据事务启动时刻(或语句开始时刻,取决于隔离级别)生成一个 Read View。Read View 包含当时所有活跃(未提交)事务ID列表。
        • 通过 DB_ROLL_PTR 遍历版本链。
        • 找到满足以下条件的版本:
          • 创建该版本的事务ID < Read View 中最小活跃事务ID (说明该版本在事务开始时已提交)
          • 创建该版本的事务ID 在 Read View 中但等于自身事务ID (说明是自己修改的)
          • 该版本的 DB_TRX_ID 是链中满足上述条件的最大值 (即该事务开始时能看到的最新已提交版本)
        • 读取该版本的数据(存储在 Undo Log 中)。读操作不阻塞写操作,写操作也不阻塞读操作。
      • 例如(Repeatable Read):事务A开始时生成Read View V1。事务B在A之后修改并提交了行R。事务A再次读R时,通过V1和Undo Log链,仍然会读到B修改前的版本(快照)。
    • 关键点: 锁机制 直接处理并发写,强制串行化写操作。MVCC 利用 Undo Log 提供的历史版本,为读操作提供一致性视图,解决了读写冲突,极大提高了并发读性能。不同的隔离级别(RC, RR)主要通过调整 Read View 的生成时机(语句级/事务级)和锁的范围(如 RR 的间隙锁)来实现。Redo Log 保证了 Undo Log 版本链的持久性,支撑 MVCC 在崩溃恢复后仍有效。

持久性 (Durability): "一旦事务提交,修改永久保存"

  • 核心机制: Redo Log + WAL 原则
  • 实现过程:
    • 事务提交时,其产生的所有修改操作(包括数据修改和 Undo Log 的写入)对应的 Redo Log 记录(物理日志),以及一个标识事务提交的 COMMIT 记录 ,必须被 强制刷盘 (fsync) 到持久化存储(Redo Log File)中。这是 WAL 原则的核心要求。
    • 此时,即使系统立即崩溃,这些修改操作已安全保存在磁盘上。
    • 内存中被修改的数据页(脏页)不需要在提交时立即刷盘。数据库会在后台选择合适的时间(Checkpoint 机制),将脏页批量、异步地写回磁盘数据文件。这极大提高了性能(将随机写转化为顺序写 + 延迟批量刷脏页)。
    • 崩溃恢复:
      • 数据库重启时,首先定位到 Redo Log 中最近的 Checkpoint(记录了当时哪些脏页已刷盘)。
      • 从 Checkpoint 开始扫描 Redo Log。
      • 重做 (Redo): 重新执行所有 Checkpoint 之后、日志末尾之前的、且带有 COMMIT 标记 的 Redo Log 记录对应的操作。这确保了所有已提交事务的修改都被重新应用到数据文件。
      • 回滚 (Undo): 对于 Redo Log 中存在但没有 COMMIT 标记的事务(崩溃时未提交的事务),利用 Undo Log 进行回滚(原理同原子性中的回滚),撤销这些未完成事务的修改。
    • 关键点: 强制刷盘 Redo Log (含Commit标记) 是持久性的绝对保证。异步刷脏页是性能优化。崩溃恢复中的 Redo 阶段确保了已提交修改不丢失,Undo 阶段(依赖 Undo Log)保证了未提交修改被清除,共同维护了数据库状态的一致性。Undo Log 本身的写入也受 Redo Log 保护。