一文读懂 Redis RDB 持久化:策略、配置与应用

在Redis的世界里,数据的持久化犹如坚固的基石,支撑着整个系统的稳定运行与数据安全。在实际应用场景中,我们不仅需要Redis强大的内存处理能力,更需要确保数据在各种意外情况下不丢失,这时候持久化机制就显得尤为重要。

Redis RDB(Redis Database)持久化作为Redis重要的持久化方式之一,有着独特的魅力与价值。它以一种紧凑且高效的方式将Redis在某一时刻的数据快照保存到磁盘上,在Redis重启时,可以快速地将这些数据恢复到内存中,极大地提高了系统的可用性和数据恢复效率。

本文将深入探讨Redis RDB持久化,从它的基本概念、工作原理讲起,详细阐述其优缺点、触发机制、配置参数等关键内容。无论是初涉Redis的新手,还是想要深入理解持久化机制的开发者,都能从本文中收获关于Redis RDB持久化的全面且深入的知识,为实际项目开发提供有力支持。

一、详解RDB基础

1. 什么是RDB

RDB持久化机制是将内存中的数据生成快照并持久化到磁盘的过程,RDB可以通过手动或者自动的方式实现持久化:

2. RDB的几种触发时机

(1) 手动触发

我们先来说说手动触发即save命令,这个指令会直接阻塞当前redis服务器,知道RDB完成了为止,对于线上生产环境数据的备份,我们非常非常不建议使用这种方式。

复制
ounter(lineounter(lineounter(line 127.0.0.1:6379> save OK1.2.3.

接下来就是bgsave指令了,bgsave则是主进程fork一个子进程,由子进程完成持久化操作,而主进程继续处理客户端的读写请求,如果我们需要手动实现持久化,非常推荐使用这种方式。

复制
ounter(lineounter(lineounter(lineounter(line # 从输出我们就可以看出这种方式会将持久化的操作放在后台执行 127.0.0.1:6379> bgsave Background saving started1.2.3.4.

(2) 被动触发

还有一种就是被动触发,或者说是自动触发,自动触发我们可以通过配置实现redis.conf的save参数实现,如下所示,假如我们希望用户20s内写入3次就进行持久化,只需在配置中加一条save 20 3即可。

复制
ounter(lineounter(line save 20 31.2.

需要注意的是save 20 3的20s是以redis的时间间隔为主,并不是用户第1次写入后的20s内再写入两次进行持久化,本质上被动触发是由redis server的一个定时任务扫描执行:

(3) 关闭时持久化

当我们执行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; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.
3. 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 OK1.2.3.4.5.6.7.

完成后查看是否生成rdb文件,确认无误后,我们将这个文件备份,并强制关闭redis服务端,模拟断电的场景:

复制
ounter(lineounter(lineounter(line # 重命名rdb文件 [root@iZ8vb7bhe4b8nhhhpavhwpZ sbin]# mv dump.rdb dump.rdb.bak1.2.3.4.5.

此时我们再启动redis就会发现数据为空:

复制
ounter(lineounter(line 127.0.0.1:6379> keys * (empty array)1.2.3.

我们将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"1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.
4. 详解bgsave的工作流程

bgsave的工作流程如下图所示,整体可以简述为:

主进程fork出一个子进程,这时候主进程会被阻塞。子进程创建完成后,redis客户端会输出Background saving started,这就意味子进程开始进行持久化操作了。子进程持久化完成后,会生成一个rdb文件,将本次的rdb文件通过原子替换的方式将上一次备份的rdb覆盖。子进程发送信号通知父进程本次任务完成。

5. RDB常见的配置参数(了解)

首先是dbfilename ,它可以指定rdb的文件名:

复制
ounter(lineounter(lineounter(lineounter(line # The filename where to dump the DB dbfilename dump.rdb1.2.3.4.5.

接下来就是dir,它可以指定rdb文件的持久化的位置,默认取redis服务端的位置。

复制
ounter(lineounter(linedir ./ ounter(lineounter(line dir ./1.2.3.

当reids无法将文件写入磁盘,我们可以讲stop-writes-on-bgsave-error设置为yes,直接关掉redis的写操作,默认为yes:

复制
ounter(lineounter(lineounter(line stop-writes-on-bgsave-error yes1.2.3.4.

rdbcompression 开启后,redis默认会通过LZF算法压缩rdb文件。这种方式会消耗CPU,但是压缩后的大小远远小于内存,但是带来的收益却远远大于这点开销,通过压缩的文件无论是通过网络发送到从节点还是存储到硬盘的空间都是非常可观的。

复制
ounter(lineounter(lineounter(lineounter(line rdbcompression yes1.2.3.4.5.6.

rdbchecksum 开启后,在存储快照后,还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。

复制
ounter(lineounter(line rdbchecksum yes1.2.
6. RDB有哪些优缺点

优点:

rdb是紧凑压缩的二进制文件,非常实用与备份或者全景复制等场景。rdb恢复数据效率远远高于aof

而缺点如下:

无法做到毫秒级别的实时性持久化,尽管我们可以通过设置紧凑的save完成持久化,但是频繁的fork子进程进行持久化,很可能造成redis主进行长期阻塞。

存储的文件是二进制,不够直观,可能还存在某些兼容问题。

二、详解RDB进阶知识点

1. 我们为Redis开辟的一块大内存空间,进行持久化时就可能耗时长,这段时间还可能收到客户端的请求,如何保持持久化后的数据一致性?

在进行周期性快照数据持久化期间,redis会fork一个子进程异步执行,但是父子进程仍然共享同一个代码段和数据段,两者并行操作存在线程安全的风险。

所以在快照持久化期间,主进程的修改操作都采用了写时复制(Copy On Write)的思想,即将需要进行操作的键值对数据从原有数据页中复制出一份副本进行修改,等到bgsave子进程快照完成后,再将这块内存区域同步到原来的内存区域中,等待下一次快照:

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

2. 在进行快照操作的这段时间,如果发生服务崩溃怎么办?

服务恢复的数据只会是上一次备份的rdb文件数据,因为bgsave子进程只会将操作成功的文件生成rdb文件覆盖上一次备份的文件。

3. 可以每秒做一次快照吗?

可能会有下面这几个问题:

频繁写入内存数据会给磁盘带来很大的压力,多个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,"Cant 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); } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.

阅读剩余
THE END