Redis 持久化机制 —— RDB 持久化

545 阅读4分钟

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

产生背景

Redis 是一个键值对数据库服务器,服务器中通常包含着任意个非空数据库,而每个非空数据库中又可以包含任意哥键值对。我们将服务器中非空数据库以及它们的键值对统称为数据库状态

Redis 是内存数据库,它将自己的数据库状态存储在内存中。如果不讲存储在内存中的数据库状态保存到磁盘里,那么一旦服务器进程退出,服务器中的数据库状态也会消失不见

RDB 持久化方式及磁盘交互

Redis 提供了 RDB 的持久化方式,这个功能可以将 Redis 在内存中的数据库状态保存到磁盘里面,避免数据丢失。

RDB 持久化既可以手动执行,也可以根据服务器配置选项定期执行,该功能可以将某个时间点上的数据库状态保存在一个 RDB 文件中。

rdb文件创建.jpg

RDB 文件是一个压缩的二进制文件,通过该文件可以还原生成 RDB 文件时的数据库状态。

rdb文件载入.jpg

RDB 文件的创建与载入

有两个 Redis 命令可以用于生成 RDB 文件,一个是 SAVE,另一个是 BGSAVE.

  • SAVE 命令会阻塞 Redis 服务器进程,直到 RDB 文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求
  • BGSAVE 命令会派生一个子进程,然后由子进程负责创建 RDB 文件,服务器进程(父进程)继续处理命令请求。

创建 RDB 文件的实际工作是由 rdb.c/rdbSave 函数完成。

void saveCommand(client *c) {
    if (server.rdb_child_pid != -1) {
        addReplyError(c,"Background save already in progress");
        return;
    }
    rdbSaveInfo rsi, *rsiptr;
    rsiptr = rdbPopulateSaveInfo(&rsi);
    
    // 创建 RDB 文件
    if (rdbSave(server.rdb_filename,rsiptr) == C_OK) {
        addReply(c,shared.ok);
    } else {
        addReply(c,shared.err);
    }
}
void bgsaveCommand(client *c) {
    // ...
    if (server.rdb_child_pid != -1) {
        addReplyError(c,"Background save already in progress");
    } else if (hasActiveChildProcess()) {
        // ...
    } else if (rdbSaveBackground(server.rdb_filename,rsiptr) == C_OK) {
        addReplyStatus(c,"Background saving started");
    } else {
        addReply(c,shared.err);
    }
}

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

    // ...
    
    // 创建子进程
    if ((childpid = redisFork(CHILD_TYPE_RDB)) == 0) {
        // ...
        
        // 保存
        retval = rdbSave(filename,rsi);
        
        // ...
    } else {
        // ...
    }
    return C_OK; /* unreached */
}
/* Save the DB on disk. Return C_ERR on error, C_OK on success. */
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);
    startSaving(RDBFLAGS_NONE);

    if (server.rdb_save_incremental_fsync)
        rioSetAutoSync(&rdb,REDIS_AUTOSYNC_BYTES);

    if (rdbSaveRio(&rdb,&error,RDBFLAGS_NONE,rsi) == C_ERR) {
        errno = error;
        goto werr;
    }

    /* Make sure data will not remain on the OS's output buffers */
    if (fflush(fp)) goto werr;
    if (fsync(fileno(fp))) goto werr;
    if (fclose(fp)) { fp = NULL; goto werr; }
    fp = NULL;
    
    // ...
}

自动间隔性保存

用户可以通过 save 选项设置多个保存条件,但只要其中任意一个条件被满足,服务器就会执行 BGSAVE 命令

举个例子,如果我们向服务器提供一下配置:

save 900 1        # 服务器 900 秒内,对数据库进行至少 1 次修改
save 300 10       # 服务器 300 秒内,对数据库进行至少 10 次修改
save 60 10000     # 服务器 60 秒内,对数据库进行至少 10000 次修改

那么只要满足以上三个条件中的任意一个,BGSAVE 命令就会被执行

设置保存条件

服务器程序会根据 save 选项所设置的保存条件,设置服务器状态 redisServer 结构的 saveparams 属性

struct redisServer {
    // ...
    
    // 记录了保存条件的数组
    struct saveparam *saveparams;
    
    // ...
}

struct saveparam {
    // 秒数
    time_t seconds;
    // 修改数
    int change;
}

比如说上述save选项的值在服务器中存储的 saveparam 数组,如下图

未命名文件 (3).jpg

dirty 计数器和 lastsave 属性

除了 saveparams 数组之外,服务器状态还维持着一个 dirty 计数器,以及一个 lastsave 属性

  • dirty 计数器记录距离上一次成功执行 SAVE 或者 BGSAVE 命令后,服务器对数据库状态进行了多少次修改
  • lastsave 是一个时间戳,记录了服务器上一次成功执行 SAVE 或者 BGSAVE 命令的时间
struct redisServer {
    // ...
    
    // 修改计数器
    long long dirty;
    // 上一次执行保存的时间
    time_t lastsave;
    
    // ...
}

当服务器成功执行一个数据库修改命令后,程序就会对 dirty 计数器进行更新: 命令修改了多少次数据库,dirty 计数器就增加多少

检查保存条件是否满足

Redis 服务器周期性操作函数 serverCron 默认每隔 100ms 就会执行一次,该函数用于对正在运行的服务器进行维护,其中一项工作就是通过遍历 saveparams 数组中的所有保存条件,检查是否存在满足的条件,如果有,则执行 BGSAVE