开坑专题——Redis Redis持久化

612 阅读5分钟

Redis是一个内存数据库,如果不将数据持久化到磁盘里,在服务器掉电或进程退出后,服务内的数据将会全部丢失。所以,持久化是Redis必不可少的一项功能。

- RDB持久化

RDB全称为Redis Database,使用快照形式将某一时刻内存中数据状态记录下来。这样一来即便是服务宕机,也能够通过RDB文件恢复到快照时的数据状态。

那么如何使用RDB这个功能呢?
通过客户端访问redis服务,使用sava或bgsave命令可以生成快照文件。从命令字面意思就大概能明白,save是同步处理,bgsave是后台处理


接下来我们从源码角度看一下save命令和bgsave的实现

  • save
int rdbSave(char *filename){
    //创建RDB文件
    //写入魔数+RDB版本号
    //遍历redis数据库
    for(){
        while(){
            //获取当前数据库所有键值对及对应过期时间
            //过期键不写入文件
            //保存对象类型、键、值
        }
    }
    // 写入EOF常量,表示rdb内容结束
    // 写入check_sum校验和,用来判断文件是否损坏
}

由此可以了解到rdb文件内容的结构

魔数rdb版本号databaseEOFcheck_sum
REDIS4字节1字节8字节
  • bgsave

fork()方法执行后会变成两个进程执行,

  • 父进程返回fork出的子进程ID
  • 子进程返回0
  • fork失败返回-1
int rdbSaveBackground(char *filename) {
    // 创建子进程
    pid = fork();
    if (pid == 0) {
        //关闭网络通信
        rdbSave(filename);
        //向父进程发送完成信号
    } else {
        if(pid == -1) {
            //fork失败 报告失败
        }
        //关闭自动rehash
    }
}

生成快照时写命令采用写时复制技术
redis提供自动生成RDB文件。在redis.conf文件中找到这样一段配置,默认是注释掉的。注释也为我们举了例子来说明配置的用法,在second秒内达到changes次修改则会触发快照,只要任意一个save配置条件满足就会触发bgsave命令。

# Save the DB to disk.
#
# save <seconds> <changes>
...
# Unless specified otherwise, by default Redis will save the DB:
#   * After 3600 seconds (an hour) if at least 1 key changed
#   * After 300 seconds (5 minutes) if at least 100 keys changed
#   * After 60 seconds if at least 10000 keys changed
#
# You can set these explicitly by uncommenting the three following lines.
#
# save 3600 1
# save 300 100
# save 60 10000

设置的触发条件被保存在saveparams里,同时,redis通过dirty计数器和lastsave时间戳来记录自上次生成RDB文件之后数据库有多少次修改

struct redisServer{
    ...
    long long dirty;
    struct saveparam *saveparams;
    time_t lastsave;
    ...
};

struct saveparam {
    // 多少秒之内
    time_t seconds;
    
    // 发生多少次修改
    int changes;
};

通过redisCron来检查条件是否需要调用bgsave

int serverCron{
    ...
    //判断当前没有在处理的bgsave或bgrewriteaof
    for(int i = 0; i < server.saveparamslen; i++) {
        if(server.dirty >= saveparam.changes && server.unixtime - server.lastsave > saveparam.seconds ) {
            bgsave();
            break;
        }
    }
}
...
return 1000/server.hz; //返回下次执行时间间隔 默认100ms

- AOF持久化

与RDB快照文件不同,AOF通过记录服务器所有的写命令来实现宕机后恢复的。
aof文件内容采用文本形式记录,与通信协议使用的RESP2规则一致。以set key value 为例,在aof文件中会表示成*3\r\n$3\r\nset\r\n$3\r\nkey\r\n$5\r\nvalue

  • *3表示当前命令有三个部分
  • $n表示后面跟的命令、键、值的占用n个字节

同样的,我们需要调整配置来开启AOF,在redis.conf文件中将appendonly no 改为yes。配置文件中默认开启的是everysec模式

  • always 每次写命令执行完都会将命令回写到磁盘
  • everysec 将写命令存入aof文件缓存,通过serverCron每秒回写
  • no 将写命令存入缓存,通过操作系统控制何时回写
# appendfsync always
appendfsync everysec
# appendfsync no
AOF重写

因为aof文件记录所有key的变更记录,随着提供服务的时长,文件也会逐渐变大。通过aof文件进行恢复也会非常耗时,需要将历史命令再执行一遍。redis提供了aof rewrite,可以把多个操作同一key的命令重写为一条能够表示当前状态的命令。
aof重写需要重新将文件写回磁盘,需要耗费大量时间。所以,aof重写也采用fork子进程的方式避免阻塞主进程,在进行bgrewrite期间,写命令会记录到aof和aof重写缓冲区,以避免宕机导致数据丢失。

源码部分

struct redisServer{
    ...
    sds aof_buf;//aof缓存字段 sds类型,可以向后追加
    ...
};

在redis.c中的main方法最后会调用aeMain开启事件Loop,处理时间事件和文件事件。

void aeMain(aeEventLoop *eventLoop) {

    eventLoop->stop = 0;

    while (!eventLoop->stop) {

        // 如果有需要在事件处理前执行的函数,那么运行它
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);

        // 开始处理事件
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
    }
}

beforesleep方法中调用了flushAppendOnlyFile,将aof缓存刷入磁盘

void beforeSleep(struct aeEventLoop *eventLoop) {
    // ...
    flushAppendOnlyFile(0);
    // ...
}
void flushAppendOnlyFile(int force) {
    // ...
    if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
        // ...
        aof_fsync(server.aof_fd); // 同步策略为always时,尝试将缓存写到磁盘
        // ...
    } else if ((server.aof_fsync == AOF_FSYNC_EVERYSEC &&
                server.unixtime > server.aof_last_fsync)) {
        // 提交任务
        if (!sync_in_progress) aof_background_fsync(server.aof_fd);
        // ... 
    }
}

aof的rewrite同样也是通过serverCron

int serverCron{
    ...
    // 没有后台执行的rdb或aof,且有aof重写任务需要执行
    if (server.rdb_child_pid == -1 && server.aof_child_pid == -1 &&
        server.aof_rewrite_scheduled)
    {
        rewriteAppendOnlyFileBackground();
    }
    ...
}

int rewriteAppendOnlyFileBackground(void) {
    ...
    if ((childpid = fork()) == 0) {
        // 创建临时文件
        snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof", (int) getpid());
        if (rewriteAppendOnlyFile(tmpfile) == REDIS_OK) {
            // 重写成功的一些操作
        }
    } else {
        // 父进程操作
    }
}

int rewriteAppendOnlyFile(char *filename) {
    ...
    // 遍历所有数据库
    for(j = 0; j < server.dbnum; j++) {
        ...
        // 迭代所有的key
        while((de = dictNext(di)) != NULL) {
            //根据不同的对象类型进行相应的重写操作
        }
    }
}