守护“真相之源”:深入理解数据库的预写日志(WAL)与检查点技术

如果说缓存和消息中间件处理的是流量的"流动"问题,那么数据库系统要解决的,则是数据的"存在"问题------即数据的最终正确性与持久性。它是整个系统的"真相之源"(Source of Truth)。

日志技术

在考虑数据库系统的持久性时,关键的考虑因素是如何在数据库中实现数据的持久化。例如,在关系型数据库中,数据被存储在表中,而这些表是通过在文件系统或块系统中的数据结构来实现的。如何保存和维护这些文件,例如在单一文件中存储索引或在多个文件中分别存储索引,这取决于具体的实现方式。

当开始修改或更新表中的数据时,这意味着将开始更改索引。索引需要被更新,同时,存储在内存中的页的数据需要被提交。

那么,什么是数据库中的提交操作呢?

每当数据发生更改时,相应的页会被标记为"脏页"。在将这些脏页写回硬盘时,不仅仅是写回单个更改的值或几个字节的列,而是将整个页写回。这个页的大小由硬盘类型决定,比如是最小的页/块/扇区大小。脏页的写回操作通常是异步进行的,其频率由操作系统调度。

如果写入了大量的数据,那么将数据写回硬盘也需要相当长的时间。这里的成本并不在于将数据写回硬盘所需的时间,而在于如果数据库崩溃,数据没有成功写回硬盘,或者只写回了一部分,那么此时应该如何遵循并坚持ACID事务的原则。

预写日志

为了解决这个问题,可以在数据发生更改时,同时生成一个日志或记录,并将其作为所有更改的真实来源。这种日志被称为预写日志(Write Ahead Log,WAL),它记录了页何时以及如何发生的更改。有了预写日志,那么就可以在内存中保留脏页,因为在提交操作之前,硬盘上已经有一个记录了这些更改的日志。因此,即使页的提交操作失败,也可以通过回放预写日志来恢复页的数据写入和更改操作。

假设需要设计一个键值(Key-Value)类型的内存查询系统,其中每次更新操作只更新一小部分数据(例如某一个键的值)。现在打算利用日志技术来实现该内存查询系统的宕机恢复。与数据库事务不同的是,在这个问题模型中,每个成功的更新操作都会立即生效。这相当于在数据库中,每个事务只包含一个更新操作,并且每次更新操作都会立即提交(Auto commit)。

现在可以按照以下步骤使用预写日志:

1)将更新操作的结果(例如SET K=V,,则记录操作类型SET以及K和V的值)以追加写(Append)的方式写入硬盘的日志文件;

2)根据更新操作修改内存中的数据;

3)返回更新成功的消息。

从预写日志的写入流程可以看出,预写的日志记录了更新操作本身,包括操作类型、涉及的数据等。由于是顺序追加写日志文件,这样可以保证较高的硬盘I/O性能。

使用预写日志进行宕机恢复非常简单,只需要"回放"日志即可。从头读取日志文件中的每次更新操作的结果,用这些结果修改内存中的数据。

从预写日志的宕机恢复流程可以看出,只有写入日志文件的更新结果才能在宕机后恢复。这也解释了为什么在预写日志流程中需要先更新日志文件再更新内存中的数据。如果先更新内存中的数据,那么用户可能立刻就能读到更新后的数据。如果在完成内存修改与写入日志之间发生宕机,那么最后一次更新操作可能无法恢复,但用户可能已经读取到了更新后的数据,这可能导致数据不一致的问题。

检查点

宕机恢复过程的一个挑战是需要回放所有预写日志。如果需要恢复的操作数量巨大,那么这个恢复过程可能会非常耗时。为了解决这个问题,可以引入检查点(Check Point)技术。

检查点执行的过程,涉及设计一种支持快照(Snapshot)的内存数据结构,可以快速的将内存数据生成快照,然后写入检查点日志再持久化到硬盘。

1)向日志文件中记录"Begin Check Point";

2)将内存中的数据生成快照再持久化到硬盘;

3)向日志文件中记录"End Check Point"。

在执行检查点过程中,数据可以继续按照预写日志方式被更新。

基于检查点的宕机恢复流程

1)将硬盘中的快照数据加载回内存;

2)从后向前扫描日志文件,寻找最后一个"End Check Point"日志;

3)从最后一个"End Check Point"日志向前找到最近的一个"Begin Check Point"日志,并回放该日志之后的所有更新操作日志。

快照生成过程,通常采用写时复制(Write-once, copy-on-write)技术。例如,创建一个子进程来执行这些操作时,不立即复制父进程中的内存数据,而是让父子进程共享同一份内存。只有当父进程需要修改某个内存页时,才会复制这个页,然后在复制后的页上进行修改。这样,只有真正需要修改的内存页才会被复制,减少了内存的使用。

未完待续

很高兴与你相遇!如果你喜欢本文内容,记得关注哦!!!

相关推荐
2301_8084143821 分钟前
MySQL表的约束
数据库·mysql
小碗羊肉1 小时前
【MySQL | 第五篇】事务
数据库·mysql
@小柯555m1 小时前
MySql(高级操作符--高级操作符练习(1))
数据库·sql·mysql
bqq198610262 小时前
MySQL分库分表
数据结构·mysql
一直会游泳的小猫2 小时前
Claude Code 连 MySQL:保姆级教程
mysql·mcp·claude code
HalvmånEver2 小时前
MySQL的内置函数
linux·数据库·学习·mysql
小松加哲2 小时前
服务器LVM磁盘内部空闲空间无损扩容+挂载原理+MySQL Binlog自动清理完整实操
运维·mysql·服务器扩容
IT界的老黄牛2 小时前
MySQL 磁盘告警 1.2TB:从衣柜原理到 gh-ost 卧底,一次释放 540GB 的实战复盘
运维·数据库·mysql
deviant-ART2 小时前
MySQL 实战:如何根据 ID 将表 B 的字段更新到表 A
数据库·mysql
qq_283720053 小时前
高并发场景下 Python+MySQL 性能优化最佳实践
python·mysql·性能优化