[Redis] Redis持久化机制的源码分析

文章基于Redis-4.0版本,介绍AOF持久化策略的具体实现过程,包括AOF持久化的触发,文件追加、写入、同步的代码实现,启动阶段的数据还原实现,AOF重写的触发与实现等。


1 AOF持久化

1.1 AOF触发

Redis执行命令时都会先建立一个客户端,然后由客户端去和服务器连接, redis的命令执行中有一个核心部分,就是call()方法,call函数声明如下:

1
2
/* src/server.c/call() */
void call(client *c, int flags)

其中client代表客户端,flags是一个特殊标识,当flags为CLIENT_FORCE_AOF时,标志着强制服务器将当前执行的命令写入到AOF文件当中。除此之外,在call函数执行过程中还维护着一个变量dirty用来标识当前执行的命令操作是否改变服务器数据,如下所示:

1
2
3
4
dirty = server.dirty;  
c->cmd->proc(c); //实际的命令执行函数
dirty = server.dirty-dirty;
if (dirty < 0) dirty = 0;

通过以上两个判断条件,就可以设置命令传播的标识,进而调用传播方法:

1
2
3
4
5
6
if (dirty)   propagate_flags |= (PROPAGATE_AOF|PROPAGATE_REPL);  
if (c->flags & CLIENT_FORCE_AOF) propagate_flags |= PROPAGATE_AOF;

//Call propagate only if at least one of AOF/REPL propagation is needed
if (propagate_flags != PROPAGATE_NONE && !(c->cmd->flags & CMD_MODULE))
propagate(c->cmd,c->db->id,c->argv,c->argc,propagate_flags);

1.2 文件追加、写入、同步

propagate()函数的作用是将命令传播给AOF以及slave中(slave是Redis集群部分的内容),propagate()将命令传播到AOF中是通过调用feedAppendOnlyFile()函数实现的,在调用该函数之前,首先需要检查AOF机制是否已开启:

1
2
3
4
5
/* src/server.c/propagate() */  
void propagate(struct redisCommand *cmd, int dbid, robj **argv, int argc, int flags) {
if (server.aof_state != AOF_OFF && flags & PROPAGATE_AOF)
feedAppendOnlyFile(cmd,dbid,argv,argc);
}

feedAppendOnlyFile()位于src/aof.c中,该函数将命令追加至aof_buf中,如果正在执行AOF重写,还需要将其追加到重写缓冲区中,具体实现过程包括以下四个步骤:

  1. 使用 SELECT 命令,显式设置数据库,确保之后的命令被设置到正确的数据库;
  2. 将命令和命令参数还原为协议格式;
  3. 将命令追加到aof_buf中(使用函数sdscatlen()实现,该函数为Redis自定义的,针对sds结构实现的追加函数,位于src/sds.c中);
  4. 如果BGREWRITEAOF正在进行,还需要将命令追加到重写缓存中(使用函数aofRewriteBufferAppend()实现,位于src/aof.c中)。
1
2
3
4
5
6
7
8
9
10
11
/* Append to the AOF buffer. This will be flushed on disk just before 
* of re-entering the event loop, so before the client will get a
* positive reply about the operation performed. */
if (server.aof_state == AOF_ON)
server.aof_buf = sdscatlen(server.aof_buf,buf,sdslen(buf));
/* If a background append only file rewriting is in progress we want to
* accumulate the differences between the child DB and the current one
* in a buffer, so that when the child process will do its work we
* can append the differences to the new append only file. */
if (server.aof_child_pid != -1)
aofRewriteBufferAppend((unsigned char*)buf,sdslen(buf));

AOF文件的写入和同步采用的是src/aof.c/flushAppendOnlyFile函数。该函数在src/server.c/beforeSleep中会被调用,而beforeSleep函数是在处理client事件之前执行的(事件循环函数aeMain先执行beforesleep,然后执行aeProcessEvents),因此,server.aof_buf中的值会在向client发送响应之前刷新到磁盘上。

flushAppendOnlyFile(int force)函数的具体实现过程中,根据不同的同步策略以及后台是否有正在进行fsync操作,共分为以下几种情况:

  1. 判断是否写入,同步策略为everysecond(server.aof_fsync == AOF_FSYNC_EVERYSEC),且没有要求强制写入(!force),且有fsync正在后台执行:
    • 之前没有推迟过write操作,则记录下时间,直接返回(如果此时强制执行write的话,服务器主线程将阻塞在write上面);
    • 之前推迟过write操作,但推迟时间<2秒,直接返回;
    • 推迟时间>=2秒,不返回,继续执行。
  2. 执行写入操作,调用aofWrite函数
  3. 判断是否同步:
    • 同步策略为always(server.aof_fsync == AOF_FSYNC_ALWAYS),执行同步操作(调用aof_fsync函数,在Linux系统,该函数使用fdatasync实现,在其他系统中,使用fsync实现)。
    • 同步策略为everysecond,且距离上次写操作已超过1秒,且没有fsync在后台执行,则后台执行同步操作(调用aof_background_fsync函数)。

2 数据还原

Redis启动之后,在src/server.c/main()函数中调用loadDataFromDisk(),当AOF为开启状态时,该函数会继续调用src/aof.c/loadAppendOnlyFile(),在该函数中会通过创建伪客户端的方式,遍历执行AOF文件的命令,还原数据库状态。


3 AOF重写

持续运行的Redis服务器需要定期对自身的资源和状态进行检查和调整,从而确保服务器可以长期、稳定地运行,这些定期操作由server.c/serverCron函数负责执行,它的主要工作包括:

  • 更新服务器的各类统计信息,比如时间、内存占用、数据库占用情况等;
  • 清理数据库中的过期键值对;
  • 关闭和清理连接失效的客户端;
  • 触发BGSAVE或者AOF重写,并处理之后由BGSAVE和AOF重写引发的子进程停止。

Redis服务器以周期性事件的方式来运行serverCron函数,在服务器运行期间,每秒调用server.hz次,直到服务器关闭为止。

3.1 BGSAVE和BGREWRITEAOF的触发

在serverCron()函数中与Redis持久化相关的检查以及处理流程如下图所示,具体实现包括:

serverCron中相关操作流程

  • 如果BGSAVE和BGREWRITEAOF都没有在执行,但是有一个BGREWRITEAOF在等待(server.aof_rewrite_scheduled),那么执行BGREWRITEAOF(调用函数src/aof.c/rewriteAppendOnlyFileBackground)
1
2
3
4
5
6
/* Start a scheduled AOF rewrite if this was requested by the user while 
* a BGSAVE was in progress. */
if (server.rdb_child_pid == -1 && server.aof_child_pid == -1 &&
server.aof_rewrite_scheduled) {
rewriteAppendOnlyFileBackground();
}
  • 如果BGSAVE和BGREWRITEAOF都没有在执行,而且也没有BGREWRITEAOF在等待,那么检查是否需要执行它们
    • BGSAVE:检查m秒内是否发生了超过n次的变化(对应于配置文件中的save m n),BGSAVE操作由函数src/rdb.c/rdbSaveBackground实现
    • BGREWRITEAOF:
      • 当前AOF文件大小大于执行 BGREWRITEAOF 所需的最小大小,即server.aof_rewrite_min_size
      • 当前AOF文件大小和最后一次重写后的大小之间的比率等于或者大于指定的增长百分比(auto-aof-rewrite-perc)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* Trigger an AOF rewrite if needed */  
if (server.rdb_child_pid == -1 && server.aof_child_pid == -1 &&
server.aof_rewrite_perc &&
server.aof_current_size > server.aof_rewrite_min_size) {
// 上一次完成 AOF 写入之后,AOF 文件的大小
long long base = server.aof_rewrite_base_size ?
server.aof_rewrite_base_size : 1;
// AOF 文件当前的体积相对于 base 的体积的百分比
long long growth = (server.aof_current_size*100/base) - 100;
// 如果增长体积的百分比超过了 growth ,那么执行 BGREWRITEAOF
if (growth >= server.aof_rewrite_perc) {
rewriteAppendOnlyFileBackground();
}
}
  • 如果存在BGSAVE或者BGREWRITEAOF在执行,则检查其是否已经执行完毕,并处理因此而引发的子进程停止
  • 如果服务器开启了AOF持久化功能,并且AOF缓冲区里面还有待写人的数据,那么serverCron会调用相应的程序,将AOF缓冲区中的内容写入到AOF文件里面
1
2
3
4
/* AOF postponed flush: Try at every cron cycle if the slow fsync 
* completed. */
if (server.aof_flush_postponed_start)
flushAppendOnlyFile(0);

在serverCron函数中会通过检查BGREWRITEAOF的两个自动触发条件来执行AOF重写,但除此之外,用户还可以通过在客户端输入bgwriteaof命令来手动触发AOF重写。与其他命令的处理流程相同,bgwriteaof命令对应的命令处理函数为aof.c/bgrewriteaofCommand():

1
2
3
4
5
6
7
8
9
10
11
12
void bgrewriteaofCommand(client *c) {  
if (server.aof_child_pid != -1) {
addReplyError(c,"Background AOF rewriting already in progress");
} else if (server.rdb_child_pid != -1) {
server.aof_rewrite_scheduled = 1;
addReplyStatus(c,"Background append only file rewriting scheduled");
} else if (rewriteAppendOnlyFileBackground() == C_OK) {
addReplyStatus(c,"Background append only file rewriting started");
} else {
addReply(c,shared.err);
}
}

在该函数中,AOF重写也是通过调用rewriteAppendOnlyFileBackground函数来实现的。但当有BGSAVE在执行时(server.rdb_child_pid != -1),BGREWRITEAOF会等待(server.aof_rewrite_scheduled = 1),如前文所述,等待的BGREWRITEAOF会在serverCron函数中被执行。

3.2 AOF重写的实现

AOF重写是依靠rewriteAppendOnlyFileBackground函数实现的,该函数的处理过程包括:

  1. 使用fork创建一个子进程;
  2. 子进程调用aof.c/rewriteAppendOnlyFile函数在一个临时文件里写入能够反映当前db状态的数据和命令,此时父进程会把这段时间内执行的能够改变当前db数据的命令放到重写缓冲区中;
  3. 当子进程退出时,父进程收到信号,将上面的重写缓冲区中的数据flush到临时文件中,然后将临时文件rename成新的aof文件。

子进程调用rewriteAppendOnlyFile函数后,在该函数中会继续调用实际的重写函数aof.c/rewriteAppendOnlyFileRio,该函数遍历db中的每条数据,取出键,取出值,然后根据值的类型选择适当的命令来进行保存,然后写入并同步AOF临时文件中。

在serverCron函数中,会周期性的检查BGREWRITEAOF子进程是否已退出,当父进程收到退出信号后,会调用aof.c/backgroundRewriteDoneHandler函数完成后续处理,包括:

  1. 调用aof.c/aofRewriteBufferWrite函数,将累计的AOF重写缓冲区的内容追加到AOF临时文件中;
  2. 将AOF 临时文件rename,替换现有的AOF文件。

参考内容:《Redis设计与实现》,黄健宏著