Redis源码剖析之RDB

智能合约
• 阅读 1594

我们小学三年级的时候就知道,redis是一个纯内存存储的中间件,那它宕机会怎么样?数据会丢失吗?答案是可以不丢。 事实上redis为了保证宕机时数据不丢失,提供了两种数据持久化的机制——rdb和aof。

rdb就定期将内存里的数据全量dump到磁盘里,下次启动时就可以直接加载之前的数据了,rdb的问题是它只能提供某个时刻的数据快照,无法保证建立快照后的数据不丢,所以redis还提供了aof。aof全程是Append Only File,它的原理就是把所有改动一条条写到磁盘上。这篇博客我们来重点介绍下rdb持久性的实现,aof留到下一篇博客。

rdb相关源码

在redis中,触发rdb保存主要有以下几种方式。

save命令

我们在redis-cli下直接调用save命令就会触发rdb文件的生成,如果后台没有在子进程在生成rdb,就会调用rdbSave()生成rdb文件,并将其保存在磁盘中。

void saveCommand(client *c) {
    // 检查是否后台已经有进程在执行save,如果有就停止执行。
    if (server.child_type == CHILD_TYPE_RDB) {
        addReplyError(c,"Background save already in progress");
        return;
    }
    rdbSaveInfo rsi, *rsiptr;
    rsiptr = rdbPopulateSaveInfo(&rsi);
    if (rdbSave(server.rdb_filename,rsiptr) == C_OK) {
        addReply(c,shared.ok);
    } else {
        addReplyErrorObject(c,shared.err);
    }
}

Redis 的 rdbSave 函数是真正进行 RDB 持久化的函数,它的大致流程如下:

  1. 首先创建一个临时文件。
  2. 创建并初始化rio,rio是redis对io的一种抽象,提供了read、write、flush、checksum……等方法。
  3. 调用 rdbSaveRio(),将当前 Redis 的内存信息全量写入到临时文件中。
  4. 调用 fflush、 fsync 和 fclose 接口将文件写入磁盘中。
  5. 使用 rename 将临时文件改名为 正式的 RDB 文件。
  6. 将server.dirty清零,server.dirty是用了记录在上次生成rdb后有多少次数据变更,会在serverCron中用到。

具体代码如下:

/* rdb磁盘写入操作 */
int rdbSave(char *filename, rdbSaveInfo *rsi) {
    char tmpfile[256];
    char cwd[MAXPATHLEN]; /* Current working dir path for error messages. */
    FILE *fp = NULL;
    rio rdb;
    int error = 0;

    snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
    fp = fopen(tmpfile,"w");
    if (!fp) {
        char *cwdp = getcwd(cwd,MAXPATHLEN);
        serverLog(LL_WARNING,
            "Failed opening the RDB file %s (in server root dir %s) "
            "for saving: %s",
            filename,
            cwdp ? cwdp : "unknown",
            strerror(errno));
        return C_ERR;
    }

    rioInitWithFile(&rdb,fp);  // 初始化rio,
    startSaving(RDBFLAGS_NONE);

    if (server.rdb_save_incremental_fsync)
        rioSetAutoSync(&rdb,REDIS_AUTOSYNC_BYTES);
    // 内存数据dump到rdb 
    if (rdbSaveRio(&rdb,&error,RDBFLAGS_NONE,rsi) == C_ERR) {
        errno = error;
        goto werr;
    }

    /* 把数据刷到磁盘删,确保操作系统缓冲区没有剩余数据 */
    if (fflush(fp)) goto werr;
    if (fsync(fileno(fp))) goto werr;
    if (fclose(fp)) { fp = NULL; goto werr; }
    fp = NULL;
    
    /* 把临时文件重命名为正式文件名 */
    if (rename(tmpfile,filename) == -1) {
        char *cwdp = getcwd(cwd,MAXPATHLEN);
        serverLog(LL_WARNING,
            "Error moving temp DB file %s on the final "
            "destination %s (in server root dir %s): %s",
            tmpfile,
            filename,
            cwdp ? cwdp : "unknown",
            strerror(errno));
        unlink(tmpfile);
        stopSaving(0);
        return C_ERR;
    }

    serverLog(LL_NOTICE,"DB saved on disk");
    server.dirty = 0;
    server.lastsave = time(NULL);
    server.lastbgsave_status = C_OK;
    stopSaving(1);
    return C_OK;

werr:
    serverLog(LL_WARNING,"Write error saving DB on disk: %s", strerror(errno));
    if (fp) fclose(fp);
    unlink(tmpfile);
    stopSaving(0);
    return C_ERR;
}

因为redis是单线程模型,所以在save的过程中处理不了请求,单线程模型可以save的过程中不会有数据变化,但save可能会持续很久,这会导致redis无法正常处理读写请求,对于线上服务来说这是非常致命的,所以redis还提供了bgsave命令,它可以在不影响正常读写的情况下执行save操作,我们来看下具体实现。

bgsave命令

bgsave提供了后台生成rdb文件的功能,bg含义就是background,具体怎么实现的? 其实就是调用fork() 生成了一个子进程,然后在子进程中完成了save的过程。

void bgsaveCommand(client *c) {
    int schedule = 0;

    /* The SCHEDULE option changes the behavior of BGSAVE when an AOF rewrite
     * is in progress. Instead of returning an error a BGSAVE gets scheduled. */
    if (c->argc > 1) {
        if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"schedule")) {
            schedule = 1;
        } else {
            addReplyErrorObject(c,shared.syntaxerr);
            return;
        }
    }

    rdbSaveInfo rsi, *rsiptr;
    rsiptr = rdbPopulateSaveInfo(&rsi);

    if (server.child_type == CHILD_TYPE_RDB) {
        addReplyError(c,"Background save already in progress");
    } else if (hasActiveChildProcess()) {
        if (schedule) {
            server.rdb_bgsave_scheduled = 1;  // 如果bgsave已经在执行中了,这次执行会放到serverCron中执行
            addReplyStatus(c,"Background saving scheduled");
        } else {
            addReplyError(c,
            "Another child process is active (AOF?): can't BGSAVE right now. "
            "Use BGSAVE SCHEDULE in order to schedule a BGSAVE whenever "
            "possible.");
        }
    } else if (rdbSaveBackground(server.rdb_filename,rsiptr) == C_OK) {
        addReplyStatus(c,"Background saving started");
    } else {
        addReplyErrorObject(c,shared.err);
    }
}

int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
    pid_t childpid;

    if (hasActiveChildProcess()) return C_ERR;

    server.dirty_before_bgsave = server.dirty;
    server.lastbgsave_try = time(NULL);
    // 创建子进程,redisFork实际就是对fork的封装 
    if ((childpid = redisFork(CHILD_TYPE_RDB)) == 0) {
        int retval;

        /* 子进程 */
        redisSetProcTitle("redis-rdb-bgsave");
        redisSetCpuAffinity(server.bgsave_cpulist);
        retval = rdbSave(filename,rsi);
        if (retval == C_OK) {
            sendChildCowInfo(CHILD_INFO_TYPE_RDB_COW_SIZE, "RDB");
        }
        exitFromChild((retval == C_OK) ? 0 : 1);
    } else {
        /* 父进程 */
        if (childpid == -1) {
            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 %ld",(long) childpid);
        server.rdb_save_time_start = time(NULL);
        server.rdb_child_type = RDB_CHILD_TYPE_DISK;
        return C_OK;
    }
    return C_OK; /* unreached */
}

bgsave其实就是把save的流程放到子进程里执行,这样就不会阻塞到父进程了。我最开始看到这里的时候有个问题,父进程在持续读写内存的情况下子进程是如何保存某一时刻快照的? 这个redid中没有特殊处理,还是依赖了操作系统提供的fork()。
当一个进程调用fork()时,操作系统会复制一份当前的进程,包括当前进程中的内存内容。所以可以认为只要fork()成功,当前内存中的数据就被全量复制了一份。当然具体实现上内核为了提升fork()的性能,使用了copy-on-write的技术,只有被复制的数据在被父进程或者子进程改动时才会真正拷贝。

serverCron

上面生成rdb的两种方式都是被动触发的,redis也提供定期生成rdb的机制。redis关于rdb生成的配置如下:

save <seconds> <changes> 
## 例如
save 3600 1 # 3600秒内如果有1条写书就生成rdb
save 300 100 # 300秒内如果有100条写书就生成rdb
save 60 10000 # 60秒内如果有1000条写书就生成rdb

定期生成rdb的实现在server.c 中的serverCron中。serverCron是redis每次执行完一次eventloop执行的定期调度任务,里面就有rdb和aof的执行逻辑,rdb相关具体如下:

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
    /*
    . 略去其他代码
    */
    /* 检测bgsave和aof重写是否在执行过程中 */
    if (hasActiveChildProcess() || ldbPendingChildren())
    {
        run_with_period(1000) receiveChildInfo();
        checkChildrenDone();
    } else {
        /* If there is not a background saving/rewrite in progress check if
         * we have to save/rewrite now. */
        for (j = 0; j < server.saveparamslen; j++) {
            struct saveparam *sp = server.saveparams+j;

            /* 检查是否达到了执行save的标准 */
            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);
                rdbSaveBackground(server.rdb_filename,rsiptr);
                break;
            }
        }
    }
    /*
    . 略去其他代码
    */
    /* 如果上次触发bgsave时已经有进程在执行了,就会标记rdb_bgsave_scheduled=1,然后放到serverCron
     * 中执行 
     */
    if (!hasActiveChildProcess() &&
        server.rdb_bgsave_scheduled &&
        (server.unixtime-server.lastbgsave_try > CONFIG_BGSAVE_RETRY_DELAY ||
         server.lastbgsave_status == C_OK))
    {
        rdbSaveInfo rsi, *rsiptr;
        rsiptr = rdbPopulateSaveInfo(&rsi);
        if (rdbSaveBackground(server.rdb_filename,rsiptr) == C_OK)
            server.rdb_bgsave_scheduled = 0;
    }
    /*
    . 略去其他代码
    */
}

rdb文件格式

rdb的具体文件格式相对比较简单,具体如下:

----------------------------#
52 45 44 49 53              # 魔术 "REDIS"
30 30 30 33                 # ASCII码rdb的版本号 "0003" = 3
----------------------------
FA                          # 辅助字段
$string-encoded-key         # 可能包含多个元信息 
$string-encoded-value       # 比如redis版本号,创建时间,内存使用量……...
----------------------------
FE 00                       # redis db号. db number = 00
FB                          # 标识db的大小
$length-encoded-int         # hash表的大小(int)
$length-encoded-int         # expire hash表的大小(int)
----------------------------# 从这里开始就是具体的k-v数据 
FD $unsigned-int            # 数据还有多少秒过期(4byte unsigned int)
$value-type                 # 标识value数据类型(1 byte)
$string-encoded-key         # key,redis字符串类型(sds)
$encoded-value              # value, 类型取决于 $value-type
----------------------------
FC $unsigned long           # 数据还有多少毫秒过期(8byte unsigned long)
$value-type                 # 标识value数据类型(1 byte)
$string-encoded-key         # key,redis字符串类型(sds) 
$encoded-value              # value, 类型取决于 $value-type
----------------------------
$value-type                 # redis数据key-value,没有过期时间
$string-encoded-key
$encoded-value
----------------------------
FE $length-encoding         # FE标识前一个db的数据结束,然后再加上数据的长度 
----------------------------
...                         # 其他redis db中的k-v数据, ...
FF                          # FF rdb文件的结束标识  
8-byte-checksum             ## 最后是8byte的CRC64校验和 

总结

rdb在一定程度上保证了redis实例在异常宕机时数据不丢,当因为是定期生成的rdb快照,在生成快照后产生的变动无法追加到rdb文件中,所以rdb无法彻底保证数据不丢,为此redis又提供了另外一种数据持久化机制aof,我们将在下篇文章中看到。另外,在执行bgsave的时候高度依赖于操作系统的fork()机制,这也是会带来很大的性能开销的,详见Linux fork隐藏的开销-过时的fork(正传)

参考资料

  1. Redis RDB 持久化详解
  2. https://rdb.fnordig.de/file_format.html
  3. Redis Persistence
  4. Linux fork隐藏的开销-过时的fork(正传)
本文是Redis源码剖析系列博文,同时也有与之对应的Redis中文注释版,有想深入学习Redis的同学,欢迎star和关注。
Redis中文注解版仓库:https://github.com/xindoo/Redis
Redis源码剖析专栏:https://zxs.io/s/1h
如果觉得本文对你有用,欢迎一键三连
点赞
收藏
评论区
推荐文章
Stella981 Stella981
4年前
Redis持久化RDB和AOF实现原理
Redis持久化RDB和AOF为什么Redis需要持久化?因为Redis属于内存型数据库,数据是储存在内存当中的,当遇到不可抗力因素,比如断电,那么储存在内存中的数据就会丢失。所以为了保证数据的完整性,我们需要做持久化操作,来保证数据的完整性。Redis中都有哪些持久化机制?Redis早
Stella981 Stella981
4年前
Redis单实例数据迁移到集群
迁移步骤:(1)停掉项目,停止对redis单实例的数据写入。(2)登录单实例redis客户端通过命令keys\查看当前单实例缓存的所有数据条数,记录下来。(3)假如单实例redis同时开启了RDB和AOF,只要AOF文件就可以了,因为当AOF和RDB同时存在的时候,Redis还是会先加载AOF文件的,在单实例redis上执行BGREW
Stella981 Stella981
4年前
NoSQL数据库Redis和MongoDB
redis简介一些特点:Redis的读写性能极高,并且有丰富的特性(发布/订阅、事务、通知等)。Redis支持数据的持久化(RDB和AOF两种方式),可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。Redis支持多种数据类型,包括:string、hash、list、set,zset、bitm
Stella981 Stella981
4年前
Redis工具之redis_rdb_tools
  redis\_rdb\_tools工具的介绍:解析redis(https://www.oschina.net/action/GoToLink?urlhttp%3A%2F%2Fwww.ttlsa.com%2Fredis%2F)的dump.rdb文件,分析内存,以JSON格式导出数据。|提供的功能有:  1.生成内存报告  2.
Stella981 Stella981
4年前
Redis 的落地策略
因为之前使用redis一般都只做热数据处理,没有考虑过落地方案,因此,通过很多次不同的交流,发现落地也挺重要的,特来学习一般。落地策略我们知道,redis是纯内存数据库,一旦发生宕机,数据就会丢失,因此,Redis的落地策略其实就是持久化(Persistence),主要有以下2种策略:1.RDB:定时快照方式(snapsho
Stella981 Stella981
4年前
Redis持久化的几种方式——深入解析RDB
Redis 的读写都是在内存中,所以它的性能较高,但在内存中的数据会随着服务器的重启而丢失,为了保证数据不丢失,我们需要将内存中的数据存储到磁盘,以便Redis重启时能够从磁盘中恢复原有的数据,而整个过程就叫做Redis持久化。!image.png(https://oscimg.oschina.net/oscnet/232e657dae2
Stella981 Stella981
4年前
Redis——持久化数据
Redis被称为是内存数据库,那是因为它会将其所有数据存储在内存里,因此Redis具有强劲的速度性能,但是,也正因为数据存储在内存中,当Redis重启后,所有存储在内存的数据就会丢失。为了使得数据持久化,Redis提供了两种方式:RDB方式和AOF方式。一、RDB方式RDB方式的持久化是通过快照(snapshotting)完成的,
Stella981 Stella981
4年前
Redis—持久化
一、持久化简介Redis的数据全部存储在内存中,如果突然宕机,数据就会全部丢失,因此必须有一套机制来保证Redis的数据不会因为故障而丢失,这种机制就是Redis的持久化机制,它会将内存中的数据库状态保存到磁盘中。持久化发生了什么|从内存到磁盘
Stella981 Stella981
4年前
Redis 持久化(10)
持久化机制Redis速度快,很大一部分原因是因为它所有的数据都存储在内存中。如果断电或者宕机,都会导致内存中的数据丢失。为了实现重启后数据不丢失,Redis提供了两种持久化的方案,一种是RDB快照(RedisDataBase),一种是AOF(AppendOnlyFile)。RDBRDB是Redis默认的持久化方案。当满足一定条
Stella981 Stella981
4年前
Redis持久化存储详解(一)
为什么要做持久化存储?持久化存储是将Redis存储在内存中的数据存储在硬盘中,实现数据的永久保存。我们都知道Redis是一个基于内存的nosql数据库,内存存储很容易造成数据的丢失,因为当服务器关机等一些异常情况都会导致存储在内存中的数据丢失。持久化存储分类在Redis中,持久化存储分为两种。一种是aof日志追加的方式
子非鱼 子非鱼
3年前
Redis高级
第一章Redis的持久化由于redis是一个内存数据库,所有的数据都是保存在内存当中的,内存当中的数据极易丢失,所以redis的数据持久化就显得尤为重要,在redis当中,提供了两种数据持久化的方式,分别为RDB以及AOF,且Redis默认开启的数据持久化方式为RDB方式。1、RDB持久化方案Redis会定期保存数据快照至一个rbd文件中,并在启动时自动
智能合约
智能合约
Lv1
等待着大雪的纷纷扬扬,把每一个梦筑成洁白的巢。
文章
4
粉丝
0
获赞
0