写在文章开头
在Redis
的世界里,数据的持久化犹如坚固的基石,支撑着整个系统的稳定运行与数据安全。在实际应用场景中,我们不仅需要Redis
强大的内存处理能力,更需要确保数据在各种意外情况下不丢失,这时候持久化机制就显得尤为重要。
Redis RDB(Redis Database)
持久化作为Redis重要的持久化方式之一,有着独特的魅力与价值。它以一种紧凑且高效的方式将Redis
在某一时刻的数据快照保存到磁盘上,在Redis重启时,可以快速地将这些数据恢复到内存中,极大地提高了系统的可用性和数据恢复效率。
本文将深入探讨Redis RDB
持久化,从它的基本概念、工作原理讲起,详细阐述其优缺点、触发机制、配置参数等关键内容。无论是初涉Redis的新手,还是想要深入理解持久化机制的开发者,都能从本文中收获关于Redis RDB持久化的全面且深入的知识,为实际项目开发提供有力支持。

Hi,我是 sharkChili ,是个不断在硬核技术上作死的技术人,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili 。
同时也非常欢迎你star我的开源项目mini-redis:https://github.com/shark-ctrl/mini-redis
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。
详解RDB基础
什么是RDB
RDB
持久化机制是将内存中的数据生成快照并持久化到磁盘的过程,RDB
可以通过手动或者自动的方式实现持久化:

RDB的几种触发时机
手动触发
我们先来说说手动触发即save
命令,这个指令会直接阻塞当前redis
服务器,知道RDB完成了为止,对于线上生产环境数据的备份,我们非常非常不建议使用这种方式。
ounter(lineounter(lineounter(line
127.0.0.1:6379> save
OK
复制
接下来就是bgsave
指令了,bgsave
则是主进程fork一个子进程,由子进程完成持久化操作,而主进程继续处理客户端的读写请求,如果我们需要手动实现持久化,非常推荐使用这种方式。
ounter(lineounter(lineounter(lineounter(line
# 从输出我们就可以看出这种方式会将持久化的操作放在后台执行
127.0.0.1:6379> bgsave
Background saving started
复制
被动触发
还有一种就是被动触发,或者说是自动触发,自动触发我们可以通过配置实现redis.conf
的save
参数实现,如下所示,假如我们希望用户20s内写入3次就进行持久化,只需在配置中加一条save 20 3
即可。
ounter(lineounter(line
save 20 3
复制
需要注意的是save 20 3
的20s是以redis的时间间隔为主,并不是用户第1次写入后的20s内再写入两次进行持久化,本质上被动触发是由redis server
的一个定时任务扫描执行:

关闭时持久化
当我们执行shutdown
指令时,如果没有明确指明参数nosave
,该指令会调用rdbSave
将当前内存中的键值对持久化到rdb文件中:

对应我们也给出redis
源码中关于shutdown
持久化的核心代码,即位于db.c
的shutdownCommand
函数:
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
void shutdownCommand(redisClient *c) {
//......
//调用prepareForShutdown执行rdb持久化
if (prepareForShutdown(flags) == REDIS_OK) exit(0);
addReplyError(c,"Errors trying to SHUTDOWN. Check logs.");
}
//服务器进程关闭时调用rdbSave生成rdb文件
int prepareForShutdown(int flags) {
int save = flags & REDIS_SHUTDOWN_SAVE;
int nosave = flags & REDIS_SHUTDOWN_NOSAVE;
......
如果存在rdb子进程则杀掉
if (server.rdb_child_pid != -1) {
redisLog(REDIS_WARNING,"There is a child saving an .rdb. Killing it!");
kill(server.rdb_child_pid,SIGUSR1);
rdbRemoveTempFile(server.rdb_child_pid);
}
//......
**
* 符合以下任意条件都会触发rdb持久化:
* 1. 如果我们有配置save参数例如(save 20 3) 则saveparamslen大于0,且nosave非0
* 2. save为1(默认情况下会指明1)
*/
if ((server.saveparamslen > 0 && !nosave) || save) {
执行rdb持久化
if (rdbSave(server.rdb_filename) != REDIS_OK) {
......
return REDIS_ERR;
}
}
......
return REDIS_OK;
}
复制
RDB的使用方式
基于上述配置我们简单演示一下RDB持久化机制,我们首先需要存点数据,20s存3个值:
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> set k3 v3
OK
复制
完成后查看是否生成rdb文件,确认无误后,我们将这个文件备份,并强制关闭redis服务端,模拟断电的场景
ounter(lineounter(lineounter(line
# 重命名rdb文件
[root@iZ8vb7bhe4b8nhhhpavhwpZ sbin]# mv dump.rdb dump.rdb.bak
复制
此时我们再启动redis就会发现数据为空
ounter(lineounter(line
127.0.0.1:6379> keys *
(empty array)
复制
我们将rdb文件还原,并重启redis,可以发现备份数据还原了
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
# 强制关闭redis
[root@iZ8vb7bhe4b8nhhhpavhwpZ sbin]# ps -ef |grep redis |grep -v grep
root 8956 1 0 23:22 ? 00:00:00 redis-server 127.0.0.1:6379
[root@iZ8vb7bhe4b8nhhhpavhwpZ sbin]# kill -9 8956
# 还原rdb,并启动redis
[root@iZ8vb7bhe4b8nhhhpavhwpZ sbin]# mv dump.rdb.bak dump.rdb
[root@iZ8vb7bhe4b8nhhhpavhwpZ sbin]# redis-server root/redis/redis.conf
[root@iZ8vb7bhe4b8nhhhpavhwpZ sbin]# redis-cli
# 可以看到之前设置的数据都回来了
127.0.0.1:6379> keys *
1) "k3"
2) "k2"
3) "k1"
复制
详解bgsave的工作流程
bgsave
的工作流程如下图所示,整体可以简述为:
主进程fork出一个子进程,这时候主进程会被阻塞。 子进程创建完成后, redis
客户端会输出Background saving started
,这就意味子进程开始进行持久化操作了。子进程持久化完成后,会生成一个rdb文件,将本次的rdb文件通过原子替换的方式将上一次备份的rdb覆盖。 子进程发送信号通知父进程本次任务完成。

RDB常见的配置参数(了解)
首先是dbfilename
,它可以指定rdb
的文件名
ounter(lineounter(lineounter(lineounter(line
# The filename where to dump the DB
dbfilename dump.rdb
复制
接下来就是dir,它可以指定rdb文件的持久化的位置,默认取redis服务端的位置。
ounter(lineounter(line
dir ./
复制
当reids
无法将文件写入磁盘,我们可以讲stop-writes-on-bgsave-error
设置为yes
,直接关掉redis的写操作,默认为yes
ounter(lineounter(lineounter(line
stop-writes-on-bgsave-error yes
复制
rdbcompression
开启后,redis
默认会通过LZF
算法压缩rdb
文件。这种方式会消耗CPU
,但是压缩后的大小远远小于内存,但是带来的收益却远远大于这点开销,通过压缩的文件无论是通过网络发送到从节点还是存储到硬盘的空间都是非常可观的。
ounter(lineounter(lineounter(lineounter(line
rdbcompression yes
复制
rdbchecksum
开启后,在存储快照后,还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。
ounter(lineounter(line
rdbchecksum yes
复制
RDB有哪些优缺点
优点:
rdb是紧凑压缩的二进制文件,非常实用与备份或者全景复制等场景。 rdb恢复数据效率远远高于aof
而缺点如下:
无法做到毫秒级别的实时性持久化,尽管我们可以通过设置紧凑的 save
完成持久化,但是频繁的fork子进程进行持久化,很可能造成redis主进行长期阻塞。

存储的文件是二进制,不够直观,可能还存在某些兼容问题。
详解RDB进阶知识点
我们为Redis开辟的一块大内存空间,进行持久化时就可能耗时长,这段时间还可能收到客户端的请求,如何保持持久化后的数据一致性
在进行周期性快照数据持久化期间,redis
会fork
一个子进程异步执行,但是父子进程仍然共享同一个代码段和数据段,两者并行操作存在线程安全的风险。
所以在快照持久化期间,主进程的修改操作都采用了写时复制(Copy On Write)
的思想,即将需要进行操作的键值对数据从原有数据页中复制出一份副本进行修改,等到bgsave
子进程快照完成后,再将这块内存区域同步到原来的内存区域中,等待下一次快照:

这样做的缺点也很明显,极端情况下,如果在bgsave
期间主进程数据都被改了,那么内存占用就是原来的两倍:

在进行快照操作的这段时间,如果发生服务崩溃怎么办?
服务恢复的数据只会是上一次备份的rdb
文件数据,因为bgsave
子进程只会将操作成功的文件生成rdb
文件覆盖上一次备份的文件。
可以每秒做一次快照
可能会有下面这几个问题:
频繁写入内存数据会给磁盘带来很大的压力,多个 fork
子进程抢占优先的磁盘带宽,前一个子进程没写完,后一个子进程又来写入。虽说快照这个操作是单位时间内只能执行一次异步,但是不间断的 rdb
异步持久化每次fork
子进程这个操作都会阻塞主进程,频繁fork
很可能对于性能开销还是很大的。对于全量大数据快照操作是很耗时的,即使我们延长了RDB快照的调度间隔, redis
每次进行rdb持久化之前也会检查当前是否有子进程执行快照,如果存在则不允许快照,所以针对数据量较大的场景做这种频繁保存的操作意义也不大。
对应笔者也给出的bgsave
的源码实现,可以看到在每次进行持久化的时候bgsaveCommand
都会检查当前是否有子进程正在执行RDB
持久化,如果存在则不允许用户进行持久化:
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
//调用rdbSaveBackground创建一个子进程生成rdb文件,不影响主线程
void bgsaveCommand(redisClient *c) {
//如果存在rdb或者aof的子进程则直接不允许执行bgsave
if (server.rdb_child_pid != -1) {
addReplyError(c,"Background save already in progress");
} else if (server.aof_child_pid != -1) {
addReplyError(c,"Can't BGSAVE while AOF log rewriting is in progress");
} else if (rdbSaveBackground(server.rdb_filename) == REDIS_OK) {
addReplyStatus(c,"Background saving started");
} else {
addReply(c,shared.err);
}
}
复制
更多关于redis rdb持久化
当然以上便是应用层面对于redis rdb持久化机制的分析,如果读者希望更深入的了解redis可以参考笔者这篇文章:
Redis RDB持久化源码深度解析:从原理到实现https://mp.weixin.qq.com/s/rbJ7YR77iJYQ2e0rZYUvzA
小结
我是 sharkchili ,CSDN Java 领域博客专家,mini-redis的作者,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili 。
同时也非常欢迎你star我的开源项目mini-redis:https://github.com/shark-ctrl/mini-redis
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。
参考
Redis持久化简介:https://www.pdai.tech/md/db/nosql-redis/db-redis-x-rdb-aof.html#rdb-更深入理解
Redis常见面试题总结(上):https://javaguide.cn/database/redis/redis-questions-01.html#怎么保证-redis-挂掉之后再重启数据可以进行恢复
面试必问的 Redis:RDB、AOF、混合持久化:https://zhuanlan.zhihu.com/p/340082703
Redis_RDB持久化之写时复制技术的应用 :https://www.cnblogs.com/zyf98/p/15934058.html