redis源代码分析 – persistence

redis有全量(save/bgsave)和增量(aof)的持久化命令。

全量的原理就是遍历里所有的DB,在每个bucket,读取链表的key和value并写入dump.rdb文件(rdb.c 405)。

save命令直接调度rdbSave函数,这会阻塞主线程的工作,通常我们使用bgsave。

bgsave命令调度rdbSaveBackground函数启动了一个子进程然后调度了rdbSave函数,子进程的退出状态由 serverCron的backgroundSaveDoneHandler来判断,这个在复制这篇文章里讲到过,这里就不罗嗦了。

除了直接的save、bgsave命令之外,还有几个地方还调用到rdbSaveBackground和rdbSave函数。

shutdown:redis关闭时需要对数据持久化,保证重启后数据一致性,会调用rdbSave()。

flushallCommand:清空redis数据后,如果不做立即执行一个rdbSave(),出现crash后,可能会载入含有老数据的dump.rdb。

void flushallCommand(redisClient *c) {
  touchWatchedKeysOnFlush(-1);
  server.dirty += emptyDb();      // 清空数据
  addReply(c,shared.ok);
  if (server.bgsavechildpid != -1) {
    kill(server.bgsavechildpid,SIGKILL);
    rdbRemoveTempFile(server.bgsavechildpid);
  }
  rdbSave(server.dbfilename);    //没有数据的dump.db
  server.dirty++;
}

sync:当master接收到slave发来的该命令的时候,会执行rdbSaveBackground,这个以前也有提过。

数据发生变化:在多少秒内出现了多少次变化则触发一次bgsave,这个可以在conf里配置

for (j = 0; j < server.saveparamslen; j++) {
  struct saveparam *sp = server.saveparams+j;
   if (server.dirty >= sp->changes && now-server.lastsave > sp->seconds) {
     rdbSaveBackground(server.dbfilename);
     break;
  }
}

增量备份就是aof,原理有点类似redo log。每次执行命令后如出现数据变化,会调用feedAppendOnlyFile,把数据写到server.aofbuf里。

void call(redisClient *c, struct redisCommand *cmd) {
  long long dirty;
  dirty = server.dirty;
  cmd->proc(c);        //执行命令
  dirty = server.dirty-dirty;
  if (server.appendonly && dirty)
    feedAppendOnlyFile(cmd,c->db->id,c->argv,c->argc);

待到下次循环的before_sleep函数会通过flushAppendOnlyFile函数把server.aofbuf里的数据write到append file里。
可以在redis.conf里配置每次write到append file后从page cache刷新到disk的规律。

# appendfsync always
appendfsync everysec
# appendfsync no

参数的原理MySQL的innodb_flush_log_at_trx_commit一样,是个比较影响io的一个参数,需要在高性能和不丢数据之间做tradeoff。软件的优化就是tradeoff的过程,没有银弹。

一个疑问先写到server.aofbuf,然后再写到数据文件,过程中如果crash会不会丢数据呢?

答案是不会,为何?我们来看函数执行的步骤:

call()
feedAppendOnlyFile()
下一次循环
beforeSleep()-->flushAppendOnlyFile()
aeMain()--->sendReplyToClient()

只有执行完了flush之后才会通知客户端数据写成功了,所以如果在feed和flush之间crash,客户接会因为进程退出接受到一个fin包,也就是一个错误的返回,所以数据并没有丢,只是执行出了错。

redis crash后,重启除了利用rdb重新载入数据外,还会读取append file(redis.c 1561)加载镜像之后的数据。

激活aof,可以在redis.conf配置文件里设置

appendonly yes

也可以通过config命令在运行态启动aof

cofig set appendonly yes

aof最大的问题就是随着时间append file会变的很大,所以我们需要bgrewriteaof命令重新整理文件,只保留最新的kv数据。

下面这个根据图形来描述一下aof的全过程,绿色的为aof.c里的函数,顺序从左到右。

启动appendly,或者config set appendonly yes 都会触发函数rewriteAppendOnlyFileBackground,bgrewriteaof也调用该函数。

实际最后调用的函数是rewriteAppendOnlyFile,这个函数与rdbSave和类似。保存全库的kv数据。

在子进程做快照的过程中,kv的变化是先写到aofbuf里。如果存在bgrewritechildpid进程,变化数据还要写到server.bgrewritebuf里(aof.c 177)。
等子进程完成快照退出之时,由backgroundRewriteDoneHandler函数再把bgrwritebuf和全镜像两部分数据进行合并(aof.c 673)。

合并后的aof文件才是最新的全库的镜像数据。

ps:在最新的2.2.11版本的stopAppendOnly函数里存在一个bug,应该杀的进程是bgrewriteachilidpid进程,bgsavechildpid不不幸中枪,和Salvatore Sanfilippo邮件交流了一下,他承认了这个bug。 说2.2版本忘记改了。

6 Comments

  1. xiangzhong 说道:

    “一个疑问先写到server.aofbuf,然后再写到数据文件,过程中如果crash会不会丢数据呢?”,那么这里会不会导致客户端重复写。其它这条之前已经写入aof文件?

  2. allen 说道:

    博主说到”一个疑问先写到server.aofbuf,然后再写到数据文件,过程中如果crash会不会丢数据呢?

    答案是不会,为何?我们来看函数执行的步骤:”
    这句话应该有个条件把,应该是在server.appendfsync == APPENDFSYNC_ALWAYS的情况下,才不会丢数据的.
    因为当”server.appendfsync == APPENDFSYNC_EVERYSEC”,redis是交给其他”线程”来fsync的,因此,在这种情况下是有可能丢数据的.—redis.2.4.3
    随便谈谈,如过说错了,请指正.

  3. allen 说道:

    博主说到”一个疑问先写到server.aofbuf,然后再写到数据文件,过程中如果crash会不会丢数据呢?答案是不会,为何?我们来看函数执行的步骤:”
    这句话应该有个条件把,应该是在server.appendfsync == APPENDFSYNC_ALWAYS的情况下,才不会丢数据的.
    因为当”server.appendfsync == APPENDFSYNC_EVERYSEC”,redis是交给其他”线程”来fsync的,因此,在这种情况下是有可能丢数据的.—redis.2.4.3
    随便谈谈,如过说错了,请指正.

  4. [...] 4.REDIS源代码分析 – PERSISTENCE [...]

Leave a Reply