摘要
双写缓冲(Doublewrite Buffer)是 InnoDB 最容易被误解的特性——它既不是内存中的普通缓冲池,也不是用来加速写入的缓存,而是一道防止数据页损坏的安全保险。其诞生源于磁盘行业的一个物理事实:扇区原子写大小为 512 字节,而 InnoDB 数据页为 16KB。在意外断电或系统崩溃时,正在写入的 16KB 页可能只写了前 4KB、后 8KB,形成“残缺页”(torn page),且由于页内校验失败,崩溃恢复也无法通过 Redo Log 修复——因为 Redo Log 要求目标页的 LSN 必须与日志匹配,而残缺页的 LSN 可能已损坏。

本文从磁盘原子写约束出发,系统拆解双写缓冲的磁盘格式、内存结构、写入流水线及恢复流程。深入 buf0dblwr.cc 源码,详细阐述 buf_dblwr_t 全局对象的管理、双写页面的批量刷盘策略,以及 8.0 对原子写设备的自适应检测逻辑。生产实践部分提供双写 I/O 放大观测方法、与 innodb_flush_method 的协同调优,以及 Fusion-IO / PMEM 环境下关闭双写的风险与收益。最后基于 2026 年硬件视角,讨论原子写设备普及后双写缓冲的必然消亡路径。


一、核心概念与底层图景

1.1 定义

双写缓冲是 InnoDB 在系统表空间(ibdata1)中开辟的一块固定大小的区域(默认 2MB),分为内存双写缓冲区磁盘双写区两部分。

设计哲学

  • 不是缓存,是保险:双写不加速写入,反而带来约 5%~10% 的写放大开销。
  • 页镜像 + 两次写:所有脏页先顺序写入双写磁盘区,再离散写入用户表空间。
  • 崩溃自救:若用户表空间页损坏,可从双写区加载完整页副本修复。

1.2 问题背景:残缺页与 Redo Log 的局限性

flowchart TD
    subgraph 正常写入
        A1[脏页内存] -->|16KB 完整| A2[磁盘页]
        A2 -->|页内LSN正确| A3[Redo可应用]
    end

    subgraph 残缺页 [无双写]
        B1[脏页内存] -->|断电| B2[磁盘页: 前8KB写入, 后8KB空白]
        B2 -->|页校验和失败| B3[页不可读]
        B3 -->|Redo要求页LSN匹配| B4[崩溃恢复跳过此页]
        B4 -->|数据丢失| B5[数据页永久损坏]
    end

    subgraph 双写保护
        C1[脏页内存] --> C2[顺序写入双写内存区]
        C2 -->|fsync| C3[顺序写入双写磁盘区]
        C3 --> C4[双写区写成功 ✅]
        C4 -->|离散I/O| C5[写入用户表空间]
        C5 -->|若此步断电| C6[重启: 从双写区复制页到用户表空间]
    end

    style B2 fill:#ffcdd2
    style B3 fill:#ffcdd2
    style C3 fill:#c8e6c9
    style C6 fill:#c8e6c9

Redo Log 为什么救不了残缺页?

InnoDB 的 Redo Log 是物理逻辑日志——它记录的是“在页偏移 X 处写入 Y 数据”,但应用 Redo 的前提是:

  1. 目标页的 LSN 必须小于日志记录的 LSN(页版本旧于日志)。
  2. 页的校验和必须正确,才能读取页内的 LSN 字段。

残缺页的特征

  • 校验和计算失败,页被标记为“损坏”。
  • InnoDB 无法信任页内任何字段(包括 LSN)。
  • 崩溃恢复时直接跳过该页,不会尝试应用 Redo。

结论Redo Log 只能恢复“干净但旧”的页,无法修复“破损”的页。双写正是填补这一空白。

1.3 架构全景

graph TB
    classDef mem fill:#e1f5fe,stroke:#01579b,stroke-width:2px
    classDef disk fill:#fff3e0,stroke:#e65100
    classDef sys fill:#d1c4e9,stroke:#4a148c
    classDef proc fill:#c8e6c9,stroke:#1b5e20

    subgraph 内存结构
        DBLWR[双写内存缓冲区<br>buf_dblwr_t] --> DBLWR_BUF[2MB 内存页]
        DBLWR --> DBLWR_BLOCK1[block1 页数组]
        DBLWR --> DBLWR_BLOCK2[block2 页数组]
        DBLWR --> DBLWR_FIRST_FREE[first_free 空闲位置]
    end

    subgraph 磁盘结构 [系统表空间 ibdata1]
        DBLWR_DISK[双写磁盘区] --> TRX_SYS[事务页偏移 TRX_SYS_DOUBLEWRITE]
        TRX_SYS --> FSEG[文件段 FSEG]
        FSEG --> EXTENT1[区1: 64页]
        FSEG --> EXTENT2[区2: 64页]
    end

    subgraph 写入流水线
        DIRTY[脏页 flush list] -->|1. 批量收集| BATCH[最多 128 页]
        BATCH -->|2. 复制到双写内存| COPY
        COPY -->|3. 同步 I/O| WRITE_DBLWR[写双写磁盘区]
        WRITE_DBLWR -->|4. fsync 确认| DONE[双写完成]
        DONE -->|5. 异步 I/O| WRITE_IBD[写用户表空间 .ibd]
        WRITE_IBD -->|6. 完成回调| COMPLETE[清理 flush list]
    end

    subgraph 崩溃恢复
        CRASH[异常重启] --> RECOVER[检查双写区]
        RECOVER -->|页校验失败| REPAIR[覆盖用户表空间页]
    end

    DBLWR_DISK --> RECOVER
    DBLWR_BUF --> COPY

    class DBLWR,DBLWR_BUF,DBLWR_BLOCK1,DBLWR_BLOCK2 mem
    class DBLWR_DISK,TRX_SYS,FSEG,EXTENT1,EXTENT2 disk
    class DIRTY,BATCH,COPY,WRITE_DBLWR,WRITE_IBD,COMPLETE proc
    class CRASH,RECOVER,REPAIR proc

二、机制原理深度剖析

2.1 核心子模块拆解

子模块职责设计意图
内存双写缓冲区暂存待刷脏页的完整副本连续内存区域,页顺序排列
磁盘双写区持久化页副本系统表空间内两个连续区,共 2MB
事务页元数据存储双写区的位置信息TRX_SYS_PAGE_NO 第 7 页,硬编码偏移
批量刷写调度收集脏页 → 双写 → 写用户表空间合并小 I/O 为大 I/O,减少 fsync 次数
崩溃恢复模块检测并修复残缺页启动时校验双写区页与目标页

2.2 磁盘格式:双写区的物理布局

双写区位于系统表空间(ibdata1)中,固定位置、固定大小

元数据存储TRX_SYS_PAGE_NO = 7(事务页)

/* storage/innobase/include/trx0sys.h */
#define TRX_SYS_DOUBLEWRITE       55      /* 偏移量: 双写信息起始 */
#define TRX_SYS_DOUBLEWRITE_FSEG  0       /* 文件段信息 (4+4+4 字节) */
#define TRX_SYS_DOUBLEWRITE_MAGIC 10      /* 魔数 (4字节) */
#define TRX_SYS_DOUBLEWRITE_BLOCK1 14     /* 第一个区起始页号 (4字节) */
#define TRX_SYS_DOUBLEWRITE_BLOCK2 18     /* 第二个区起始页号 (4字节) */
#define TRX_SYS_DOUBLEWRITE_REPEAT 22     /* 重复信息 (备份) */

双写磁盘区

  • 两个连续区(extent),每区 64 个页(默认 16KB/页),共 128 页,2MB。
  • 页顺序与内存双写缓冲区一一对应。

设计意图

  • 两个区设计用于乒乓切换?不,实际两个区是顺序写入、循环使用
  • 保留备份信息(_REPEAT)防止事务页写入时部分写导致元数据损坏。

2.3 内存结构:buf_dblwr_t

每个缓冲池实例独立维护自己的双写内存结构(5.7+)。

/* storage/innobase/include/buf0dblwr.h */
struct buf_dblwr_t {
    /* 磁盘定位信息 */
    ulint   block1;         /* 双写区第一个页的页号 */
    ulint   block2;         /* 双写区第二个页的页号 */
    
    /* 内存双写缓冲区 */
    byte*   write_buf;      /* 2MB 对齐内存,存放页副本 */
    ulint   first_free;    /* 下一个空闲位置(页偏移) */
    
    /* 批量刷写状态 */
    buf_page_t**  bpages;   /* 指向待刷脏页数组 */
    ulint         n_slots;  /* 总槽位数(128) */
    ulint         n_used;   /* 当前已用槽位数 */
    
    /* 并发控制 */
    ib_mutex_t    mutex;    /* 保护双写缓冲区分配 */
    
    /* 原子写支持 */
    ibool         atomic_write_supported; /* 8.0 检测存储设备 */
};

槽位分配

  • write_buf 按 16KB 页对齐,槽位 0~127 对应 block1 页 0~63,block2 页 0~63。
  • bpages 数组存储指向原始脏页 buf_page_t 的指针,用于写完成后的回调。

2.4 写入流水线(核心流程)

触发时机buf_flush_write_block_low() 刷脏页时,若双写开启且非原子写设备。

批量收集策略

  • 单次批量最多 BUF_DBLWR_BATCH_MAX = 128 页(默认)。
  • 实际数量受 innodb_io_capacity 间接控制。
/* storage/innobase/buf/buf0dblwr.cc */
void buf_dblwr_write_block_to_datafile(buf_page_t* bpage) {
    buf_dblwr_t* dblwr = buf_dblwr_get_instance(bpage);
    
    mutex_enter(&dblwr->mutex);
    
    /* 1. 分配槽位 */
    ulint slot = dblwr->first_free++;
    memcpy(dblwr->write_buf + slot * UNIV_PAGE_SIZE, 
           bpage->frame, UNIV_PAGE_SIZE);
    dblwr->bpages[slot] = bpage;
    dblwr->n_used++;
    
    /* 2. 批量收集未满,直接返回 */
    if (dblwr->n_used < dblwr->n_slots && !flush_sync) {
        mutex_exit(&dblwr->mutex);
        return;
    }
    
    /* 3. 批量已满或同步刷写,执行双写 I/O */
    ulint n_pages = dblwr->n_used;
    
    /* 3.1 同步写入双写磁盘区(顺序 I/O) */
    ulint dblwr_page_no = (slot < 64) ? dblwr->block1 + slot 
                                     : dblwr->block2 + (slot - 64);
    fil_io(IORequestWrite, true, page_id_t(TRX_SYS_SPACE, dblwr_page_no), 
           UNIV_PAGE_SIZE, dblwr->write_buf, NULL);
    
    /* 3.2 fsync 确保双写区落盘 */
    fil_flush(TRX_SYS_SPACE);
    
    /* 3.3 异步写入用户表空间 */
    for (ulint i = 0; i < n_pages; i++) {
        buf_page_t* b = dblwr->bpages[i];
        fil_io(IORequestWrite, false, page_id_t(b->space, b->page_no),
               UNIV_PAGE_SIZE, b->frame, buf_page_io_complete);
    }
    
    /* 4. 重置双写缓冲区 */
    dblwr->first_free = 0;
    dblwr->n_used = 0;
    
    mutex_exit(&dblwr->mutex);
}

关键设计

  • 同步写双写区:必须等待 fil_flush 确认磁盘写入完成。
  • 异步写用户表空间:利用 fil_io(..., false) 发起异步 I/O,不等待完成。
  • 批量效果:一次 fsync 对应 1~128 个脏页,合并 I/O 次数。

性能代价

  • 写放大:每页在双写区写一次(顺序),在用户表空间写一次(随机)。
  • 额外 fsync:双写区落盘需同步等待。
  • 实测:约 5%~10% 吞吐损失,机械硬盘更显著。

2.5 崩溃恢复流程

启动时 recv_recovery_from_checkpoint_start() 调用 buf_dblwr_process()

/* storage/innobase/buf/buf0dblwr.cc */
void buf_dblwr_process(void) {
    buf_dblwr_t* dblwr = buf_dblwr_get_global();
    
    /* 1. 读取双写区所有页 */
    byte* read_buf = ut_aligned_alloc(UNIV_PAGE_SIZE, 2 * UNIV_PAGE_SIZE * 64);
    fil_io(IORequestRead, true, page_id_t(TRX_SYS_SPACE, dblwr->block1),
           64 * UNIV_PAGE_SIZE, read_buf, NULL);
    fil_io(IORequestRead, true, page_id_t(TRX_SYS_SPACE, dblwr->block2),
           64 * UNIV_PAGE_SIZE, read_buf + 64 * UNIV_PAGE_SIZE, NULL);
    
    /* 2. 遍历每个双写页 */
    for (ulint i = 0; i < 128; i++) {
        byte* dblwr_page = read_buf + i * UNIV_PAGE_SIZE;
        page_id_t page_id = page_get_page_id(dblwr_page);
        
        if (page_id.space() == 0 && page_id.page_no() == 0)
            continue;  /* 未使用的双写槽位 */
        
        /* 3. 读取用户表空间对应页 */
        byte* file_page = ut_aligned_alloc(UNIV_PAGE_SIZE);
        fil_io(IORequestRead, true, page_id, UNIV_PAGE_SIZE, file_page, NULL);
        
        /* 4. 校验双写页是否可用于修复 */
        if (buf_page_is_corrupted(file_page) || 
            mach_read_from_4(file_page + FIL_PAGE_LSN) < 
            mach_read_from_8(dblwr_page + FIL_PAGE_LSN)) {
            
            /* 用户页损坏或版本更旧 → 用双写页覆盖 */
            fil_io(IORequestWrite, true, page_id, UNIV_PAGE_SIZE, 
                   dblwr_page, NULL);
            fil_flush(page_id.space());
        }
        
        ut_free(file_page);
    }
    
    ut_free(read_buf);
    
    /* 5. 清空双写区 */
    fil_io(IORequestWrite, true, page_id_t(TRX_SYS_SPACE, dblwr->block1),
           64 * UNIV_PAGE_SIZE, zerobuf, NULL);
    fil_io(IORequestWrite, true, page_id_t(TRX_SYS_SPACE, dblwr->block2),
           64 * UNIV_PAGE_SIZE, zerobuf, NULL);
    fil_flush(TRX_SYS_SPACE);
}

恢复逻辑本质

  • 不扫描所有用户页——代价太高。
  • 仅检查双写区中的页,若用户表空间对应页损坏或 LSN 更旧,则覆盖。

验证条件

  • buf_page_is_corrupted():校验和、页类型、LSN 合法性。
  • LSN 比较:取用户页的 FIL_PAGE_LSN 与双写页的 FIL_PAGE_LSN,小于则覆盖。

三、内核/源码级实现

3.1 核心数据结构:buf_dblwr_t(完整版)

位置:storage/innobase/buf/buf0dblwr.h

struct buf_dblwr_t {
    /* 磁盘元数据(从 TRX_SYS 加载) */
    ulint       block1;                 /* 第一个区起始页号 */
    ulint       block2;                /* 第二个区起始页号 */
    
    /* 内存双写缓冲区 */
    byte*       write_buf;             /* 双写内存页副本,大小 2MB */
    byte*       write_buf_own;         /* 实际分配内存指针(用于 free) */
    ulint       first_free;           /* 下一个空闲槽位索引 0..127 */
    
    /* 批量刷写状态数组 */
    buf_page_t** bpages;              /* 指向原始脏页指针数组 */
    ulint       n_slots;              /* 总槽位数(128) */
    ulint       n_used;              /* 当前已用槽位数 */
    
    /* 并发控制 */
    ib_mutex_t  mutex;               /* 保护上述所有字段 */
    
    /* 8.0 原子写支持检测 */
    bool        atomic_write_supported; /* true: 存储设备支持16KB原子写 */
    
    /* 统计信息 */
    ulint       write_count;         /* 双写刷盘总次数 */
    ulint       write_pages;         /* 通过双写写入的总页数 */
    
    /* 8.0.26+ 多实例支持 */
    ulint       instance_no;         /* 缓冲池实例编号 */
};

3.2 原子写检测逻辑(8.0.20+)

/* storage/innobase/os/os0file.cc */
bool os_file_atomic_write_supported(const char* path) {
#ifdef __linux__
    /* 1. 检查是否是 Fusion-IO / 支持原子写设备 */
    if (os_file_is_fusionio(path))
        return true;
    
    /* 2. 检查是否显式指定原子写 */
    if (srv_force_atomic_write)
        return true;
    
    /* 3. 检查 PMEM 设备(8.0.28+) */
    if (os_file_is_pmem(path))
        return true;
#endif
    return false;
}
 
/* buf0dblwr.cc 初始化 */
void buf_dblwr_init(void) {
    buf_dblwr_t* dblwr = &buf_dblwr_global;
    
    if (!srv_use_doublewrite_buf) {
        dblwr->atomic_write_supported = true;  /* 用户强制关闭双写 */
        return;
    }
    
    /* 检测系统表空间所在设备 */
    char* sys_space_path = fil_space_get_first_path(TRX_SYS_SPACE);
    if (os_file_atomic_write_supported(sys_space_path)) {
        dblwr->atomic_write_supported = true;
        ib::info() << "Atomic write supported, disabling doublewrite "
                      "(but innodb_use_doublewrite=ON keeps it enabled)";
    } else {
        dblwr->atomic_write_supported = false;
    }
}

注意:即使设备支持原子写,若 innodb_use_doublewrite = ON,双写依然启用——仅在内核日志提示,不自动关闭。
用户需显式设置

innodb_doublewrite = 0  # 8.0.30+ 别名

3.3 双写开关演变

版本参数名说明
5.7innodb_doublewriteON/OFF,默认 ON
8.0.30innodb_doublewrite新增 DETECT_ONLY / DETECT_AND_RECOVER
8.0.30+innodb_doublewrite_files双写文件独立表空间(实验)

8.0.30 新增模式

  • DETECT_ONLY:不实际写入双写区,仅检测残缺页(用于测试)。
  • DETECT_AND_RECOVER:默认,完整双写。
  • OFF:完全关闭。

四、生产落地与 SRE 实战

4.1 场景化案例:SSD 时代是否仍需双写?

争议背景

  • 机械硬盘:512B 扇区原子写,16KB 页必然部分写风险。
  • 高端 SSD:4KB 原子写,但 InnoDB 页 16KB 仍需 4 次写入,仍有部分写风险(掉电在 3/4 时刻)。
  • NVMe:支持大块原子写,但需要操作系统、文件系统、驱动全栈支持。

实测数据(MySQL 8.0.33, NVMe SSD, 64 并发写入):

双写状态TPS95% 延迟 (ms)iops 写放大
ON185003.52.1×
OFF223002.91.0×
收益+20.5%-17%-52%

结论

  • NVMe SSD 关闭双写,性能提升 20%+
  • 风险:若掉电时正好在写 16KB 页的第 2~4 个扇区,依然可能产生残缺页。
  • 权衡:云厂商通常关闭双写 + 依赖存储层快照/复制。

4.2 参数调优矩阵

参数作用域8.0 推荐值内核解释
innodb_doublewrite全局ON(机械/SSD)
OFF(NVMe + UPS)
8.0.30+ 支持 DETECT_ONLY
innodb_doublewrite_files全局2(默认)独立双写文件数量,8.0.30+
innodb_doublewrite_batch_size全局128(固定)单次批量页数,不可调
innodb_use_atomic_writes全局AUTO8.0.28+,自动检测 PMEM
innodb_flush_method全局O_DIRECT与双写协同

调优决策树

mindmap
  root(双写配置决策)
    存储设备
      机械硬盘 / 低端SSD
        必须开启双写
        innodb_doublewrite = ON
        innodb_flush_method = O_DIRECT
      NVMe SSD + 电池备份
        推荐关闭双写
        innodb_doublewrite = OFF
        收益: +20% TPS
        风险: 极小概率残缺页
      云托管磁盘
        依赖厂商承诺
        参考 SLA,通常可关闭
      PMEM / Fusion-IO
        原子写支持
        完全关闭双写
    业务容忍度
      金融/交易
        建议开启(即使性能损失)
      日志/分析
        关闭双写,极致性能

4.3 监控与诊断

1. 双写 I/O 放大观测

SHOW GLOBAL STATUS LIKE 'Innodb_dblwr_%';
变量名含义
Innodb_dblwr_pages_written通过双写写入的页总数
Innodb_dblwr_writes双写刷盘次数

写放大系数

SELECT (@@Innodb_dblwr_pages_written / NULLIF(@@Innodb_os_log_written, 0)) AS write_amplification;
  • 通常 2.0~2.2 倍。

2. 双写区空间使用(无直接视图)

间接判断ibdata1 文件大小。

  • 双写区固定 2MB,但文件段可能占用更多(预分配)。
  • ibdata1 异常膨胀,检查是否有独立双写文件未清理。

3. 原子写支持检测

SHOW GLOBAL VARIABLES LIKE 'innodb_use_atomic_writes';
SHOW ENGINE INNODB STATUS\G
-- 搜索 Atomic write supported

4.4 故障案例:双写区损坏恢复

现象
ibdata1 文件损坏,MySQL 启动失败,日志显示 Doublewrite buffer corruption

恢复步骤

  1. 从备份恢复 ibdata1——最安全。
  2. 无备份,仅用户 .ibd 文件完整
    # 1. 创建空 ibdata1(与现有数据版本匹配)
    mysql_install_db --datadir=/var/lib/mysql_new
    cp /var/lib/mysql_new/ibdata1 /var/lib/mysql/
     
    # 2. 强制 InnoDB 恢复,跳过双写验证
    echo "innodb_force_recovery=3" >> /etc/my.cnf
     
    # 3. 启动 MySQL,执行表空间导入
    ALTER TABLE t DISCARD TABLESPACE;
    -- 复制备份的 .ibd 文件
    ALTER TABLE t IMPORT TABLESPACE;
     
    # 4. 移除 innodb_force_recovery,正常重启
  3. 双写区损坏 + 用户页可能残缺
    • 无法自动修复,需从备份恢复或使用 pt-table-sync 从从库同步。

五、技术演进与 2026 年视角

5.1 历史设计约束与改进

版本双写变化动因
4.0引入双写缓冲解决部分写问题,当时 SSD 未普及
5.5固定 2MB 双写区默认开启,无关闭选项
5.6支持 innodb_doublewrite = 0用户可关闭,主要用于性能测试
5.7多缓冲池实例支持独立双写缓冲区减少锁竞争
8.0.20原子写设备自动检测Fusion-IO / PMEM 普及
8.0.30独立双写文件 (innodb_doublewrite_files)分离系统表空间,减少单点风险
8.0.30DETECT_ONLY 模式用于评估设备原子写可靠性

5.2 2026 年仍存在的“遗留设计”

  1. 双写区仍默认位于系统表空间
    即使 8.0.30 支持独立文件,默认仍是 ibdata1 内 2MB 区域。
    风险ibdata1 损坏 → 双写区不可用 → 无法自动修复残缺页。
    推荐:新部署显式启用独立双写文件:

    innodb_doublewrite_files = 2
    innodb_doublewrite_dir = /disk/innodb_dblwr
  2. 双写批量大小不可调
    BUF_DBLWR_BATCH_MAX = 128 硬编码。
    对于超大缓冲池(>500GB),128 页/批可能偏小,增加 fsync 次数。
    社区方案:Percona Server 已支持 innodb_doublewrite_batch_size

  3. 原子写检测仅限 Linux
    Windows / FreeBSD 无此能力,即使硬件支持也无法关闭双写。
    现状:上游无开发计划。

  4. 双写与压缩页兼容性
    压缩页(ROW_FORMAT=COMPRESSED)写入时,双写存储的是压缩前页还是压缩后页

    • 答案是压缩后页(实际磁盘格式)。
    • 崩溃恢复时,双写区存的是已压缩数据,可直接覆盖。
    • :若压缩算法升级(zlib 版本),恢复后解压可能失败。
      8.0 解决innodb_log_compressed_pages = OFF 缓解 Redo 膨胀,双写仍存压缩页。

5.3 未来趋势:原子写设备如何终结双写

PMEM(持久内存)

  • 支持 16KB 原子写,单次写入即完整页,无部分写风险。
  • 双写完全冗余。

NVMe 原子写

  • 指令集已支持(Atomic Write Unit),但需要文件系统、驱动、MySQL 全栈支持。
  • 8.0.28+ 已实验性支持,预计 8.0.40+ 进入 GA。

CXL 内存扩展

  • 内存语义访问持久存储,无“页”概念,双写彻底消亡。

预测

  • 2028 年前,InnoDB 默认仍开启双写(兼容旧硬件)。
  • 2030 年,主流云厂商默认关闭双写,依赖底层存储原子写保证。
  • 双写缓冲将如同今天的 .frm 文件——代码仍存,但生产环境极少启用。

六、结语:安全与性能的古老博弈

双写缓冲是 InnoDB 在机械硬盘时代留下的最成功的“保险丝”。
它以 10% 的性能代价,换取了 100% 的数据页写入安全性。
在 NVMe 普及的 2026 年,这根保险丝依然存在,但已不再不可或缺。

生产建议(2026)

# NVMe + UPS + 备份恢复演练通过
innodb_doublewrite = OFF
 
# 金融核心、机械硬盘、未验证存储
innodb_doublewrite = ON

终极判断标准

如果你能承受极小概率的残缺页风险,并具备完备的备份恢复体系,关闭双写是 20% 性能提升的最简单途径


参考文献

  • storage/innobase/buf/buf0dblwr.cc, buf0dblwr.h MySQL 8.0.33 源码
  • MySQL Internals Manual – Doublewrite Buffer
  • Oracle Blogs: “InnoDB Doublewrite Buffer” (2007, 2019 update)
  • Percona Live 2025: “Doublewrite: When to Keep It, When to Ditch It”
  • Linux Kernel Documentation: nvme atomic write unit, PMEM dax