RDB 快照与 AOF 文件集合保存不同的可恢复状态。

RDB 与 AOF:两种保住 Redis 的方式

RDB 与 AOF 保存的是 Redis 状态的不同表示。选择之前应回答两个问题:允许丢失多少近期数据,以及可以接受多长恢复时间?单独一个持久化开关无法回答这些问题。真正需要的是可恢复的文件、明确的写入确认策略,以及验证过具体数据值的恢复流程,而不只是能够启动的进程。 快照记录一个时点,不保证固定间隔 RDB 是紧凑的时点数据集。BGSAVE 派生子进程,先写临时文件,完成后再替换旧快照;父进程继续处理请求,但派生进程、写时复制增加的内存以及磁盘工作仍有成本。SAVE 则同步保存并阻塞其他客户端,本文只把它用于很小的隔离测试数据,不应照搬到繁忙服务上。 例如 save 60 1000 同时要求经过指定时间并达到修改次数条件,不是“无论有多少修改,每六十秒都必定生成一份快照”,也不保证保存能按时成功完成。实际恢复暴露窗口取决于最近一份可用快照的状态有多旧;漏掉或失败的保存会扩大这个窗口。因此,要检查成功保存的记录与结果,不能只看配置中的时间数字。 AOF 改变恢复数据的表示 AOF 记录数据变更,并按 appendfsync 策略同步。everysec 通常暴露大约最近一秒的写入,no 把刷盘时机交给操作系统;always 在有效配置允许的情况下先同步再回复,代价是额外的同步开销。这些策略不保护整块存储设备丢失,也不能保证故障切换时被提升的副本包含每一条已经在原节点落盘的写入。 现代 AOF 是 manifest、基础文件和增量文件组成的集合,基础文件可以使用 RDB 格式。因此,“AOF 启动必定从头回放庞大的文本历史”不准确;重写还会去掉重建当前状态不再需要的历史操作。文件大小、启动耗时、重写开销和请求延迟,应结合实际数据集测量,不能仅凭 RDB 或 AOF 的名称排序。 两者同时启用,不等于自动合并 在没有额外启动覆盖的通常配置下,启用了 AOF 的 Redis 会选择 AOF 恢复,而不是先独立加载 dump.rdb,再把另一份日志叠加上去。AOF 内部的 RDB 格式基础文件属于其自身文件集合。另行保留的 RDB 仍可用于备份和明确选择的恢复操作,但不会自动补齐缺损的 AOF 文件。 ...

六月 15, 2027 · 3 分钟 · XB Field Notes
改动先追加到顺序日志,稍后再真正更新数据文件。

WAL 让提交持久化不必等待所有数据页刷盘

预写日志允许数据库把事务变成持久状态,而不必立即刷出这个事务修改过的每一个表页与索引页。在 PostgreSQL 中,恢复某个页面所需的日志,必须在这个页面写入存储之前先持久化。正常同步提交还会等待提交记录完成刷盘。因此,缓冲区里的表变更与已经持久化的日志记录,沿着不同路径向前推进。 坏掉的样子 WAL 不是一张稍后再执行 SQL 的预约单。执行过程已经修改内存页面,日志记录描述的是恢复动作。有些日志来自最终没有提交的事务。恢复需要结合重做与事务状态,确保一个未完成事务不会仅因部分日志或数据页已经落盘,就变成其他查询可见的提交结果。把“日志存在”当成“事务已经成功”,会误解恢复后数据的可见性。 持久性设置会改变承诺。关闭同步提交后,成功响应可能先于日志持久化,崩溃时最近已经确认的事务因此可能消失。关闭文件同步的风险更大,系统故障后甚至可能留下不一致存储。整页写入解决的是另一类问题:在页面只写入一部分时,提供重新构造页面的材料。这些机制都无法弥补存储虚假确认同步,也不能补回所有持久副本同时丢失的数据。 客户端没有收到回复,又是另一种不确定性。服务端可能已经成功提交,连接随后才断开。恢复后事务仍然存在,客户端却从未看到成功,此时直接重试业务动作可能造成重复。因此,对需要保证业务操作唯一性的场景,应使用稳定操作标识并核对结果,不能把网络失败一概解释成数据库已经回滚。 快速验证 在 Linux 环境中,以非超级操作系统账户运行脚本,并准备 PostgreSQL 18 服务端与客户端程序。本次实际使用 PostgreSQL 18.6。脚本初始化隔离的临时集群,使用私有 Unix 套接字,并显式开启正常持久性设置。立即停止操作只针对它刚创建的集群。因为演练会故意打断数据库进程,应使用完整测试夹具,不要把路径替换成生产数据目录。 #!/bin/sh set -eu umask 077 lab=$(mktemp -d /tmp/xbcatm-crash.XXXXXX) mkdir "$lab/socket" cluster=$lab/data writer_pid='' cleanup() { if test -f "$cluster/postmaster.pid"; then pg_ctl -D "$cluster" -m fast -w stop >/dev/null fi if test -n "$writer_pid"; then wait "$writer_pid" || :; fi } trap cleanup EXIT initdb -D "$cluster" --no-locale --encoding=UTF8 --auth-local=trust --auth-host=reject >/dev/null cat >> "$cluster/postgresql.conf" <<CONF listen_addresses = '' unix_socket_directories = '$lab/socket' port = 56432 fsync = on synchronous_commit = on full_page_writes = on checkpoint_timeout = '1h' CONF pg_ctl -D "$cluster" -l "$lab/server.log" -w -t 15 start >/dev/null export PGHOST=$lab/socket PGPORT=56432 PGDATABASE=postgres PGUSER=$(id -un) export PGCONNECT_TIMEOUT=2 psql -X -v ON_ERROR_STOP=1 -qAt <<'SQL' CREATE TABLE marker (id integer PRIMARY KEY,value text); INSERT INTO marker VALUES (1,'base'); CHECKPOINT; SQL checkpoint_before=$(psql -X -v ON_ERROR_STOP=1 -qAt -c 'SELECT checkpoint_lsn FROM pg_control_checkpoint()') psql -X -v ON_ERROR_STOP=1 -qAt -c "INSERT INTO marker VALUES (2,'committed')" commit_end=$(psql -X -v ON_ERROR_STOP=1 -qAt -c 'SELECT pg_current_wal_lsn()') test "$(psql -X -v ON_ERROR_STOP=1 -qAt -c "SELECT pg_current_wal_flush_lsn()>='$commit_end'::pg_lsn")" = t PGAPPNAME=crash_uncommitted psql -X -v ON_ERROR_STOP=1 -qAt >"$lab/writer.log" 2>&1 <<'SQL' & BEGIN; UPDATE marker SET value='uncommitted' WHERE id=1; SELECT pg_sleep(120); COMMIT; SQL writer_pid=$! attempt=0 while test "$(psql -X -v ON_ERROR_STOP=1 -qAt -c "SELECT EXISTS (SELECT FROM pg_stat_activity WHERE application_name='crash_uncommitted' AND wait_event='PgSleep')")" != t; do attempt=$((attempt+1)) test "$attempt" -lt 100 || { echo 'uncommitted writer was not ready' >&2; exit 1; } sleep 0.1 done test "$(psql -X -v ON_ERROR_STOP=1 -qAt -c "SELECT checkpoint_lsn='$checkpoint_before'::pg_lsn FROM pg_control_checkpoint()")" = t echo committed_wal_flushed_without_new_checkpoint pg_ctl -D "$cluster" -m immediate -w stop >/dev/null if wait "$writer_pid"; then writer_pid='' echo 'interrupted writer unexpectedly succeeded' >&2 exit 1 fi writer_pid='' pg_ctl -D "$cluster" -l "$lab/server.log" -w -t 15 start >/dev/null grep -F 'redo starts at' "$lab/server.log" psql -X -v ON_ERROR_STOP=1 -qAt <<'SQL' SELECT id,value FROM marker ORDER BY id; DO $$ BEGIN IF (SELECT string_agg(id||':'||value,',' ORDER BY id) FROM marker) IS DISTINCT FROM '1:base,2:committed' THEN RAISE EXCEPTION 'crash recovery result mismatch'; END IF; END $$; SQL printf 'lab_directory %s\n' "$lab" 第一行以 base 值完成检查点。随后脚本提交第二行,并核对已经刷出的日志位置覆盖提交之后捕获的位置。另一个后端在事务里修改第一行,然后在提交之前进入休眠。通过统计视图观察该后端真实进入休眠状态,证明这项未完成更新确实执行了,而不是只把一条尚未送达数据库的命令放在客户端等待。 ...

八月 14, 2026 · 2 分钟 · XB Field Notes