Redis的持久化分为RDB和AOF,各有优缺点,在redis.conf中默认配置只开启了RDB的持久化,但是RDB持久化是定时job驱动的且配合一些条件
在X秒内Y个key改变了,触发异步任务保存数据库
RDB的文件是二进制的序列化协议数据,对于恢复数据比较快,通常是aof恢复数据的2倍速度。但是容易丢失一段时间的数据,如果对数据丢失容忍度低则需要考虑
aof的持久化方式。aof持久化配置策略可以为同步落盘和每秒落盘,或交由操作系统落盘数据(Redis负责将数据写入OS缓冲区)。aof持久化也有一定缺点,同步落盘会大大降低服务性能,每秒落盘是个折中选择。但是aof文件毕竟是redis协议格式的字符串写命令,文件大小会比rdb要大,恢复数据时性能也比较慢。redis需要fork出子进程,子进程伪装成client,一条一条读出aof的写命令发送给主进程执行回放,达到恢复数据的目的。
综上官方推荐开启混合持久化的方式,可使用下面配置
aof-use-rdb-preamble yes
本篇文章会解析几个问题,帮助深入理解AOF机制的实现原理和细节。
1.aof文件数据什么时候写入的?
2.aof文件刷盘的真实执行过程
3.aof文件重写执行过程(混合持久化怎么做的)
1.aof文件数据什么时候写入?
命令执行前的简要流程
call函数是redis执行命令的核心函数入口。在此之前的processInputBuffer函数已经从client的socket提取数据放入client.querybuf中,processCommand函数将querybuf的数据按照redis协议解析成redisCommand对象,以及参数解析成argv数组,argc数量。
执行命令
flags是用来控制操作流程的变量,根据某些二进制位的值决定是否做操作。
有两个重要的地方,一个是c->cmd->proc(c)执行客户端的命令,一个是propagate(c->cmd,c->db->id,c->argv,c->argc,propagate_flags)函数。
在执行命令前后通过server.dirty变量记录命令执行前的数据库key的脏值,每个写命令执行后都会改变server.dirty值。
所以通过dirty可以知道此命令是否为写命令,从而判断是否要执行propagate函数
void call(
// 客户端对象
client *c,
// 外部传进来的标志位,用来判断要做哪些操作
int flags) {
long long dirty, start, duration;
int client_old_flags = c->flags;
struct redisCommand *real_cmd = c->cmd;
// ......
/* 执行命令 */
// 这里的dirty记录redis每一次数据的变更,每次数据变更了dirty会自增,比如:setCommand hsetCommand等写命令
dirty = server.dirty;
start = ustime();
// 调用redisCommend的proc函数执行命令,所有的命令在 server.c的redisCommandTable可找到底层实现函数
c->cmd->proc(c);
duration = ustime()-start;
// 执行完命令后,看看之前的dirty会现在的dirty变化情况,有变化则是写命令,下面记录aof buf和主从复制会用来判断
dirty = server.dirty-dirty;
if (dirty < 0) dirty = 0;
/* 当服务器正在从磁盘加载数据且现在命令要执行lua脚本,不希望这个命令被记录在慢日志和统计,重置下flags相关位 */
if (server.loading && c->flags & CLIENT_LUA)
flags &= ~(CMD_CALL_SLOWLOG | CMD_CALL_STATS);
/* 记录命令到aof_buf和repl_backlog */
if (flags & CMD_CALL_PROPAGATE &&
(c->flags & CLIENT_PREVENT_PROP) != CLIENT_PREVENT_PROP)
{
int propagate_flags = PROPAGATE_NONE;
// dirty在这里用到,如果redis数据发生了变化,代表本次命令是写命令,就需要记录到aof_buf和repl_backlog
if (dirty) propagate_flags |= (PROPAGATE_AOF|PROPAGATE_REPL);
/* 如果命令指定要强制同步数据给slave或强制追加aof,那就不管是否写命令,都开启flags相关控制位 */
if (c->flags & CLIENT_FORCE_REPL) propagate_flags |= PROPAGATE_REPL;
if (c->flags & CLIENT_FORCE_AOF) propagate_flags |= PROPAGATE_AOF;
/*
做两件事。
1.写命令追加到aof_buf(持久化文件)和repl_backlog(增量同步缓冲区)
2.对于master->slave的数据同步,命令以redis协议格式写到每个client.buf/client.reply
*/
if (propagate_flags != PROPAGATE_NONE && !(c->cmd->flags & CMD_MODULE))
propagate(c->cmd,c->db->id,c->argv,c->argc,propagate_flags);
}
// .....
}
propagate函数执行,是有前提的,就是前面提到的当数据库数据发生了改变则需要执行。
void propagate(struct redisCommand *cmd, int dbid, robj **argv, int argc,
int flags)
{
// 追加命令到aof buf
if (server.aof_state != AOF_OFF && flags & PROPAGATE_AOF)
feedAppendOnlyFile(cmd,dbid, argv, argc);
// 写命令,主从复制,传输命令给slave
// 因为是写到所有slave的client.buf,在下次进入事件循环前 beforeSleep中发送给slave,
// 接着回复客户端写命令的执行结果,都是单线程完成的所以对于用户来说可认为是每条写命令同步广播给slave的
if (flags & PROPAGATE_REPL)
replicationFeedSlaves(server.slaves,dbid,argv,argc);
}
feedAppendOnlyFile函数主要做了3件事。1.把redisCommand按照redis协议转成字符串存入buf数组。buf字节数组是临时的sds字符串。2.把buf数组追加写入到aof buf数组。
aof buf就是aof文件的内存缓冲,每次aof数据写入不是立即写入到磁盘的aof文件中的。aof buf通过配置的刷盘策略,在一定的时间内写入到磁盘文件。
3.正在进行aof重写,则还要写入到aof rewrite buf中发送给子进程追加写入到新aof文件。
void feedAppendOnlyFile(
// 刚执行的命令
struct redisCommand *cmd,
// 在哪个数据库执行的
int dictid,
// 参数数组和个数
robj **argv, int argc) {
// 造个空sds数组,放redis协议格式的命令
sds buf = sdsempty();
robj *tmpargv[3];
/* 刚执行玩的命令所在数据库和上一次记录的aof写入数据库不一样,需要记录select切换数据库 */
if (dictid != server.aof_selected_db) {
char seldb[64];
snprintf(seldb,sizeof(seldb),"%d",dictid);
buf = sdscatprintf(buf,"*2\r\n$6\r\nSELECT\r\n$%lu\r\n%s\r\n",
(unsigned long)strlen(seldb),seldb);
server.aof_selected_db = dictid;
}
// 对特殊的 expire/set 系列命令做特殊处理,转成redis协议写入buf
if (cmd->proc == expireCommand || cmd->proc == pexpireCommand ||
cmd->proc == expireatCommand) {
/* Translate EXPIRE/PEXPIRE/EXPIREAT into PEXPIREAT */
buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);
} else if (cmd->proc == setexCommand || cmd->proc == psetexCommand) {
/* Translate SETEX/PSETEX to SET and PEXPIREAT */
tmpargv[0] = createStringObject("SET",3);
tmpargv[1] = argv[1];
tmpargv[2] = argv[3];
buf = catAppendOnlyGenericCommand(buf,3,tmpargv);
decrRefCount(tmpargv[0]);
buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);
} else if (cmd->proc == setCommand && argc > 3) {
int i;
robj *exarg = NULL, *pxarg = NULL;
/* Translate SET [EX seconds][PX milliseconds] to SET and PEXPIREAT */
buf = catAppendOnlyGenericCommand(buf,3,argv);
for (i = 3; i < argc; i ++) {
if (!strcasecmp(argv[i]->ptr, "ex")) exarg = argv[i+1];
if (!strcasecmp(argv[i]->ptr, "px")) pxarg = argv[i+1];
}
serverAssert(!(exarg && pxarg));
if (exarg)
buf = catAppendOnlyExpireAtCommand(buf,server.expireCommand,argv[1],
exarg);
if (pxarg)
buf = catAppendOnlyExpireAtCommand(buf,server.pexpireCommand,argv[1],
pxarg);
} else {
/* 对写命令转成redis协议写入buf数组 */
buf = catAppendOnlyGenericCommand(buf, argc, argv);
}
// buf其实是个sds字符串,上面已经记录了redis协议到buf中,现在追加到aof buf
// 在下次重新进入事件循环,beforeSleep中将aof_buf写到os或刷盘,aof_buf至少写到os buf或刷盘后才回复给客户端命令已执行
if (server.aof_state == AOF_ON)
server.aof_buf = sdscatlen(server.aof_buf, buf, sdslen(buf));
/*
aof_child_pid有值,代表有子进程在做aof重写,主进程fork子进程之前会创建3对管道用于子进程通信
主进程会将重写期间的写命令追加到aof_rewrite_buf_blocks缓冲,并等待管道可写时将aof_rewrite_buf_blocks传输给子进程持久化
子进程完成aof重写后,回放rewrite_buf的数据写入新aof文件。然后通知主进程,主进程会替换新旧aof文件
*/
if (server.aof_child_pid != -1)
aofRewriteBufferAppend((unsigned char*)buf, sdslen(buf));
sdsfree(buf);
}
以上就是aof文件数据写入过程,我们知道了当执行完一个写命令对于客户端来说是同步将数据写入到aof buf中的。不过这个过程是一个内存顺序写入操作,性能是有保证的。
2.aof文件落盘策略的真实执行过程
aof buf是什么时候写入磁盘的,写入磁盘是一个比较耗时的操作,必然不会放在执行命令的时候阻塞客户端的命令执行。应该是一个异步操作,实际上根据redis.conf配置决定是同步操作或异步操作。
这就要看beforeSleep函数。beforeSleep函数是每次进入redis处理文件事件循环前执行的逻辑,主要做不耗时的操作,例如:快速随机淘汰过期key,回复client数据(将client.buf写入socket),执行aof刷盘策略。如果是同步刷盘那beforeSleep的耗时就会比较高,甚至可能会使redis性能大幅下降!一般不推荐redis作持久化数据库,就单纯的把它当做缓存来用。更不要说开启同步刷盘策略
void beforeSleep(struct aeEventLoop *eventLoop) {
UNUSED(eventLoop);
// ......
/* aof buf写入OS buffer,至于是否fsync根据配置决定,有可能同步fsync或异步fsync,0代表本次fsync不是强制的 */
flushAppendOnlyFile(0);
}
将aof文件落盘的执行逻辑 flushAppendOnlyFile函数
void flushAppendOnlyFile(
// 每次常规的beforeSleep函数的aof fsync动作都不是强制的
// 强制aof刷盘只发生在关闭aof和redis准备退出的时候
int force) {
ssize_t nwritten;
int sync_in_progress = 0;
mstime_t latency;
// aofbuf空的就返回
if (sdslen(server.aof_buf) == 0) return;
if (server.aof_fsync == AOF_FSYNC_EVERYSEC)
// 如果是每秒将aof_buf刷盘,取异步阻塞刷盘的job数量,异步是指子线程执行阻塞刷盘job,不为0代表正在刷盘
sync_in_progress = bioPendingJobsOfType(BIO_AOF_FSYNC) != 0;
// 每秒刷盘策略是开启异步线程做的,所以这里要关注下异步任务是否正在刷盘
if (server.aof_fsync == AOF_FSYNC_EVERYSEC && !force) {
/*
这里可以看出aof_sync_everysec并不是精确的每秒刷盘
如果提交的aof job数量>0,上次aof fsync请求还在阻塞,代表磁盘IO负载大,且间隔没超过2秒,会暂缓aof写入OS buf
*/
if (sync_in_progress) {
if (server.aof_flush_postponed_start == 0) {
/* 如果aof_buf正在刷盘,本次又不强制刷,就推迟,记录下推迟开始的时间 */
server.aof_flush_postponed_start = server.unixtime;
return;
} else if (server.unixtime - server.aof_flush_postponed_start < 2) {
/* 上次记录的推迟开始时间到现在不到2秒,再推迟下 */
return;
}
/* 等待2秒是阈值,超过了就记个数且继续向下走,将aof_buf写入os buf */
server.aof_delayed_fsync++;
serverLog(LL_NOTICE,"Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis.");
}
}
latencyStartMonitor(latency);
// 系统调用write,将aof_buf写入os buf
nwritten = aofWrite(server.aof_fd,server.aof_buf,sdslen(server.aof_buf));
latencyEndMonitor(latency);
// ....
/* 到这里aof_buf已经写入os buf,所以重置延迟刷盘开始时间为0 */
server.aof_flush_postponed_start = 0;
// aof_buf写入OS缓冲的字节数量不对,处理错误
if (nwritten != (ssize_t)sdslen(server.aof_buf)) {
// 打日志,如果配置了同步刷盘 redis进程会退出
} else {
/* 如果aof上次刷盘是失败的,这次成功了,重置状态为成功 */
if (server.aof_last_write_status == C_ERR) {
serverLog(LL_WARNING,
"AOF write error looks solved, Redis can write again.");
server.aof_last_write_status = C_OK;
}
}
server.aof_current_size += nwritten;
/* 如果配置了aof rewrite时不要刷盘,且当前刚好有子进程在执行aof重写或rdb保存,就先不要aof fsync */
if (server.aof_no_fsync_on_rewrite &&
(server.aof_child_pid != -1 || server.rdb_child_pid != -1))
return;
/* aof_fsync是同步刷盘策略,这里才开始执行系统调用fsync,将os buf数据刷盘 */
if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
/* redis_fsync is defined as fdatasync() for Linux in order to avoid
* flushing metadata. */
latencyStartMonitor(latency);
// 系统调用fsync,上面将aof_buf写入OS缓冲,这里将OS缓冲刷盘
redis_fsync(server.aof_fd); /* Let's try to get this data on the disk */
latencyEndMonitor(latency);
latencyAddSampleIfNeeded("aof-fsync-always",latency);
server.aof_last_fsync = server.unixtime;
}
// 每秒刷盘策略,unixtime是秒级,这里判断了上次提交异步刷盘job的时间已经过了1秒就提交
else if ((server.aof_fsync == AOF_FSYNC_EVERYSEC &&
server.unixtime > server.aof_last_fsync)) {
// 注意这里!!main线程提交了 BIO_AOF_FSYNC job到任务队列,唤醒专门的线程异步刷盘
// 这里也解释了,上面为什么判断 sync_in_process,因为存在异步刷盘线程
if (!sync_in_progress) aof_background_fsync(server.aof_fd);
// 更新aof最近一次刷盘时间
server.aof_last_fsync = server.unixtime;
}
}
从刷盘策略来看,配置每秒刷盘是最好的,在性能和数据一致性上做了平衡。性能上因为是异步线程刷盘,主线程还是提供服务,服务性能并不会下降多少。来简单看一下aof_fsync_everysec的策略异步刷盘过程
/*
创建job加到任务队列中,然后唤醒对应job线程处理。
例如:aof_fsync_everysec策略是调用此方法创建job加到BIO_AOF_FSYNC队尾,唤醒BIO_AOF_FSYNC线程处理
*/
void bioCreateBackgroundJob(int type, void *arg1, void *arg2, void *arg3) {
struct bio_job *job = zmalloc(sizeof(*job));
job->time = time(NULL);
// 对于aof job,arg1是aof fd
job->arg1 = arg1;
job->arg2 = arg2;
job->arg3 = arg3;
pthread_mutex_lock(&bio_mutex[type]);
// 加到aof job队尾
listAddNodeTail(bio_jobs[type],job);
// aof job待处理数自增
bio_pending[type]++;
// 根据不同的任务type选择对应的条件队列,唤醒条件队列中挂起的线程,包括aof刷盘线程也是在此唤醒
pthread_cond_signal(&bio_newjob_cond[type]);
pthread_mutex_unlock(&bio_mutex[type]);
}
bio_newjob_cond是一个线程数组,根据任务类型保存对应任务的执行线程
/* Background job opcodes */
#define BIO_CLOSE_FILE 0 /* Deferred close(2) syscall. */
#define BIO_AOF_FSYNC 1 /* Deferred AOF fsync. */
#define BIO_LAZY_FREE 2 /* Deferred objects freeing. */
#define BIO_NUM_OPS 3
3.aof文件重写的过程逻辑(rdb&aof混合持久化)
先从这张图上了解下整个重写的大致流程。
aof重写有2个入口:
1.客户端用bgrewriteaof命令即刻发起
2.在服务器周期性的serverCron函数中通过检查发起
serverCron函数的功能介绍。 这个函数执行周期是每秒一次,在这里会做以下事情:
1.过期key的随机抽样收集
2.redis本身数据库的渐进式rehash
3.更新统计数据
4.检查触发rdb和aof重写
/* This is our timer interrupt, called server.hz times per second.
* Here is where we do a number of things that need to be done asynchronously.
* For instance:
*
* - Active expired keys collection (it is also performed in a lazy way on
* lookup).
* - Software watchdog.
* - Update some statistic.
* - Incremental rehashing of the DBs hash tables.
* - Triggering BGSAVE / AOF rewrite, and handling of terminated children.
* - Clients timeout of different kinds.
* - Replication reconnection.
* - Many more...
*
* Everything directly called here will be called server.hz times per second,
* so in order to throttle execution of things we want to do less frequently
* a macro is used: run_with_period(milliseconds) { .... }
*/
在serverCron中有这么一段逻辑,会根据统计数据检查是否要触发rdb保存或aof重写。
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
// ......
/* 检查子进程任务rdb保存,aof重写是否完成 */
if (server.rdb_child_pid != -1 || server.aof_child_pid != -1 || ldbPendingChildren()) {
// ...
} else {
/* 根据统计数据,是否触发rdb、aof重写 */
for (j = 0; j < server.saveparamslen; j++) {
struct saveparam *sp = server.saveparams+j;
/* 优先触发rdb保存 在单位时间内超过x个key改变 */
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;
}
}
/* 检查是否需要开启重写aof任务 */
if (server.aof_state == AOF_ON &&
// 要求现在rdb和重写aof任务都没运行
server.rdb_child_pid == -1 &&
server.aof_child_pid == -1 &&
// aof文件重写的阈值(默认100%),aof现文件大小/aof旧文件大小
server.aof_rewrite_perc &&
// aof文件现大小 > aof文件重写最低阈值(默认64mb)
server.aof_current_size > server.aof_rewrite_min_size)
{
// aof旧文件的大小
long long base = server.aof_rewrite_base_size ?
server.aof_rewrite_base_size : 1;
// 计算增长比值
long long growth = (server.aof_current_size*100/base) - 100;
// 增长比值超过了 重写阈值 就开始重写任务
if (growth >= server.aof_rewrite_perc) {
serverLog(LL_NOTICE,"Starting automatic rewriting of AOF on %lld%% growth",growth);
// 提交aof重写job,唤醒线程
rewriteAppendOnlyFileBackground();
}
}
}
// ......
}
在这个rewriteAppendOnlyFileBackground函数执行重写,通过fork子进程异步去做重写这件耗时的事情。主进程继续执行事件循环服务客户端。
有意思地方在于父子进程通信依靠3对管道,第一对传输重写期间增量的写命令给子进程,第二对三对做类似TCP的挥手告别。
int rewriteAppendOnlyFileBackground(void) {
pid_t childpid;
long long start;
if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;
if (aofCreatePipes() != C_OK) return C_ERR;
// 主进程和子进程通信用3对管道,第一对管道发送重写期间的写命令,第二三对管道用来做挥手告别
openChildInfoPipe();
start = ustime();
// fork函数创建子进程,子进程和父进程拥有相同虚拟地址共享同样物理内存,子进程写内存数据时运用copy-on-write机制
// fork函数返回0代表子进程,返回正数是子进程id代表运行的是父进程,负数是fork调用错误
if ((childpid = fork()) == 0) {
char tmpfile[256];
/* 子进程因为只做aof重写,所以关闭其它无用文件描述符 */
closeListeningSockets(0);
// 子进程名字
redisSetProcTitle("redis-aof-rewrite");
snprintf(tmpfile, 256, "temp-rewriteaof-bg-%d.aof", (int) getpid());
// 执行aof重写
if (rewriteAppendOnlyFile(tmpfile) == C_OK) {
size_t private_dirty = zmalloc_get_private_dirty(-1);
if (private_dirty) {
serverLog(LL_NOTICE,
"AOF rewrite: %zu MB of memory used by copy-on-write",
private_dirty/(1024*1024));
}
server.child_info_data.cow_size = private_dirty;
// 告诉父进程,重写完毕
sendChildInfo(CHILD_INFO_TYPE_AOF);
// 正常退出
exitFromChild(0);
} else {
// aof重写失败退出
exitFromChild(1);
}
} else {
/* 父进程逻辑 */
server.stat_fork_time = ustime()-start;
server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024*1024*1024); /* GB per second. */
latencyAddSampleIfNeeded("fork",server.stat_fork_time/1000);
if (childpid == -1) {
closeChildInfoPipe();
serverLog(LL_WARNING,
"Can't rewrite append only file in background: fork: %s",
strerror(errno));
aofClosePipes();
return C_ERR;
}
serverLog(LL_NOTICE,
"Background append only file rewriting started by pid %d",childpid);
server.aof_rewrite_scheduled = 0;
server.aof_rewrite_time_start = time(NULL);
server.aof_child_pid = childpid;
// rdb持久化或aof重写期间,禁止dict rehash,那如果dict必须要扩容时怎么办?
// 禁止dict扩容是针对一般扩容条件,如果元素个数/哈希槽数 > 5,也会强制扩容开启rehash
updateDictResizePolicy();
/* We set appendseldb to -1 in order to force the next call to the
* feedAppendOnlyFile() to issue a SELECT command, so the differences
* accumulated by the parent into server.aof_rewrite_buf will start
* with a SELECT statement and it will be safe to merge. */
server.aof_selected_db = -1;
replicationScriptCacheFlush();
// 直接返回了,父进程继续服务客户端
return C_OK;
}
return C_OK; /* unreached */
}
rewriteAppendOnlyFile函数分为2大部分,一个是执行当前数据库数据的持久化,一个是执行持久化期间父进程接收到的增量写命令持久化。
当前数据库的持久化根据配置,可能做rdb和aof的混合持久化,也可能做aof重写。混合持久化就是直接rdb的方式保存数据库,这样文件的前大部分都是二进制协议的序列化,在最后追加redis协议的aof增量写命令。推荐这种方式,在恢复数据时rdb协议会更快,既能有一定性能也保留了aof持久化较为完整的数据一致性。
aof重写不是将旧的aof文件合并重复命令,而是直接基于当前数据库entry数据,序列化成redis协议字符串写到新临时文件。最后追加写入父进程发来的增量aof写命令。
int rewriteAppendOnlyFile(
// 重写生成的目标文件名
char *filename) {
rio aof;
FILE *fp;
char tmpfile[256];
char byte;
/* Note that we have to use a different temp name here compared to the
* one used by rewriteAppendOnlyFileBackground() function. */
// 创建临时的重写aof文件
snprintf(tmpfile, 256, "temp-rewriteaof-%d.aof", (int) getpid());
fp = fopen(tmpfile,"w");
if (!fp) {
serverLog(LL_WARNING, "Opening the temp file for AOF rewrite in rewriteAppendOnlyFile(): %s", strerror(errno));
return C_ERR;
}
server.aof_child_diff = sdsempty();
rioInitWithFile(&aof, fp);
if (server.aof_rewrite_incremental_fsync)
rioSetAutoSync(&aof,REDIS_AUTOSYNC_BYTES);
// redis.conf开启了aof和rdb的混合持久化,会执行rdb保存当前数据库然后在尾部追加增量的aof写命令
if (server.aof_use_rdb_preamble) {
int error;
// 执行rdb持久化保存当前数据库
if (rdbSaveRio(&aof,&error,RDB_SAVE_AOF_PREAMBLE,NULL) == C_ERR) {
errno = error;
goto werr;
}
} else {
// 执行aof重写,重写是将数据库所有entry以redis协议格式重新写到新的临时aof文件
if (rewriteAppendOnlyFileRio(&aof) == C_ERR) goto werr;
}
/* 走到这里,上面已完成rdb或aof重写,这里先进行fsync将大部分数据刷盘,方便增量aof数据写入后的fsync更快 */
if (fflush(fp) == EOF) goto werr;
if (fsync(fileno(fp)) == -1) goto werr;
/* 短暂循环,等待父进程发送aof增量数据给子进程 */
int nodata = 0;
mstime_t start = mstime();
// 在1秒内等待,或者20毫秒内父进程都没发数据过来,就跳出
while (mstime()-start < 1000 && nodata < 20) {
// 监听父进程发送aof增量数据的管道,每次阻塞1毫秒,这一毫秒没数据就累加一次nodata
if (aeWait(server.aof_pipe_read_data_from_parent, AE_READABLE, 1) <= 0)
{
nodata++;
continue;
}
// 但是如果从父进程收到了数据,就重新开始计 20毫秒
nodata = 0; /* Start counting from zero, we stop on N *contiguous*
timeouts. */
aofReadDiffFromParent();
}
/* 停止从管道读数据了,从3号管道发送"!"告诉父进程,停止发送aof增量数据给子进程 */
if (write(server.aof_pipe_write_ack_to_parent,"!",1) != 1) goto werr;
if (anetNonBlock(NULL,server.aof_pipe_read_ack_from_parent) != ANET_OK)
goto werr;
/* 上面发送了"!",等待父进程回"!" */
if (syncRead(server.aof_pipe_read_ack_from_parent,&byte,1,5000) != 1 ||
byte != '!') goto werr;
serverLog(LL_NOTICE,"Parent agreed to stop sending diffs. Finalizing AOF...");
/* 最后一次从管道读父进程发来的aof增量数据 */
aofReadDiffFromParent();
/* 将aof增量数据写到新aof文件 */
serverLog(LL_NOTICE,
"Concatenating %.2f MB of AOF diff received from parent.",
(double) sdslen(server.aof_child_diff) / (1024*1024));
if (rioWrite(&aof,server.aof_child_diff,sdslen(server.aof_child_diff)) == 0)
goto werr;
/* fsync将aof文件刷盘,这里也是为了防止数据停留在os buf,确保真正落盘 */
if (fflush(fp) == EOF) goto werr;
if (fsync(fileno(fp)) == -1) goto werr;
if (fclose(fp) == EOF) goto werr;
/* 改名,aof文件名改为外部传来的目标文件名 */
if (rename(tmpfile,filename) == -1) {
serverLog(LL_WARNING,"Error moving temp append only file on the final destination: %s", strerror(errno));
unlink(tmpfile);
return C_ERR;
}
serverLog(LL_NOTICE,"SYNC append only file rewrite performed");
// 到这里子进程的aof重写逻辑就完成了
return C_OK;
werr:
serverLog(LL_WARNING,"Write error writing append only file on disk: %s", strerror(errno));
fclose(fp);
unlink(tmpfile);
return C_ERR;
}
总结:
我们知道了aof数据的写入过程和时机,也知道了为啥同步落盘能保证数据的完整性。因为在每一条写命令执行完毕后都会将aof buf刷盘,然后再回复给客户端,在客户端没收到回复前是阻塞状态,当收到回复时要么是成功执行要么是错误提示,但这就会导致很明显的性能下降。
我们也知道了aof落盘的执行过程,每一次写命令都会写入到aof buf中,这是一个用户空间的缓冲区,写入速度非常快不耽误命令的执行。然后redis执行完这一批次的文件事件后进入到下次监听事件前,会根据redis.conf配置的aof落盘策略执行fysnc或将aof buf写到os buf。
最后解析了aof文件重写的过程,也是混合持久化的过程。在每秒执行一次的serverCron函数里,检查当前aof文件大小是否超过64m和是否比上一次aof文件大了1倍,此时触发aof重写。主进程fork子进程共享同块内存,创建3对管道与子进程通信,发送增量的写命令数据给子进程。在重写逻辑里根据配置选择混合持久化,如果是混合持久化则先执行rdb保存然后追加增量的aof写命令。