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,二者不能因为扩展名相似就混为一谈。
BGREWRITEAOF 的成功回复只表示开始或安排了后台重写,不代表工作已经完成。应结合 INFO persistence 确认重写完成且状态成功。备份时不能只挑一个名字像日志的文件,也不能在重写切换文件期间随意复制目录;要保存彼此一致的 manifest 及其引用的活动文件,才能沿正确顺序恢复。
坏掉的样子
磁盘写入错误、空间耗尽和重写失败都需要告警,配置了 appendonly yes 并不会让这些风险消失。应结合 aof_last_write_status、aof_last_bgrewrite_status、当前重写状态、剩余磁盘空间与应用错误判断健康度。后台任务仍会竞争磁盘,并因写时复制增加内存占用,“后台”不能解释为完全没有前台延迟影响。
日志末尾截断与中部字节损坏的处理语义也不同。检查或修复前应保留原始文件;修复可能丢弃损坏位置之后的命令,不能直接把修复成功当成数据完整。AOF 会忠实保留应用误操作的效果,所以仍需独立留存备份。进程重启成功并不能证明断电安全、复制确认或业务恰好执行一次。
快速验证
准备 POSIX 环境、C 编译器、make、curl、tar 和 Python 3.10 以上版本。本实验已在 macOS、Redis 8.10.1、redis-py 8.1.0、Python 3.14.4 上运行。安装步骤仅以 libc 构建 Redis 核心,并把客户端放入临时虚拟环境,不安装到系统环境。
redis_lab=$(mktemp -d)
curl --fail --location --output "$redis_lab/redis.tar.gz" https://download.redis.io/releases/redis-8.10.1.tar.gz
tar -xzf "$redis_lab/redis.tar.gz" -C "$redis_lab"
make -C "$redis_lab/redis-8.10.1" build redis -j2 MALLOC=libc
export REDIS_SERVER="$redis_lab/redis-8.10.1/src/redis-server"
python3 -m venv "$redis_lab/venv"
"$redis_lab/venv/bin/python" -m pip install redis==8.1.0
在同一个 shell 中,用 "$redis_lab/venv/bin/python" 执行下面的 Python 代码。它创建私有 Unix socket,不开放 TCP,不安排自动快照或自动 AOF 重写;AOF 使用 always,重写期间也不跳过同步。代码只杀死自己创建的子进程,持久化文件会在临时目录内保留到重启验证完成,最后清理该目录,不连接默认 Redis。
from contextlib import contextmanager
import os
from pathlib import Path
import signal
import subprocess
from tempfile import TemporaryDirectory
import time
import redis
from redis.backoff import NoBackoff
from redis.retry import Retry
@contextmanager
def instance(directory, aof):
endpoint = str(Path(directory) / "redis.sock")
process = subprocess.Popen([
os.environ["REDIS_SERVER"], "--port", "0",
"--unixsocket", endpoint, "--unixsocketperm", "700",
"--dir", directory, "--dbfilename", "dump.rdb", "--save", "",
"--appendonly", "yes" if aof else "no", "--appendfsync", "always",
"--appenddirname", "appendonlydir", "--appendfilename", "appendonly.aof",
"--aof-use-rdb-preamble", "yes", "--no-appendfsync-on-rewrite", "no",
"--auto-aof-rewrite-percentage", "0",
], stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
client = redis.Redis(unix_socket_path=endpoint, decode_responses=False,
socket_timeout=2, retry=Retry(NoBackoff(), 0))
try:
deadline = time.monotonic() + 5
while True:
assert process.poll() is None, "Redis exited during startup"
try:
if client.ping():
break
except redis.ConnectionError:
if time.monotonic() >= deadline:
raise
time.sleep(0.02)
yield client, process
finally:
client.close()
if process.poll() is None:
process.terminate()
try:
_, errors = process.communicate(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
process.communicate()
raise
assert process.returncode in (0, -signal.SIGKILL), errors.decode()
def crash(process):
assert process.poll() is None
process.kill()
assert process.wait(timeout=5) == -signal.SIGKILL
with TemporaryDirectory(prefix="redis-aof-", dir="/tmp") as directory:
with instance(directory, aof=True) as (client, process):
client.set("counter", 0)
for expected in range(1, 5):
assert client.incr("counter") == expected
assert client.hset("profile", mapping={"name": "Ada", "level": "2"}) == 2
client.set("deleted", "not retained")
assert client.delete("deleted") == 1
rewrites = client.info("persistence")["aof_rewrites"]
assert client.bgrewriteaof()
deadline = time.monotonic() + 5
while True:
state = client.info("persistence")
if (state["aof_rewrites"] > rewrites
and not state["aof_rewrite_in_progress"]
and not state["aof_rewrite_scheduled"]):
assert state["aof_last_bgrewrite_status"] == "ok"
break
assert time.monotonic() < deadline, "AOF rewrite did not finish"
time.sleep(0.02)
assert client.incr("counter") == 5
assert client.info("persistence")["aof_last_write_status"] == "ok"
assert not (Path(directory) / "dump.rdb").exists()
print("rewrite completed; a later INCR returned 5; no standalone RDB")
crash(process)
aof_dir = Path(directory) / "appendonlydir"
entries = []
for line in (aof_dir / "appendonly.aof.manifest").read_text().splitlines():
parts = line.split()
entries.append(dict(zip(parts[::2], parts[1::2], strict=True)))
active = [entry for entry in entries if entry["type"] in {"b", "i"}]
assert {entry["type"] for entry in active} == {"b", "i"}
for entry in active:
assert (aof_dir / entry["file"]).is_file()
base = next(aof_dir / entry["file"] for entry in active if entry["type"] == "b")
assert base.read_bytes().startswith(b"REDIS")
print("manifest references an RDB-format AOF base and an incremental AOF")
with instance(directory, aof=True) as (client, process):
assert client.get("counter") == b"5"
assert client.hgetall("profile") == {b"name": b"Ada", b"level": b"2"}
assert client.get("deleted") is None
assert client.dbsize() == 2
print("after SIGKILL and restart: counter=5, hash restored, deletion retained")
实验先等待一次确实完成且状态成功的重写,再执行一次额外递增。SIGKILL 之后,manifest 引用 RDB 格式的 AOF 基础文件和增量文件,目录内却没有独立 dump.rdb。重新启动同一目录的 Redis,计数器恢复为五,hash 的两个字段都在,已经删除的键仍不存在。验证字段值和数据类型比只比较键总数更有说服力。
清单检查只针对这里生成的简单文件名,不是通用备份解析器。SIGKILL 跳过正常关闭流程,但操作系统仍然运行,文件系统缓存也仍在。因此,该结果证明的是这条本地 AOF 恢复路径,而不是 everysec 或 no 的实测丢失上界,更不是底层硬件故障时的数据保证。
来源
- Redis 持久化,同步策略、多文件 AOF、恢复与备份边界,已核验 2026-09-08。
- Redis BGREWRITEAOF,安排重写与完成重写的区别,已核验 2026-09-08。
- Redis INFO,持久化状态字段,已核验 2026-09-08。
- Redis 8.10.1 配置,同步策略、重写期间的覆盖选项与文件布局,已核验 2026-09-08。
- Redis 8.10.1 构建说明,仅构建核心与选择分配器,已核验 2026-09-08。