部分写问题(Partial Page Write)

问题

页大小16KB > 磁盘块大小4KB,写入过程中系统崩溃,导致页损坏。

写入16KB页(4个磁盘块):
┌──────┬──────┬──────┬──────┐
│ 4KB  │ 4KB  │ 4KB  │ 4KB  │
└──────┴──────┴──────┴──────┘
   ✅     ✅     ❌     ❌
  已写入  已写入  未写入  未写入
        ↑ 系统崩溃

结果:页损坏(部分写)

redo log无法恢复

  • redo log记录的是页的逻辑修改(如"将id=1的name改为Alice")
  • 如果页本身损坏,无法应用redo log

Double Write Buffer解决方案

原理

先写双写缓冲区(连续空间),再写数据文件(离散空间)

写入流程:
1. 脏页 → Double Write Buffer(共享表空间,连续2MB)
2. Double Write Buffer → 磁盘(刷盘,原子操作)
3. 脏页 → 数据文件(离散写入)

崩溃恢复:
IF 数据文件页损坏 THEN
    从Double Write Buffer恢复页
ELSE
    使用redo log恢复
END IF

Double Write Buffer结构

1. 内存结构

Buffer Pool
  ├─ 脏页1(16KB)
  ├─ 脏页2(16KB)
  └─ ...

Double Write Buffer(内存,2MB)
  ├─ 页1(16KB)
  ├─ 页2(16KB)
  ├─ ...
  └─ 页128(16KB)

2. 磁盘结构

共享表空间(ibdata1)
┌─────────────────────────────────┐
│ 数据字典                           │
├─────────────────────────────────┤
│ Double Write Buffer(2MB,连续)   │  ← 双写缓冲区
│   ├─ 区1(1MB = 64页)            │
│   └─ 区2(1MB = 64页)            │
├─────────────────────────────────┤
│ Undo Log                         │
└─────────────────────────────────┘

写入流程

1. 正常写入

1. 脏页刷新触发
   ├─ Buffer Pool中的脏页批量刷新(如128个页)

2. 写入Double Write Buffer(内存)
   ├─ 128个页复制到Double Write Buffer

3. 写入共享表空间(磁盘,顺序写)
   ├─ fsync刷盘(原子操作)
   ├─ 2MB连续空间,1次IO

4. 写入数据文件(磁盘,随机写)
   ├─ 128个页分别写入各自的表空间文件
   ├─ 随机IO,多次寻址

5. 清空Double Write Buffer(内存)

关键

  • 步骤3(Double Write Buffer刷盘)是原子操作
  • 步骤4(数据文件写入)可能部分写

2. 崩溃恢复

-- MySQL启动时
1. 读取Double Write Buffer(共享表空间)

2. 对比Double Write Buffer和数据文件
   FOR EACH  IN Double Write Buffer DO
       IF 数据文件对应页损坏(校验和不匹配) THEN
           Double Write Buffer恢复页
       ELSE
           使用redo log恢复
       END IF
   END FOR

3. 应用redo log,重做已提交事务

性能影响

1. 写放大

写入1个16KB页:
- 写Double Write Buffer:16KB(顺序写)
- 写数据文件:16KB(随机写)
- 总共:32KB(写放大2倍)

2. 顺序写优化

批量刷新128个页:
- 写Double Write Buffer:2MB(1次顺序IO)
- 写数据文件:2MB(128次随机IO)

顺序写速度 >> 随机写速度
→ 总体性能影响较小(约5-10%)

配置

1. 查看状态

-- 查看Double Write开关
SHOW VARIABLES LIKE 'innodb_doublewrite';
-- ON(默认开启,强烈推荐)

2. 关闭Double Write(不推荐)

-- 关闭(仅用于特殊场景)
# my.cnf
[mysqld]
innodb_doublewrite = 0

-- 适用场景:
1. 使用SSD + 文件系统原子写支持(如XFSext4
2. 主从复制从库(可从主库重建)
3. 临时测试环境

-- 风险:
页损坏无法恢复,数据丢失!

Double Write vs redo log

特性Double Write Bufferredo log
作用防止页损坏(部分写)崩溃恢复(持久性)
记录内容完整的数据页(16KB)页的逻辑修改(如"id=1改为2")
恢复方式直接复制页到数据文件应用逻辑修改到页
依赖关系前提:页完整前提:页完整(Double Write保障)
写入位置共享表空间(连续2MB)ib_logfile0/1(循环写)

关系

  • Double Write保证页完整
  • redo log在页完整的基础上恢复数据

监控

1. 查看统计信息

-- 查看Double Write统计
SHOW GLOBAL STATUS LIKE 'Innodb_dblwr%';

-- 关键指标
Innodb_dblwr_pages_written : 1000000  -- 写入Double Write的页数
Innodb_dblwr_writes        : 10000    -- Double Write写入次数

-- 每次写入页数
pages_per_write = Innodb_dblwr_pages_written / Innodb_dblwr_writes
                = 1000000 / 10000
                = 100/次(批量刷新)

2. 性能影响评估

-- 测试关闭Double Write的性能影响
1. 记录当前写入TPS
2. SET GLOBAL innodb_doublewrite = OFF;
3. 运行1小时,记录TPS
4. 计算性能差异

-- 通常影响:5-10%

实战建议

1. 保持开启(强烈推荐)

# my.cnf
[mysqld]
innodb_doublewrite = 1  # 默认开启

-- 理由:
1. 性能影响小(5-10%)
2. 数据安全性高(防止页损坏)
3. 生产环境必备

2. 特殊场景关闭

-- 场景1:从库(可从主库重建)
innodb_doublewrite = 0

-- 场景2:临时测试环境
innodb_doublewrite = 0

-- 场景3:SSD + 原子写支持
innodb_doublewrite = 0
# 但仍有风险,不推荐

3. 监控写入频率

# Prometheus监控
rate(innodb_dblwr_writes_total[5m])

# 告警规则(写入频率过高,影响性能)
rate(innodb_dblwr_writes_total[5m]) > 1000

常见面试题

Q1: 什么是部分写问题?

  • 页大小16KB > 磁盘块4KB,写入过程中崩溃,导致页损坏

Q2: Double Write Buffer如何解决部分写?

  • 先写连续的Double Write Buffer(原子操作),再写离散的数据文件
  • 崩溃时从Double Write Buffer恢复损坏的页

Q3: Double Write和redo log的区别?

  • Double Write:防止页损坏,记录完整页
  • redo log:崩溃恢复,记录逻辑修改

Q4: 关闭Double Write的风险?

  • 页损坏无法恢复,数据丢失

小结

部分写问题:页大小 > 磁盘块,崩溃导致页损坏 ✅ Double Write:先写连续空间,再写离散空间 ✅ 性能影响:5-10%(批量顺序写优化) ✅ 建议:生产环境保持开启

Double Write Buffer是InnoDB数据一致性的重要保障。


📚 相关阅读

  • 下一篇:《MySQL启动与关闭流程》
  • 推荐:《redo log:崩溃恢复的保障》