Redis AOF 的同步策略,以及从基础文件与增量文件恢复状态。

Redis AOF 用速度换持久性

AOF 记录 Redis 的数据变更,供启动时恢复,但选择 AOF 并不等于承诺任何已确认写入都绝不会丢失。持久性取决于 fsync 策略、实际生效的配置、底层存储,以及讨论的是进程退出、操作系统崩溃还是整块磁盘损坏。应该先明确恢复目标,再决定日志策略,不能仅凭“追加日志”这个名称推导保证。 执行、记录与确认不是同一步 Redis 先在内存数据集上执行命令,再把需要持久化的效果传播到 AOF。它不是“每条命令必须先落盘才能修改内存”的执行模型。使用 appendfsync always 时,Redis 在发送回复前同步追加的数据;一起处理的命令可以共用一次写入和 fsync,并不必然每个客户端命令都对应一个独立系统调用。 执行成功、完成同步、客户端收到回复仍是不同的时刻。连接在回复到达前断开,客户端不能据此判断操作没有发生,也不能因为配置了 always 就放心重复执行扣减或计数操作。持久化协议并没有替业务提供重试去重。 AOF 是用于重建状态的表示,不是永久保存所有原始请求的审计流水。Redis 可以转换用于持久化的命令,重写也只保留重建当前数据所需的信息,而非完整历史。因此,已经删除的值并不保证还能从重写后的文件中找回来。 按确认语义选择同步策略 appendfsync everysec 大约每秒安排同步,通常以约一秒的写入暴露窗口换取性能,但不能把它当成任意存储阻塞下都成立的严格丢失上界。appendfsync no 则把刷盘时机交给操作系统;它表示 Redis 不主动请求 fsync,不表示数据永远不会落到磁盘。这两种策略下,一条成功回复都不等于该写入已经进入稳定存储。 appendfsync always 给出更强的本机写入确认约定,同时增加同步开销,影响延迟与吞吐。它仍不能保护磁盘整体丢失、存储栈失信或应用主动误删。还要检查 no-appendfsync-on-rewrite:如果将其打开,后台持久化工作期间会暂停通常的 fsync,即使主要策略写着 always,实际保证也会变弱。检查有效配置比只检查某一个配置项更重要。 重写产物是一组文件 Redis 7 起,AOF 使用 manifest 清单、基础文件和增量文件。启用 aof-use-rdb-preamble 时,基础文件采用 RDB 格式,其后的修改放在增量 AOF 中。这个基础文件属于 AOF 自己的文件集合,不是另外定期生成的 dump.rdb,二者不能因为扩展名相似就混为一谈。 ...

十一月 17, 2027 · 3 分钟 · XB Field Notes
周期性冲刷脏页,从而缩短重启所需的重做日志。

检查点为恢复建立起点

检查点记录一部分已经完成的工作,使数据库恢复不必把整个日志历史都视为待处理内容。具体操作取决于引擎:PostgreSQL 同步检查点要求的数据变更,建立重做起点;SQLite 的 WAL 模式则把符合条件的已提交页面帧从日志转移到主数据库文件。两者都不能简单当作事务提交的另一个名字。 坏掉的样子 已经请求检查点与检查点完整完成,是两个不同观察。SQLite 读者可能持有旧快照,阻止较新的日志帧写入主文件,或者阻止日志重置。被动检查点可以只做当前能够完成的部分,然后正常返回。若只看成功标志,不看日志总帧数与已完成帧数,就会漏掉尚未处理的工作,以及那个仍然阻止完成的读事务。 日志大小也要放在具体上下文中理解。文件可以继续复用原有空间,因此已分配大小不变时,检查点进度仍可能推进。PostgreSQL 完成检查点之后,也可能继续为归档或复制槽保留旧日志。为了让磁盘曲线变小而手动删除日志,会破坏恢复材料。应先核对引擎进度和保留义务,再判断文件大小究竟代表什么。 检查点间隔不是恢复时间的硬上限。重放速度、存储吞吐、写入形态以及实际崩溃位置,都会影响恢复耗时。更频繁的检查点可能减少未来重做,也可能增加平时写流量。把日志放到另一块设备,不会把所有数据页写入也搬过去,更不会使检查点的写盘成本自动消失。参数选择需要对应具体工作负载。 快速验证 把下面内容保存为 checkpoint_lab.py,使用普通的 python3 checkpoint_lab.py 运行,保持断言开启。脚本只使用标准库 SQLite 模块与私有临时文件。本次记录来自 Python 3.14.4 搭配 SQLite 3.53.0。比较结果时应同时查看运行时的引擎版本,因为 Python 版本与实际链接的 SQLite 版本并不是一回事。 import sqlite3 import tempfile from pathlib import Path with tempfile.TemporaryDirectory(prefix="xbcatm-checkpoint-") as directory: path = Path(directory) / "test.db" writer = sqlite3.connect(path, isolation_level=None, timeout=0.1) reader = sqlite3.connect(path, isolation_level=None, timeout=0.1) try: assert writer.execute("PRAGMA journal_mode=WAL").fetchone() == ("wal",) writer.execute("PRAGMA synchronous=FULL") writer.execute("PRAGMA wal_autocheckpoint=0") writer.execute("CREATE TABLE marker (id INTEGER PRIMARY KEY, value INTEGER)") writer.execute("INSERT INTO marker VALUES (1,10)") assert writer.execute("PRAGMA wal_checkpoint(TRUNCATE)").fetchone() == (0, 0, 0) reader.execute("BEGIN") assert reader.execute("SELECT value FROM marker").fetchone() == (10,) writer.execute("UPDATE marker SET value=20 WHERE id=1") assert writer.execute("SELECT value FROM marker").fetchone() == (20,) passive = writer.execute("PRAGMA wal_checkpoint(PASSIVE)").fetchone() blocked = writer.execute("PRAGMA wal_checkpoint(TRUNCATE)").fetchone() print("passive", passive) print("truncate_with_reader", blocked) assert passive[0] == 0 and passive[1] > passive[2] >= 0 assert blocked[0] == 1 and blocked[1] > blocked[2] assert Path(str(path) + "-wal").stat().st_size > 0 assert reader.execute("SELECT value FROM marker").fetchone() == (10,) reader.execute("COMMIT") completed = writer.execute("PRAGMA wal_checkpoint(TRUNCATE)").fetchone() print("truncate_after_reader", completed) assert completed == (0, 0, 0) assert Path(str(path) + "-wal").stat().st_size == 0 assert reader.execute("SELECT value FROM marker").fetchone() == (20,) print("fresh_reader", 20) finally: reader.close() writer.close() 初始截断检查点把值十写入主文件,并重置日志。随后一个读者显式开启事务,读取十;另一个连接在关闭自动检查点之后,把值二十提交。写者立刻就能读到二十,而旧读者仍然需要自己的原始快照。这个顺序用真实连接建立约束,不依赖线程恰好在某个微小时间窗口发生竞争。 ...

十月 17, 2027 · 1 分钟 · XB Field Notes