1 命令执行的过程中是如何触发持久化的?
redis命令执行的时候会根据是否有AOF或主从复制的需要调用propagate()
函数,这个判断在redis命令执行核心函数call()
中:
/* Call propagate() only if at least one of AOF / replication
* propagation is needed. Note that modules commands handle replication
* in an explicit way, so we never replicate them automatically. */
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)
{
if (server.aof_state != AOF_OFF && flags & PROPAGATE_AOF)
feedAppendOnlyFile(cmd,dbid,argv,argc);
if (flags & PROPAGATE_REPL)
replicationFeedSlaves(server.slaves,dbid,argv,argc);
}
feedAppendOnlyFile()
函数便是与AOF持久化的关键函数了。
*
:AOF持久化的相关实现主要集中在aof.c
文件中。这个文件包含了AOF的工作机制,包括命令的写入、文件的同步、AOF重写等功能的实现。
2 AOF持久化过程
AOF持久化过程主要包括以下几个阶段和步骤:
-
命令追加(Append):
- 当AOF持久化功能开启时,Redis服务器在执行数据库的写入命令时,会将该写入命令追加到一个名为
aof_buf
的缓冲区末尾。这意味着所有修改数据的命令(如SET, HSET等)都会被记录。feedAppendOnlyFile()
函数负责这一操作。
- 当AOF持久化功能开启时,Redis服务器在执行数据库的写入命令时,会将该写入命令追加到一个名为
-
缓冲区写入文件(Write to File):
- Redis会定期或者根据配置策略(通过
appendfsync
配置项设置)将aof_buf
中的内容写入到AOF文件。策略可选为always
(每次写操作都同步)、everysec
(每秒同步一次)或no
(操作系统决定何时同步)。flushAppendOnlyFile
函数负责执行这一操作。
- Redis会定期或者根据配置策略(通过
-
文件同步(Sync):
- 根据
appendfsync
的配置,决定何时执行文件系统的同步操作,以确保数据真正被写入磁盘。这一步骤对于确保数据安全至关重要,但也影响性能。
- 根据
-
AOF重写(Rewrite):
-
随着AOF文件的增长,Redis提供了AOF重写机制来压缩AOF文件的大小。重写不是持续进行的,而是根据配置触发(如通过
bgrewriteaof
命令手动触发,或自动根据auto-aof-rewrite-percentage
和auto-aof-rewrite-min-size
配置)。重写过程会在一个子进程中进行,该子进程会遍历内存中的数据生成一个新的、更紧凑的AOF文件,之后用这个新文件替换旧的AOF文件。 -
在重写期间,Redis继续将新的写操作追加到旧的AOF文件,并同时记录这些操作到一个重写缓冲区中,以确保重写期间的数据不会丢失。
-
-
AOF文件加载与数据恢复:
- Redis服务器启动时,如果检测到存在AOF文件,会通过
loadAppendOnlyFile
函数读取AOF文件中的命令,并重新执行这些命令来恢复数据库状态。这个过程确保了服务器重启后数据能够恢复到最新状态。
- Redis服务器启动时,如果检测到存在AOF文件,会通过
整个AOF持久化过程确保了Redis数据变更的高可用性和灾难恢复能力,通过牺牲一定的写入性能来换取数据的持久性。
2.1 feedAppendOnlyFile()
|命令追加(Append)
feedAppendOnlyFile()
实现细节见代码注释:
/**
* 向只追加文件中附加命令
*
* 该函数负责将指定的Redis命令以特定格式追加到Append-Only File(AOF)中。
* 根据命令的不同,可能会进行一些转换,例如将EXPIRE相关命令转换为PEXPIREAT,
* 或者在SET命令中处理EX和PX选项。
*
* @param cmd 指向当前要追加的Redis命令的指针。
* @param dictid 当前命令目标数据库的ID。
* @param argv 包含命令参数的数组。
* @param argc 命令参数的数量。
*/
void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int argc) {
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;
}
/* 处理命令转换,主要针对过期时间设置相关的命令 */
if (cmd->proc == expireCommand || cmd->proc == pexpireCommand ||
cmd->proc == expireatCommand) {
buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);
} else if (cmd->proc == setexCommand || cmd->proc == psetexCommand) {
/* 将SETEX和PSETEX命令转换为SET和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;
/* 处理SET命令中的EX和PX选项,转换为SET和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 {
/* 对于其他命令,直接追加到AOF文件中 */
buf = catAppendOnlyGenericCommand(buf,argc,argv);
}
/* 将构建好的命令追加到AOF缓冲区,等待写入到AOF文件 */
if (server.aof_state == AOF_ON)
server.aof_buf = sdscatlen(server.aof_buf,buf,sdslen(buf));
/* 如果正在后台进行AOF文件重写,将当前命令的差异追加到重写缓冲区 */
if (server.aof_child_pid != -1)
aofRewriteBufferAppend((unsigned char*)buf,sdslen(buf));
sdsfree(buf);
}
2.2 flushAppendOnlyFile()
|缓冲区写入文件(Write to File)
/**
* 刷新并写入附加只追加文件(AOF文件)。
* 如果force为1,则立即写入并同步AOF文件,否则根据配置和条件可能延迟写入。
*
* @param force 强制写入和同步的标志。如果为1,则忽略AOF_FSYNC_EVERYSEC配置,立即进行fsync。
* 这通常用于在关闭AOF时确保数据完整性。
*/
void flushAppendOnlyFile(int force) {
ssize_t nwritten;
int sync_in_progress = 0; // 记录是否正在进行fsync操作
mstime_t latency; // 记录操作的延迟
// 如果AOF缓冲区为空,但需要执行fsync,即使没有数据也要执行
if (sdslen(server.aof_buf) == 0) {
if (server.aof_fsync == AOF_FSYNC_EVERYSEC &&
server.aof_fsync_offset != server.aof_current_size &&
server.unixtime > server.aof_last_fsync &&
!(sync_in_progress = aofFsyncInProgress())) {
goto try_fsync;
} else {
return;
}
}
// 如果配置为每秒fsync且没有强制写入,则检查是否正在进行fsync操作
if (server.aof_fsync == AOF_FSYNC_EVERYSEC)
sync_in_progress = aofFsyncInProgress();
// 处理AOF_FSYNC_EVERYSEC模式下的延迟写入逻辑
if (server.aof_fsync == AOF_FSYNC_EVERYSEC && !force) {
if (sync_in_progress) {
if (server.aof_flush_postponed_start == 0) {
/* 记录开始推迟fsync的时间 */
server.aof_flush_postponed_start = server.unixtime;
return;
} else if (server.unixtime - server.aof_flush_postponed_start < 2) {
/* 如果推迟时间小于2秒,再次推迟 */
return;
}
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);
// 尝试写入AOF缓冲区的内容到文件
nwritten = aofWrite(server.aof_fd, server.aof_buf, sdslen(server.aof_buf));
// 结束监控延迟
latencyEndMonitor(latency);
// 根据当前状态记录延迟写入的原因
if (sync_in_progress) {
latencyAddSampleIfNeeded("aof-write-pending-fsync", latency);
} else if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) {
latencyAddSampleIfNeeded("aof-write-active-child", latency);
} else {
latencyAddSampleIfNeeded("aof-write-alone", latency);
}
// 总是记录一次写入操作的延迟
latencyAddSampleIfNeeded("aof-write", latency);
// 重置推迟fsync的开始时间
server.aof_flush_postponed_start = 0;
// 处理写入结果
if (nwritten != (ssize_t)sdslen(server.aof_buf)) {
static time_t last_write_error_log = 0;
int can_log = 0;
// 限制错误日志的输出频率
if ((server.unixtime - last_write_error_log) > AOF_WRITE_LOG_ERROR_RATE) {
can_log = 1;
last_write_error_log = server.unixtime;
}
// 记录写入错误
if (nwritten == -1) {
if (can_log) {
serverLog(LL_WARNING,"Error writing to the AOF file: %s", strerror(errno));
server.aof_last_write_errno = errno;
}
} else {
if (can_log) {
serverLog(LL_WARNING,"Short write while writing to the AOF file: (nwritten=%lld, expected=%lld)",
(long long)nwritten,
(long long)sdslen(server.aof_buf));
}
// 尝试通过ftruncate移除部分写入的数据
if (ftruncate(server.aof_fd, server.aof_current_size) == -1) {
if (can_log) {
serverLog(LL_WARNING, "Could not remove short write from the append-only file. Redis may refuse "
"to load the AOF the next time it starts. ftruncate: %s", strerror(errno));
}
} else {
nwritten = -1; // 移除成功,标记为-1表示没有部分数据
}
server.aof_last_write_errno = ENOSPC;
}
// 根据AOF fsync策略处理写入错误
if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
serverLog(LL_WARNING,"Can't recover from AOF write error when the AOF fsync policy is 'always'. Exiting...");
exit(1);
} else {
server.aof_last_write_status = C_ERR;
// 如果有部分成功写入,更新AOF大小并截断缓冲区
if (nwritten > 0) {
server.aof_current_size += nwritten;
sdsrange(server.aof_buf, nwritten, -1);
}
return; /* 出错时,等待下一次调用尝试恢复 */
}
} else {
// 写入成功,如果有之前写入错误,则记录恢复事件
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缓冲区,如果其大小足够小
if ((sdslen(server.aof_buf)+sdsavail(server.aof_buf)) < 4000) {
sdsclear(server.aof_buf);
} else {
sdsfree(server.aof_buf);
server.aof_buf = sdsempty();
}
try_fsync:
// 在特定条件下跳过fsync
if (server.aof_no_fsync_on_rewrite &&
(server.aof_child_pid != -1 || server.rdb_child_pid != -1))
return;
// 根据配置执行fsync
if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
latencyStartMonitor(latency);
redis_fsync(server.aof_fd); /* 尝试将数据写入磁盘 */
latencyEndMonitor(latency);
latencyAddSampleIfNeeded("aof-fsync-always", latency);
server.aof_fsync_offset = server.aof_current_size;
server.aof_last_fsync = server.unixtime;
} else if ((server.aof_fsync == AOF_FSYNC_EVERYSEC &&
server.unixtime > server.aof_last_fsync)) {
if (!sync_in_progress) {
aof_background_fsync(server.aof_fd);
server.aof_fsync_offset = server.aof_current_size;
}
server.aof_last_fsync = server.unixtime;
}
}
2.3 redis_fsync()
|文件同步(Sync)
redis_fsync|文件同步
是强制将文件的数据部分(不包括所有的元数据)从内核缓冲区同步到磁盘上,这意味着它确保了对文件内容的所有修改都已经持久化存储。
在Linux系统中,redis_fsync会被定义为fdatasync函数,而在其他操作系统中,则会被定义为fsync函数。这两个函数都是用于将文件缓冲区中的数据同步到磁盘上的系统调用,fdatasync与fsync的区别在于fdatasync不会同步文件元数据,而fsync会同步文件元数据。
AOF缓冲区写入文件和文件同步的区别
AOF缓冲区写入文件和刷盘是两个相关但有区别的步骤,它们共同确保了数据的安全性和持久性。下面是这两个步骤的具体区别:
AOF缓冲区写入文件
- 目的:这是将积累在内存中的AOF缓冲区数据写入到AOF文件的过程。当Redis接收到写命令时,它首先把这些命令追加到内存中的AOF缓冲区。缓冲区的写入文件操作是为了释放内存空间,并且是数据持久化的第一步。
- 操作:Redis会在适当的时候(根据
appendfsync
策略)将缓冲区中的内容批量写入到AOF文件中,这个过程相对快速,因为它只是在操作系统层面将数据从内存转移到磁盘的缓存中。- 影响:此操作减少了内存占用,但数据尚未完全安全,因为操作系统可能还未将这些数据真正写入到持久化存储中。
刷盘(fsync)
- 目的:确保AOF文件中的数据被物理地写入到磁盘上,从而在系统崩溃时数据不会丢失。这是一个更为关键的操作,因为它直接影响到数据的持久性和完整性。
- 操作:
fsync
或类似的系统调用强制操作系统将文件系统缓存中的数据同步到持久化存储设备上。在Redis中,根据appendfsync
配置的不同,刷盘可以是每次写操作后立即执行(always
)、每秒执行一次(everysec
)或由操作系统决定(no
)。- 影响:刷盘操作相对耗时,因为它涉及到实际的磁盘I/O操作,可能会对Redis的响应时间和吞吐量产生影响。然而,它是确保数据持久化的必要步骤,特别是在对数据安全性要求高的场景下。
AOF缓冲区写入文件主要是内存到磁盘缓存的数据传输,侧重于释放内存和初步持久化;而刷盘则是确保数据从磁盘缓存真正写入到非易失性存储器中,是数据安全的关键保障。。
2.4 rewriteAppendOnlyFileBackground()
|AOF重写(Rewrite)
aofRewriteBufferAppend()
:命令追加的时候,如果AOF正在重写,用于将命令添加到重写缓冲区。aofRewrite()
:触发AOF重写的入口点,管理重写过程,包括创建子进程进行实际的重写工作。
aof重写动作是在serverCron()
函数中触发的,并且有两种触发aof重写的情况。
用户调用 BGREWRITEAOF 命令:如果没有正在进行的RDB持久化和AOF重写,并且AOF重写已经被用户请求,满足这些条件,则调用rewriteAppendOnlyFileBackground()函数在后台启动AOF重写。
定时任务:如果没有正在进行的RDB持久化和AOF重写,AOF文件的当前大小超过指定的最小重写大小(aof_rewrite_min_size),同时AOF当前大小与基础大小(aof_rewrite_base_size)的百分比增长达到触发重写的条件,则会后台执行AOF重写操作。
//用户调用 BGREWRITEAOF 命令
/* 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();
}
//定时任务
/* Trigger an AOF rewrite if needed. */
if (server.aof_state == AOF_ON &&
server.rdb_child_pid == -1 &&
server.aof_child_pid == -1 &&
server.aof_rewrite_perc &&
server.aof_current_size > server.aof_rewrite_min_size)
{
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);
rewriteAppendOnlyFileBackground();
}
}
aof重写工作原理:*
- 用户调用 BGREWRITEAOF 命令。
- Redis 执行此函数并调用 fork() 系统调用:
2a) 子进程负责将现有AOF文件重写到一个临时文件中。
2b) 父进程在此期间累积所有差异到server.aof_rewrite_buf 缓冲区中。- 当子进程完成 ‘2a’ 步骤后,它将退出。
- 父进程捕获子进程的退出码,如果一切正常,父进程会将累积在 server.aof_rewrite_buf
中的数据追加到临时文件中,然后使用rename(2)
系统调用将临时文件替换为实际的AOF文件名。之后,新文件被重新打开作为新的AOF文件。
rewriteAppendOnlyFileBackground()
源码:
/**
* 在后台重写只AOF文件
*
* 该函数用于在后台重写Redis的AOF,以减少对主服务的影响。
* 重写过程中,会创建一个子进程来完成实际的重写工作,而父进程则继续处理客户端请求。
*
* @return C_OK 表示成功启动了AOF文件的重写后台进程;C_ERR 表示启动失败。
*/
int rewriteAppendOnlyFileBackground(void) {
pid_t childpid; // 子进程ID
long long start; // 开始时间,用于计算fork操作的耗时
// 检查是否有正在运行的子进程,如果有则不能启动重写进程
if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;
// 创建用于父进程和子进程间通信的管道
if (aofCreatePipes() != C_OK) return C_ERR;
openChildInfoPipe(); // 打开子进程信息管道
start = ustime(); // 记录当前时间
// 创建子进程来执行重写操作
if ((childpid = fork()) == 0) {
char tmpfile[256]; // 用于存储临时AOF文件路径
// 子进程的初始化工作
closeClildUnusedResourceAfterFork();
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); // 获取内存拷贝使用的内存量
// 记录copy-on-write内存使用的日志
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 {
exitFromChild(1); // 重写失败后退出子进程
}
} else {
// 父进程的后续处理
server.stat_fork_time = ustime()-start; // 计算fork操作耗时
// 计算fork操作的内存拷贝速率(GB/秒)
server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024*1024*1024);
latencyAddSampleIfNeeded("fork",server.stat_fork_time/1000); // 添加fork操作的延迟样本
if (childpid == -1) {
// fork失败的处理
closeChildInfoPipe();
serverLog(LL_WARNING,
"Can't rewrite append only file in background: fork: %s",
strerror(errno));
aofClosePipes();
return C_ERR;
}
// fork成功后的处理
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;
updateDictResizePolicy();
// 重置AOF缓冲区相关的属性,以准备新的AOF数据写入
server.aof_selected_db = -1;
replicationScriptCacheFlush();
return C_OK;
}
return C_OK; /* unreached */
}
rewriteAppendOnlyFile()
函数源码:
/**
* 重写aof
*
* 此函数用于重写Redis的aof文件,通过读取父进程的AOF diff(差异数据)来完成。
* 它首先创建一个临时文件,将接收到的diff数据写入该文件,最后将临时文件重命名为原始AOF文件的名称,
* 以完成重写过程。
*
* @param filename 指向要重写的AOF文件名称的字符指针。
* @return 返回C_OK表示成功,C_ERR表示失败。
*/
int rewriteAppendOnlyFile(char *filename) {
rio aof;
FILE *fp;
char tmpfile[256];
char byte;
// 生成用于重写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;
}
// 初始化AOF重写缓冲区为空,并设置rio结构以使用该文件
server.aof_child_diff = sdsempty();
rioInitWithFile(&aof,fp);
// 如果配置了增量fsync,则设置自动同步
if (server.aof_rewrite_incremental_fsync)
rioSetAutoSync(&aof,REDIS_AUTOSYNC_BYTES);
// 根据配置,写入RDB preamble或直接重写AOF
if (server.aof_use_rdb_preamble) {
int error;
if (rdbSaveRio(&aof,&error,RDB_SAVE_AOF_PREAMBLE,NULL) == C_ERR) {
errno = error;
goto werr;
}
} else {
if (rewriteAppendOnlyFileRio(&aof) == C_ERR) goto werr;
}
// 初始慢速fsync,以便后续的final fsync更快
if (fflush(fp) == EOF) goto werr;
if (fsync(fileno(fp)) == -1) goto werr;
// 尝试多次从父进程读取更多数据
int nodata = 0;
mstime_t start = mstime();
while(mstime()-start < 1000 && nodata < 20) {
if (aeWait(server.aof_pipe_read_data_from_parent, AE_READABLE, 1) <= 0)
{
nodata++;
continue;
}
nodata = 0;
aofReadDiffFromParent();
}
// 请求父进程停止发送diff
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;
// 读取父进程的ACK
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...");
// 读取并写入最后的diff
aofReadDiffFromParent();
if (rioWrite(&aof,server.aof_child_diff,sdslen(server.aof_child_diff)) == 0)
goto werr;
// 确保数据从OS输出缓冲区刷新到底层存储
if (fflush(fp) == EOF) goto werr;
if (fsync(fileno(fp)) == -1) goto werr;
if (fclose(fp) == EOF) goto werr;
// 使用RENAME原子地替换旧的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");
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;
}
2.5 loadAppendOnlyFile()
|AOF文件加载与数据恢复
在Redis启动时,通过loadAppendOnlyFile()
函数读取AOF文件并重新执行文件中的命令以恢复数据。
loadAppendOnlyFile()
函数源码:
/**
* 加载aof。
*
* @param filename aof文件名。
* @return 返回C_OK表示加载成功,否则返回C_ERR。
*
* 该函数负责加载并执行AOF文件中的命令,以恢复服务器状态。
* 它首先检查文件是否可读,然后处理文件中的RDB preamble(如果存在),
* 接着按命令逐个读取并执行AOF文件中的内容。
* 在加载过程中,会定期处理客户端请求以保持响应性。
*/
int loadAppendOnlyFile(char *filename) {
struct client *fakeClient;
FILE *fp = fopen(filename,"r");
struct redis_stat sb;
int old_aof_state = server.aof_state;
long loops = 0;
off_t valid_up_to = 0; /* 最后一个正确加载的命令的偏移量 */
off_t valid_before_multi = 0; /* 加载MULTI命令之前的偏移量 */
/* 打开AOF文件失败时的错误处理 */
if (fp == NULL) {
serverLog(LL_WARNING,"Fatal error: can't open the append log file for reading: %s",strerror(errno));
exit(1);
}
/* 处理AOF文件大小为0的特殊情况 */
if (fp && redis_fstat(fileno(fp),&sb) != -1 && sb.st_size == 0) {
server.aof_current_size = 0;
server.aof_fsync_offset = server.aof_current_size;
fclose(fp);
return C_ERR;
}
/* 暂时禁用AOF,以防止EXEC命令将MULTI命令写入相同的文件中 */
server.aof_state = AOF_OFF;
/* 创建一个虚拟客户端以执行AOF文件中的命令 */
fakeClient = createFakeClient();
startLoading(fp);
/* 检查AOF文件是否以RDB preamble开始,如果是,则先加载RDB,然后继续加载AOF尾部 */
char sig[5]; /* "REDIS" */
if (fread(sig,1,5,fp) != 5 || memcmp(sig,"REDIS",5) != 0) {
/* 没有RDB preamble,回到文件开头 */
if (fseek(fp,0,SEEK_SET) == -1) goto readerr;
} else {
/* 有RDB preamble,处理RDB加载 */
rio rdb;
serverLog(LL_NOTICE,"Reading RDB preamble from AOF file...");
if (fseek(fp,0,SEEK_SET) == -1) goto readerr;
rioInitWithFile(&rdb,fp);
if (rdbLoadRio(&rdb,NULL,1) != C_OK) {
serverLog(LL_WARNING,"Error reading the RDB preamble of the AOF file, AOF loading aborted");
goto readerr;
} else {
serverLog(LL_NOTICE,"Reading the remaining AOF tail...");
}
}
/* 读取并执行AOF文件中的命令 */
while(1) {
int argc, j;
unsigned long len;
robj **argv;
char buf[128];
sds argsds;
struct redisCommand *cmd;
/* 定期处理客户端请求 */
if (!(loops++ % 1000)) {
loadingProgress(ftello(fp));
processEventsWhileBlocked();
}
/* 读取并解析命令 */
if (fgets(buf,sizeof(buf),fp) == NULL) {
if (feof(fp))
break;
else
goto readerr;
}
if (buf[0] != '*') goto fmterr;
if (buf[1] == '\0') goto readerr;
argc = atoi(buf+1);
if (argc < 1) goto fmterr;
/* 分配内存并读取命令参数 */
argv = zmalloc(sizeof(robj*)*argc);
fakeClient->argc = argc;
fakeClient->argv = argv;
for (j = 0; j < argc; j++) {
/* 读取并解析参数长度 */
char *readres = fgets(buf,sizeof(buf),fp);
if (readres == NULL || buf[0] != '$') {
fakeClient->argc = j; /* 释放已分配的内存 */
freeFakeClientArgv(fakeClient);
if (readres == NULL)
goto readerr;
else
goto fmterr;
}
len = strtol(buf+1,NULL,10);
/* 读取参数值 */
argsds = sdsnewlen(SDS_NOINIT,len);
if (len && fread(argsds,len,1,fp) == 0) {
sdsfree(argsds);
fakeClient->argc = j; /* 释放已分配的内存 */
freeFakeClientArgv(fakeClient);
goto readerr;
}
argv[j] = createObject(OBJ_STRING,argsds);
/* 跳过CRLF */
if (fread(buf,2,1,fp) == 0) {
fakeClient->argc = j+1; /* 释放已分配的内存 */
freeFakeClientArgv(fakeClient);
goto readerr;
}
}
/* 查找并执行命令 */
cmd = lookupCommand(argv[0]->ptr);
if (!cmd) {
serverLog(LL_WARNING,
"Unknown command '%s' reading the append only file",
(char*)argv[0]->ptr);
exit(1);
}
/* 记录MULTI命令的位置,以便在遇到EXEC时处理 */
if (cmd == server.multiCommand) valid_before_multi = valid_up_to;
/* 在虚拟客户端上下文中执行命令 */
fakeClient->cmd = cmd;
if (fakeClient->flags & CLIENT_MULTI &&
fakeClient->cmd->proc != execCommand)
{
queueMultiCommand(fakeClient);
} else {
cmd->proc(fakeClient);
}
/* 清理 */
freeFakeClientArgv(fakeClient);
fakeClient->cmd = NULL;
}
/* 如果读取结束时客户端处于MULTI状态,视为不完整事务,进行处理 */
if (fakeClient->flags & CLIENT_MULTI) {
serverLog(LL_WARNING,
"Revert incomplete MULTI/EXEC transaction in AOF file");
valid_up_to = valid_before_multi;
goto uxeof;
}
/* 加载成功,进行清理并返回C_OK */
fclose(fp);
freeFakeClient(fakeClient);
server.aof_state = old_aof_state;
stopLoading();
aofUpdateCurrentSize();
server.aof_rewrite_base_size = server.aof_current_size;
server.aof_fsync_offset = server.aof_current_size;
return C_OK;
readerr: /* 读取错误处理 */
if (!feof(fp)) {
if (fakeClient) freeFakeClient(fakeClient); /* 避免Valgrind警告 */
serverLog(LL_WARNING,"Unrecoverable error reading the append only file: %s", strerror(errno));
exit(1);
}
uxeof: /* 意外的AOF文件结束处理 */
if (server.aof_load_truncated) {
serverLog(LL_WARNING,"!!! Warning: short read while loading the AOF file !!!");
serverLog(LL_WARNING,"!!! Truncating the AOF at offset %llu !!!",
(unsigned long long) valid_up_to);
if (valid_up_to == -1 || truncate(filename,valid_up_to) == -1) {
if (valid_up_to == -1) {
serverLog(LL_WARNING,"Last valid command offset is invalid");
} else {
serverLog(LL_WARNING,"Error truncating the AOF file: %s",
strerror(errno));
}
} else {
/* 确保AOF文件描述符指向文件末尾 */
if (server.aof_fd != -1 && lseek(server.aof_fd,0,SEEK_END) == -1) {
serverLog(LL_WARNING,"Can't seek the end of the AOF file: %s",
strerror(errno));
} else {
serverLog(LL_WARNING,
"AOF loaded anyway because aof-load-truncated is enabled");
goto loaded_ok;
}
}
}
if (fakeClient) freeFakeClient(fakeClient); /* 避免Valgrind警告 */
serverLog(LL_WARNING,"Unexpected end of file reading the append only file. You can: 1) Make a backup of your AOF file, then use ./redis-check-aof --fix <filename>. 2) Alternatively you can set the 'aof-load-truncated' configuration option to yes and restart the server.");
exit(1);
fmterr: /* 文件格式错误处理 */
if (fakeClient) freeFakeClient(fakeClient); /* 避免Valgrind警告 */
serverLog(LL_WARNING,"Bad file format reading the append only file: make a backup of your AOF file, then use ./redis-check-aof --fix <filename>");
exit(1);
}
文章中的源码基于对应redis 5.0.14版本。