浅谈redis的RDB持久化机制
  TEZNKK3IfmPf 2023年11月14日 39 0

1、RDB简介

Redis有两种持久化形式,一种是RDB快照模式(snapshot),另一种是AOF(Append-Only-file)。
RDB是将当前的数据集快照写入磁盘, Redis 重启动时, 可以通过载入 RDB 文件来还原数据库的状态。假如Redis没有持久化机制,可不可以,可以!当成纯内存使用,禁止所有持久化机制,追求高性能。
RDB 功能最核心的是 rdbSaverdbLoad 两个函数, rdbSave用于生成 RDB 文件到磁盘, 而rdbLoad 则用于将 RDB 文件中的数据重新载入到内存中。
rdbsave将快照写入RDB文件的时候,如果文件已经存在,则新的RDB文件会将已有的文件覆盖。

2、如何触发

RDB是怎么工作的?只要安装好resdis可以了么?当然不是!!!

RDB有两种方式可以触发RDB持久化,这里说的RDB持久化,指的是一次将当前的数据集快照写入磁盘的操作。分别是通过执行命令手动触发通过配置redis.conf文件自动触发

2.1 手动触发

手动触发redis有两个指令,分别是savebgsave

  • save: 该线程会阻塞当前redis服务器,执行save指令期间,redis不能处理其他的命令,直到RDB持久化完成。如果RDB文件较大,会产生比较大的影响,生产上谨慎使用
  • bgsave:为了解决save命令存在的痛点,bgsave会异步执行RDB持久化操作,不会影响redis处理其他的请求。Redis服务器会通过fork操作来创建一个子进程,专门处理此次的RDB持久化操作,完成后自动结束。Redis只会在fork过程中发生阻塞,而且时间较短,当然,fork时间也会随着数据量变大而变长,需要占用的内存也会加倍。

savebgsave命令都会调用上面说的rdbsave方法,只是方式不一样。

2.2 自动触发

需要配置redis.conf文件:
浅谈redis的RDB持久化机制

save 900 1     # 如果900 秒内至少有 1 个 key 的值变化,则触发RDB
save 300 10   # 如果300 秒内至少有 10 个 key 的值变化,则触发RDB
save 60 10000    # 如果60 秒内至少有 10000 个 key 的值变化,则触发RDB

如何关闭自动触发?
只需要注释掉上面的代码,或者直接用空字符串即可。

save ""

如果使用命令行操作就是

redis-cli config set save " "

2.2.1 自动保存的原理

为什么配置了就可以了?底层的机制是什么?
redis有一个周期操作函数serverCron,默认是每100毫秒就会执行一次,只要服务器在运行,这个函数也就一直在工作,检查save设置的条件是否有被满足,这个是serverCron函数的其中一个功能,如果满足,就会执行bgsave。基本所有的RDB操作都是默认使用bgsave,而不是save

除了上面说的save参数之外,我们来看看其他的和RDB相关的配置

  • save 900 1  
    • 配置触发RDB持久化的条件
  • stop-writes-on-bgsave-error yes  
    • 默认是yes,这个配置意思是如果启动RDB持久化,那么如果后台保存数据失败,redis是否会停止接受数据。这蛮好的,能起到告警的作用,如果redis持久化有问题了,能够反映到redis本身的读写上来。
  • rdbcompression yes  
    • 默认是yes,也就是对rab快照到磁盘的内容,是不是要启动压缩,压缩会默认使用LZF算法压缩。压缩的好处是:快照会比较小,占用空间小。不压缩的好处是:节省CPU资源,毕竟压缩是需要消耗CPU的计算型任务。
  • rdbchecksum yes  
    • 由于RDB的版本5在文件的末尾放置了一个CRC64校验和。这使得格式更能抵抗破坏,但是在保存和加载RDB文件时,要付出大约10%的性能代价,所以可以禁用它以获得最大的性能。禁用校验和后创建的RDB文件的校验和为零,这将告诉加载代码跳过校验。
  • dbfilename dump.rdb  
    • 设置快照的文件名
  • dir ./  
    • 设置快照的存放的目录,注意是目录而不是文件,默认和当前的配置文件是反正同一个目录。

说起自动保存,我们需要从源码解读,redis的server.h中有一个结构体数组redisServer,里面有一个配置struct saveparam *saveparams; /* Save points array for RDB */
浅谈redis的RDB持久化机制

struct redisServer {
    ...
    struct saveparam *saveparams;          //记录了save保存触发条件点的数组
    long long dirty;                               //修改的次数
    time_t lastsave;                            //上一次执行保存的时间
    ...
};

这个saveparam结构体有两个属性,一个是秒数,一个是修改数:

struct saveparam {
    time_t seconds;   
    int changes;
};

除此之外,还有两个属性比较重要,也是在redisServer中:

  • dirty:记录了上次执行save或者bgsave命令之后,到现在,一共发生了多少次修改
  • lastsave:UNIX的时间戳,保存了最后一次执行保存命令的时间。
    我们来看看这段检查代码,也在redis.c
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
    int j;
    ......
    /* 检查正在进行中的后台保存或AOF重写是否已终止。 */
    if (server.rdb_child_pid != -1 || server.aof_child_pid != -1 ||
        ldbPendingChildren())
    {
        int statloc;
        pid_t pid;
        // 查看这个进程是否正常返回信号
        if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {
            int exitcode = WEXITSTATUS(statloc);
            int bysignal = 0;
            if (WIFSIGNALED(statloc)) bysignal = WTERMSIG(statloc);
            if (pid == -1) {
            // 异常处理
                serverLog(LL_WARNING,"wait3() returned an error: %s. "
                    "rdb_child_pid = %d, aof_child_pid = %d",
                    strerror(errno),
                    (int) server.rdb_child_pid,
                    (int) server.aof_child_pid);
            } else if (pid == server.rdb_child_pid) {
                 //成功持久化 RDB 文件,调用方法用心的RDB文件覆盖旧的RDB文件
                backgroundSaveDoneHandler(exitcode,bysignal);
                if (!bysignal && exitcode == 0) receiveChildInfo();
            } else if (pid == server.aof_child_pid) {
                // 成功执行 AOF,替换现有的 AOF文件
                backgroundRewriteDoneHandler(exitcode,bysignal);
                if (!bysignal && exitcode == 0) receiveChildInfo();
            } else {
                if (!ldbRemoveChild(pid)) {
                    serverLog(LL_WARNING,
                        "Warning, detected child with unmatched pid: %ld",
                        (long)pid);
                }
            }
            updateDictResizePolicy();
            closeChildInfoPipe();
        }
    } else {
        /* 如果没有后台保存/重写正在进行中,检查是否我们现在必须保存/重写。*/
        for (j = 0; j < server.saveparamslen; j++) {
            //计算距离上次执行保存操作多少秒
            struct saveparam *sp = server.saveparams+j;
            /* 如果我们达到了给定的更改量,并且最新的bgsave是成功的,或者在出现错误的情况下,至少CONFIG_BGSAVE_RETRY_DELAY秒已经过去,那么就进行保存。*/
            if (server.dirty >= sp->changes &&
                server.unixtime-server.lastsave > sp->seconds &&
                (server.unixtime-server.lastbgsave_try >
                 CONFIG_BGSAVE_RETRY_DELAY ||
                 server.lastbgsave_status == C_OK))
            {
                serverLog(LL_NOTICE,"%d changes in %d seconds. Saving...",
                    sp->changes, (int)sp->seconds);
                rdbSaveInfo rsi, *rsiptr;
                rsiptr = rdbPopulateSaveInfo(&rsi);
                // 启动bgsave
                rdbSaveBackground(server.rdb_filename,rsiptr);
                break;
            }
        }
       ......
}

rdbSaveBackground函数在哪里呢?怎么实现的,我们看看rdb.c文件中。

int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
    pid_t childpid;
    long long start;
    // 如果已经有aof或者rdb持久化任务,那么child_pid就不是-1,会直接返回
    if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;
    //bgsave之前的dirty数保存起来
    server.dirty_before_bgsave = server.dirty;
    server.lastbgsave_try = time(NULL);
    //打开子进程信息管道
    openChildInfoPipe();
    // 记录RDB开始的时间
    start = ustime();
    // fork一个子进程
    if ((childpid = fork()) == 0) {
        //如果fork()的结果childpid为0,即当前进程为fork的子进程,那么接下来调用rdbSave()进程持久化;
        int retval;
        /* Child */
        closeListeningSockets(0);
        redisSetProcTitle("redis-rdb-bgsave");
        // 真正在子进程里调用rdbsave
        retval = rdbSave(filename,rsi);
        if (retval == C_OK) {
            size_t private_dirty = zmalloc_get_private_dirty(-1);
            if (private_dirty) {
                serverLog(LL_NOTICE,
                    "RDB: %zu MB of memory used by copy-on-write",
                    private_dirty/(1024*1024));
            }
            server.child_info_data.cow_size = private_dirty;
            sendChildInfo(CHILD_INFO_TYPE_RDB);
        }
        exitFromChild((retval == C_OK) ? 0 : 1);
    } else {
        /* 父进程 */
        // 更新fork进程需要消耗的时间
        server.stat_fork_time = ustime()-start;
        // 更新fork速率:G/秒,zmalloc_used_memory()的单位是字节,所以通过除以(1024*1024*1024),得到GB信息,fork_time:fork时间是微妙,所以得乘以1000000,得到每秒钟fork多少GB的速率;
        server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024*1024*1024); /* GB per second. */
        latencyAddSampleIfNeeded("fork",server.stat_fork_time/1000);
        if (childpid == -1) {
            // fork子进程出错
            closeChildInfoPipe();
            server.lastbgsave_status = C_ERR;
            serverLog(LL_WARNING,"Can't save in background: fork: %s",
                strerror(errno));
            return C_ERR;
        }
        serverLog(LL_NOTICE,"Background saving started by pid %d",childpid);
        // 最后需要记录redisServer中的变量值
        server.rdb_save_time_start = time(NULL);
        server.rdb_child_pid = childpid;
        server.rdb_child_type = RDB_CHILD_TYPE_DISK;
        updateDictResizePolicy();
        return C_OK;
    }
    return C_OK; /* unreached */
}

3、删除rdb文件能不能恢复?

先说结论,结论是如果只开启了RDB持久化
首先我们写入了两对

set hello world
set hello2 world

突然想看rdb文件,心血来潮啊这是,
直接打开,完蛋,有很多乱码,大概能看到redis版本,以及刚刚写进去的那两个键值对。
浅谈redis的RDB持久化机制

于是乎使用神器Vs Code,下载一个插件,hexdump,打开文件之后,对准文件右键,show hexdump,好家伙,出来了。里面其实也是差不多,可以看到个大概。

  Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F   
00000000: 52 45 44 49 53 30 30 30 37 FA 09 72 65 64 69 73    REDIS0007z.redis
00000010: 2D 76 65 72 05 33 2E 32 2E 38 FA 0A 72 65 64 69    -ver.3.2.8z.redi
00000020: 73 2D 62 69 74 73 C0 40 FA 05 63 74 69 6D 65 C2    s-bits@@z.ctimeB
00000030: 9C BD A5 5E FA 08 75 73 65 64 2D 6D 65 6D C2 08    .=%^z.used-memB.
00000040: 3B 0C 00 FE 00 FB 02 00 00 05 68 65 6C 6C 6F 05    ;..~.{....hello.
00000050: 77 6F 72 6C 64 00 06 68 65 6C 6C 6F 32 06 77 6F    world..hello2.wo
00000060: 72 6C 64 32 FF C0 E6 1E 14 9F CB 5F 15             rld2.@f...K_.

我们先看看redis的进程是什么?

 ps -ef | grep redis
root        953      1  0 16:34 ?        00:00:02 /usr/local/bin/redis-server 127.0.0.1:6379
root       1297   1241  0 16:54 pts/0    00:00:00 grep --color=auto redis
root       1300   1241  0 16:55 pts/0    00:00:00 grep --color=auto redis

上面可以看到server的是953,我们先快速删除掉rdb文件,然后快速使用kill -9杀死redis进程
浅谈redis的RDB持久化机制

关闭这个进程了,但是这个时候我们去重启redis会遇到一个问题,那就是pid文件没有删掉,会提示/var/run/redis_6379.pid exists, process is already running or crashed.
我们必须手动去删除pid的文件,再手动重启redis,会发现所有的数据都丢失了。
浅谈redis的RDB持久化机制

4、RDB的优点和缺点

4.1 优点

(1)RDB会生成多个数据文件,每个数据文件都代表了某一个时刻中redis的数据,这种多个数据文件的方式,非常适合做冷备份,可以将这种完整的数据文件发送到一些远程的安全存储上去,比如说Amazon的云服务上去,在国内可以是阿里云的ODPS分布式存储上,以预定好的备份策略来定期备份redis中的数据。

(2)RDB对redis对外提供的读写服务,影响非常小,可以让redis保持高性能,因为redis主进程只需要fork一个子进程,让子进程执行磁盘IO操作来进行RDB持久化即可

(3)相对于AOF持久化机制来说,直接基于RDB数据文件来重启和恢复redis进程,更加快速。因为AOF存放的是指令日志,所以数据恢复的时候需要回放每一个操作,也就是执行所有之前的过程来进行数据恢复。RDB是直接将数据加载到内存中。

4.2 缺点

(1)如果想要在redis故障时,尽可能少的丢失数据,那么RDB没有AOF好。一般来说,RDB数据快照文件,都是每隔5分钟,或者更长时间生成一次,这个时候就得接受一旦redis进程宕机,那么会丢失最近5分钟的数据。RDB不适合做第一优先的恢复方案,可能数据恢复地比较多。也就是说从最后一次备份开始到出现故障那一段时间,所有的数据修改都会丢失。

(2)RDB每次在fork子进程来执行RDB快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,或者甚至数秒。所以一般不能让RDB的间隔太长,这样每次生产的RDB文件太大了,会对性能有一定影响。

此文章仅代表自己(本菜鸟)学习积累记录,或者学习笔记,如有侵权,请联系作者删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有错误之处,还望指出,感激不尽~

技术之路不在一时,山高水长,纵使缓慢,驰而不息。

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月14日 0

暂无评论

推荐阅读
  TEZNKK3IfmPf   2024年05月31日   23   0   0 redis用户
  TEZNKK3IfmPf   2024年05月31日   27   0   0 dataredis
  TEZNKK3IfmPf   2024年05月31日   23   0   0 awkredis
  TEZNKK3IfmPf   2024年04月19日   35   0   0 javarediskey
TEZNKK3IfmPf