这一篇讲什么
「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 文件太大和损坏。