Redis 实战(二):宕机会丢多少数据 —— kill -9 与真断电下 RDB / AOF 逐个实测(6.2.4)

这一篇讲什么

「Redis 宕机会不会丢数据」的标准答案是:RDB 丢上次快照以后的、AOF everysec 最多丢 1 秒、always 不丢。这一篇把这几句话放到真机上验一遍,并且把进程被杀 和机器断电分开测 ------ 这两种情况的结果差别很大。

另外几个经常一起被问的问题,也都实测了:

  • 明明有 dump.rdb,重启后数据却没了,是怎么回事?
  • bgsave 执行的这几秒里写进来的数据,会不会被保存?
  • Redis 挂了的那段时间,Java 应用会不会卡住?卡多久?

实测环境:

  • CentOS 7.9(VMware 虚拟机,xfs),Redis 6.2.4 源码编译。实验实例端口 6380,和正式实例分开。
  • 写入方放在另一台机器上 (宿主机),这样断电时它不会跟着一起停。它做的事很简单:循环执行 INCR seq,每条都等到回复再发下一条,记下最后一次收到的值(下面叫「已确认」)。
  • 宕机后用同一份配置重启,GET seq,和「已确认」比:少了就是丢了,多了也有,后面说。
  • 每种情况写 75 秒后宕机。

每种模式、每种宕机方式都只跑了一次,下面的数字是这一次的结果,不是固定值;能稳定说明的是「丢不丢、大概丢多少的量级」。

1. 进程被杀(kill -9)

持久化 已确认 重启后 结果
只开 RDB(默认 save 规则) 257,351 171,657 丢 85,694 次写入
AOF,appendfsync everysec 252,018 252,018 一条不丢
AOF,appendfsync always 140,428 140,429 多了 1 条
都不开(save "") 258,543 (nil) 全丢

1.1 只开 RDB:丢的是「上次快照以后的全部」

不写 save 时,6.2.4 的默认规则是:

arduino 复制代码
127.0.0.1:6380> config get save
1) "save"
2) "3600 1 300 100 60 10000"

意思是 3600 秒内有 1 次修改、300 秒内有 100 次、或 60 秒内有 1 万次修改,就做一次快照。kill 之前查了一下:

makefile 复制代码
rdb_changes_since_last_save:85692

最后丢的是 85,694 条,和这个数对得上(查完到 kill 之间又写了 2 条)。重启日志:

markdown 复制代码
* Loading RDB produced by version 6.2.4
* RDB age 25 seconds
* DB loaded from disk: 0.000 seconds

INFO persistence 里的 rdb_changes_since_last_save,就是「如果现在宕机、会丢多少次写入」。 平时监控它,比背规则有用。

1.2 AOF everysec:kill -9 一条不丢

kill -9 杀掉的只是 Redis 进程。AOF 的内容已经 write 进了操作系统的页缓存,只是还没 fsync 到磁盘;进程死了,页缓存还在,操作系统会照常把它写盘。所以重启后:

sql 复制代码
* DB loaded from append only file: 0.061 seconds
重启后 GET seq = 252018      ← 和已确认的一样

1.3 AOF always:重启后比「已确认」还多 1

最后一个 INCR 已经执行、也写进了 AOF,但回复还没送到客户端进程就没了。客户端看到的是连接断开,它会认为这次写入失败了,而数据其实已经在里面。

这和数据库里「超时的写入其实提交了」是一回事:宕机那一刻正在进行的写入,客户端拿到报错,不代表没写进去。 需要严格对账的业务,重试时要按「可能已经成功」处理(做成幂等)。

顺带:同一个逐条等回复的客户端,75 秒里 always 只写了 14 万次,everysec 写了 25 万次。

2. 真断电

kill -9 不丢,是因为操作系统还活着。断电时页缓存里还没落盘的东西就没了。

断电用的是虚拟机的硬重置:宿主机执行 vmrun reset hard,效果等于直接拔电源。

持久化 已确认 断电重启后 结果
AOF,appendfsync everysec 233,692 231,184 丢 2,508 次写入
AOF,appendfsync always 138,010 138,010 一条不丢
只开 RDB 238,257 197,216 丢 41,041 次写入
  • everysec :这一轮的写入速度大约每秒 3,100 次(233,692 ÷ 75),丢了 2,508 次,不到 1 秒的量。重启日志 DB loaded from append only file: 0.056 seconds,没有任何截断警告。同样的配置,kill -9 时一条不丢,断电时丢了将近 1 秒。
  • always:断电也一条不丢。
  • RDB:最后一次快照的日志时间是 22:15:10,断电在 22:15:26,丢的就是这 16 秒的写入。

重启后 RDB 的日志写的是:

markdown 复制代码
* RDB age 60 seconds

这个 60 秒是「快照生成」到「重启加载」之间的时间,包含了机器重启的那段停机时间,不是丢了 60 秒的数据。看日志判断丢了多少时,别直接拿这个数。

断电时,写入方(另一台机器上)看到的是 TimeoutError: timed out ------ 它不会收到「连接被重置」,只会一直等到超时。

3. 明明有 dump.rdb,重启后数据却没了

3.1 把配置改成 appendonly yes 再重启

这是最容易踩、后果也最重的一个。一个只开 RDB 的实例,有 1001 个 key,dump.rdb 9,003 字节。想开 AOF,于是在配置文件里把 appendonly no 改成 appendonly yes,然后重启:

ini 复制代码
dbsize=0
x=
-rw-r--r--. 1 root root    0 10月 15 21:59 appendonly.aof
-rw-r--r--. 1 root root 9003 10月 15 21:59 dump.rdb

库是空的。 AOF 开着时,Redis 只从 AOF 加载;AOF 不存在,它就新建一个 0 字节的,从空库开始 ------ dump.rdb 就在旁边,但不会去读它。日志里没有任何警告,只有正常的 Ready to accept connections。

更糟的是接下来:空库状态下随便写一条,下一次快照就把 dump.rdb 覆盖了。我写了一条 SET y 1 再 BGSAVE:

diff 复制代码
-rw-r--r--. 1 root root   50 10月 15 21:59 appendonly.aof
-rw-r--r--. 1 root root  102 10月 15 21:59 dump.rdb      ← 9,003 字节变成了 102 字节

原来的数据到这里就彻底没了。发现重启后是空库,第一件事是先把 dump.rdb 拷走,别让它被覆盖。

正确的开法是运行中开,让 Redis 先把当前数据写进 AOF:

makefile 复制代码
127.0.0.1:6380> config set appendonly yes
OK
127.0.0.1:6380> info persistence
aof_enabled:1
aof_rewrite_in_progress:0
aof_last_bgrewrite_status:ok
127.0.0.1:6380> config rewrite
OK

CONFIG SET appendonly yes 会触发一次 AOF 重写,把现有数据写成 appendonly.aof(这次是 9,003 字节,开头就是 RDB 格式的内容);CONFIG REWRITE 把 appendonly yes 写回配置文件。之后重启:

sql 复制代码
重启后 dbsize=1001 x=from-rdb
* DB loaded from append only file: 0.000 seconds

3.2 dir 是相对路径,换个目录启动

配置里不写 dir,默认是 ./,即启动时所在的目录。在数据目录里启动,数据存在数据目录;换个目录启动,它就去别处找了:

ruby 复制代码
在 /works/redis-lab/p2/reldir 下启动:dir=/works/redis-lab/p2/reldir   ← 写了 1000 个 key,SAVE
换到 / 下启动(配置文件用绝对路径):dir=/ dbsize=0

配置文件路径写对了也没用,dir 跟的是启动时所在的目录 ------ 只要哪次启动不是在数据目录里执行的,就会碰上。dir 一定写绝对路径。

3.3 启动时没带配置文件

bash 复制代码
cd /root && redis-server --port 6380 --daemonize yes
dir=/root dbsize=0 appendonly=no maxmemory=0

没带配置文件,所有配置都是默认值:dir 是当前目录、不开 AOF、不限内存。数据文件在别处,自然是空库。

这三种情况,都可以先用一条命令确认:

ruby 复制代码
127.0.0.1:6379> config get dir
127.0.0.1:6379> config get appendonly
127.0.0.1:6379> info server        ← 看 config_file 那一行,是不是你以为的那个配置文件

4. bgsave 期间写进来的数据,会不会被保存

灌了 200 万个 key,执行 BGSAVE,在 rdb_bgsave_in_progress 为 1 的这段时间里不停地写 during:*:

vbnet 复制代码
bgsave 期间写入 during:* 401 个
rdb_last_bgsave_status:ok
rdb_last_bgsave_time_sec:2
* DB saved on disk
* RDB: 9 MB of memory used by copy-on-write
现在内存里 during:* = 401

然后 kill -9,重启:

ini 复制代码
kill -9 后重启:dbsize=2000000 during:* = 0

不会。 BGSAVE 是 fork 出一个子进程来写快照,快照内容就是 fork 那一刻的数据;之后主进程收到的写入只在内存里,要等下一次快照。日志里那句 9 MB of memory used by copy-on-write,就是 bgsave 期间被改动、需要复制一份的内存。

5. Redis 挂了,Java 应用会卡多久

Spring Boot 4.1.1(Lettuce),spring.data.redis.timeout: 5s。应用每 0.5 秒调一次 set,中途把 Redis kill -9,15 秒后再启动:

bash 复制代码
#4 22:01:14.975 耗时 1ms    OK
#5 22:01:15.492 耗时 5008ms QueryTimeoutException / io.lettuce.core.RedisCommandTimeoutException: Command timed out after 5 second(s)
#6 22:01:21.016 耗时 5005ms QueryTimeoutException / io.lettuce.core.RedisCommandTimeoutException: Command timed out after 5 second(s)
#7 22:01:26.537 耗时 4200ms OK        ← Redis 22:01:30 启动
#8 22:01:31.250 耗时 2ms    OK
......之后全部 1~2ms
  • 不是立刻报错,是每次调用卡满 timeout 才失败。 报的是超时,不是「连接被拒绝」------ Lettuce 在断线时会把命令先攒着、自己在后台重连,命令等不到结果就超时。所以 Redis 一挂,用到它的接口每个请求都会慢 5 秒 ;timeout 设得越长,卡得越久。

  • #7 是在 Redis 还没起来时发出的,等了 4.2 秒,正好赶上 Redis 启动、重连成功,这条成功了。

  • 超时的 #5、#6 后来有没有被执行?去 AOF 里看 lab:down 被写入的值的顺序:

    1 2 3 4 7 8 9 10 11 ...

这次实验里,超时的那两条没有被执行。

  • Redis 回来以后,应用不用重启就自动恢复了。

6. 速查

想知道 看哪里
现在宕机会丢多少写入(RDB) INFO persistence 的 rdb_changes_since_last_save
现在用的是什么持久化 CONFIG GET appendonly、CONFIG GET appendfsync、CONFIG GET save
重启后从哪个文件加载的 启动日志:DB loaded from disk(RDB)还是 DB loaded from append only file(AOF)
重启后数据没了 config get dir、config get appendonly、info server 的 config_file;先把 dump.rdb 拷走
想给已有数据的实例开 AOF 运行中 CONFIG SET appendonly yes → 看 aof_last_bgrewrite_status:ok → CONFIG REWRITE;别直接改配置重启
持久化 kill -9(这次实测) 断电(这次实测)
只开 RDB 丢上次快照之后的全部 丢上次快照之后的全部
AOF everysec 不丢 丢不到 1 秒
AOF always 不丢(还可能多出「客户端以为失败」的那一条) 不丢
都不开 全丢 全丢

下一篇:持久化本身出错 ------ bgsave 失败(fork: Cannot allocate memory)、MISCONF 让所有写入被拒、磁盘满了、AOF 文件太大和损坏。

相关推荐
李哈哈8522 小时前
微服务依赖冲突
后端
努力的小雨2 小时前
每天盯着屏幕写代码,我终于不想再凑合了
后端
海岳云舟2 小时前
springboot项目中使用AOP
后端
小稀土1232 小时前
5步跑通第一个迭代:敏捷研发管理平台落地最小闭环
后端
zz在掘金337082 小时前
Django 中间件全家桶背诵笔记
后端
斑鸠喳喳2 小时前
调度线程池执行器 ScheduledThreadPoolExecutor
后端
大哥43092 小时前
索引 mapping 与代码对不上:一次不报错的静默失效排查
后端
神魔皆以血嗜2 小时前
Java 存储金额
java·后端
大哥43092 小时前
模型名停用与分层降级:一次 LLM 链路的失败面设计
后端