文章目录
1. 从硬件到软件:数据持久化的底层写入方式
在软件系统中,数据持久化最终都要落盘。但“如何落盘”决定了性能、可靠性和恢复能力。本文将深入底层,剖析几种核心的磁盘写入方式。
在开始前,先建立一个基本的存储层次模型:
数据持久化的核心目标,在于将易失性内存(RAM)中的数据安全、可靠地转移到非易失性磁盘(Disk)中。这一数据搬运路径的设计,直接决定了系统的性能表现与可靠性水平。
2. WAL(预写日志,Write-Ahead Logging)
2.1 工作原理
数据修改时,先将操作日志顺序写入磁盘,再异步更新数据文件。
2.2 关键特点
- 写入方式:顺序写(日志文件是追加的)
- 时机:同步刷盘(事务提交时强制落盘)
- 恢复方式:重放(Replay)日志
2.3 优缺点
| 优点 | 缺点 |
|---|---|
| 顺序写性能极高 | 恢复时可能需要重放大量日志 |
| 崩溃后可完整恢复 | 需要额外的日志管理逻辑 |
| 支持事务的原子性和持久性 |
2.4 典型实现
- PostgreSQL WAL
- MySQL InnoDB Redo Log
- Kafka 的日志段(Log Segment)
- RocksDB / LevelDB 的 WAL
一句话总结:先写日志,再写数据;顺序追加,异步落盘。
3. Checkpoint(检查点)
3.1 工作原理
定期将内存中的脏页(已修改的数据页)批量强制刷写到磁盘,并记录一个“检查点”标记。
3.2 关键特点
- 写入方式:批量随机写 + 顺序写
- 时机:周期性触发(如每隔几分钟),或日志达到阈值时
- 恢复方式:从最近一次完整检查点开始,重放之后的 WAL
3.3 与 WAL 的关系
两者往往配合使用。Checkpoint 保证了恢复时只需重放检查点之后的 WAL,大大缩短了恢复时间。
3.4 典型实现
- PostgreSQL Checkpoint
- MySQL InnoDB Checkpoint
- RocksDB Flush + Compaction
一句话总结:批量刷脏页到磁盘,缩短崩溃恢复时间。
4. Direct I/O(直接 I/O)
4.1 工作原理
绕过操作系统的 Page Cache,数据直接在应用程序和磁盘之间传输。
4.2 关键特点
- 写入方式:绕过内核缓存,直接落盘
- 时机:由应用程序显式控制
- 优势:减少内核态/用户态切换,避免双缓存(应用缓存 + 系统缓存)
4.3 适用场景
- 数据库自身已经有完善缓存管理(如 MySQL InnoDB)
- 需要精确控制 I/O 行为
- 避免 Page Cache 带来的额外内存占用
4.4 典型实现
- 打开文件时使用
O_DIRECT标志(Linux) - MySQL InnoDB 的
innodb_flush_method=O_DIRECT
一句话总结:绕过操作系统缓存,应用直接落盘。
5. mmap(内存映射文件)
5.1 工作原理
将磁盘文件直接映射到进程的虚拟内存地址空间,读写内存即读写文件。
5.2 关键特点
- 写入方式:内存映射 + 操作系统异步刷盘
- 时机:内存写入后,由操作系统在适当时机刷盘
- 优势:简化编程模型(读写文件像操作内存)
5.3 风险
- 系统崩溃可能导致数据丢失(已写入内存但未刷盘的部分)
- 映射大文件时可能消耗大量虚拟地址空间
- 写入性能不可控(依赖操作系统刷盘策略)
5.4 典型实现
- 很多数据库使用 mmap 管理索引文件
- Elasticsearch 的 Lucene 索引
- LevelDB / RocksDB 的部分实现
一句话总结:把文件映射成内存,读写像操作内存一样简单,但依赖操作系统刷盘。
6. 双写与屏障(Double Write & Write Barrier)
6.1 双写缓冲区(Double Write Buffer)
InnoDB 独有的机制,解决 页断裂(Partial Page Write) 问题。
- 问题:数据库以 16KB 为单位写入,但操作系统通常以 4KB 为单位刷盘。写入过程中断电,可能导致 16KB 页只写了部分,造成数据页损坏(Page Corruption)。
- 解决方案:先把修改后的完整页写入一个双写缓冲区(顺序写),然后再写入实际数据文件。
下面用图示说明这个机制如何防止页损坏:
场景:没有双写缓冲区
1. InnoDB 准备写入一个 16KB 的脏页(数据页)
2. 操作系统分 4 次,每次 4KB,将数据写入磁盘
┌───────┬───────┬───────┬───────┐
│ 4KB │ 4KB │ 4KB │ 4KB │
└───────┴───────┴───────┴───────┘
↓ ↓ ↓
写入成功 写入成功 🔌 断电!
3. 结果:磁盘上的 16KB 页只有前 8KB 是新数据,后 8KB 是旧数据
→ 该页变为“撕裂页”,InnoDB 无法识别 → 数据页损坏
场景:有双写缓冲区
1. InnoDB 先把脏页写到内存中的双写缓冲区(一个 2MB 的连续区域)
2. 将双写缓冲区的内容 fsync 刷到磁盘上的双写文件(顺序写入)
→ 此时磁盘上有了该页的一份完整副本(16KB)
3. 再将脏页写入实际的表空间(ibd 文件)数据文件
→ 此时如果发生断电,导致数据文件中的页部分写入(损坏)
4. 恢复过程:
- InnoDB 检查数据页:校验和不一致 → 页损坏
- 从双写文件中找到该页的完整副本(因为双写文件是顺序写,且已完成 fsync)
- 用双写文件中的副本覆盖损坏的数据页 → 恢复完成!
注意: 双写缓冲区不是用来“防止数据丢失”的,而是用来“防止数据页损坏”的。
数据丢失(Data Loss)
- 现象:数据库已提交的事务(COMMIT)丢失了,重启后数据没了。
- 原因:写入操作还在内存(Page Cache 或数据库缓存)中,还没来得及刷到磁盘,断电了。
- 解决方案:WAL(预写日志) + 同步刷盘(fsync)。
数据页损坏(Page Corruption)
- 现象:磁盘上的某个 16KB 数据页读不出来了,或者读到的是“一半旧一半新”的混合数据,导致 InnoDB 无法解析这个页。
- 原因:操作系统以 4KB 为单位刷盘,而 InnoDB 以 16KB 为单位写页。写入过程中断电,16KB 页只写成功了 4KB,导致该页处于“撕裂(Torn)”状态。
- 解决方案:双写缓冲区(Double Write Buffer)。
6.2 Write Barrier(写屏障)
- 作用:确保所有在屏障之前的写操作必须先于屏障之后的写操作完成
- 背景:现代存储设备(SSD、磁盘)有内部缓存和重排序机制,可能导致写操作的顺序不符合预期
- 应用:使用
fsync、fdatasync等系统调用,或特定文件系统标志(如O_SYNC)
一句话总结:双写缓冲区解决页断裂,写屏障保证写入顺序。
7. 多种写入方式综合对比
| 写入方式 | 核心特点 | 适用场景 | 代表技术 |
|---|---|---|---|
| WAL | 顺序写、同步刷盘 | 事务型数据库 | PostgreSQL WAL, MySQL Redo Log |
| Checkpoint | 批量随机写、周期性 | 配合 WAL 缩短恢复时间 | PostgreSQL, MySQL InnoDB |
| Direct I/O | 绕过 Page Cache | 自管理缓存的应用 | MySQL InnoDB (O_DIRECT) |
| mmap | 内存映射文件 | 索引管理、简单读写 | Lucene, LevelDB |
| 双写缓冲区 | 防止页断裂 | 确保数据页完整性 | MySQL InnoDB |
| Write Barrier | 保证写入顺序 | 需要写序保证的日志 | fsync, O_SYNC |
8. 实际应用中的组合策略
以 MySQL InnoDB 为例,展示如何组合这些技术:
| 技术 | 在 InnoDB 中的角色 |
|---|---|
| Redo Log (WAL) | 保证事务持久性,崩溃后重放 |
| Checkpoint | 定期刷脏页,缩短恢复时间 |
| Double Write | 防止页断裂 |
| fsync (Write Barrier) | 确保日志和双写缓冲区落盘 |
| O_DIRECT (Direct I/O) | 数据文件绕过 Page Cache |
9. 总结与选型建议
| 写入技术 | 核心价值 | 一句话记忆 |
|---|---|---|
| WAL | 性能 + 可恢复 | 顺序写日志,异步写数据 |
| Checkpoint | 缩短恢复时间 | 定期批量刷脏页 |
| Direct I/O | 控制权交应用 | 绕过系统缓存 |
| mmap | 简化编程 | 把文件当内存用 |
| 双写缓冲区 | 防止页断裂 | 先写缓冲区,再写数据文件 |
| Write Barrier | 保证写序 | 确保关键写入先完成 |
9.1 选型建议
| 场景 | 推荐组合 |
|---|---|
| 关系型数据库 | WAL + Checkpoint + Double Write + Direct I/O |
| 键值存储(LSM-Tree) | WAL + mmap |
| 日志系统 | WAL + 顺序写 |
| 内存数据库 | WAL(AOF)+ Checkpoint(RDB) |
1958

被折叠的 条评论
为什么被折叠?



