aof.c 文件是 Redis 中处理 Append-Only 文件(AOF)持久化的源代码文件。AOF 是一种持久化方式,用于记录 Redis 服务器接收到的每个写命令,以便在服务器重启时重新执行这些命令,从而将内存中的数据恢复到持久化状态。以下是 aof.c 文件的主要功能:
- AOF 文件的打开和关闭:
- 包括函数 aofOpen 用于打开 AOF 文件,以及 aofClose 用于关闭 AOF 文件。
- AOF 缓冲区:
- Redis 使用一个缓冲区来暂时保存待写入 AOF 文件的命令。相关的函数包括 aofRewriteBufferAppend 用于将命令追加到缓冲区。
- AOF 文件的写入:
- 函数 flushAppendOnlyFile 用于将缓冲区的内容写入 AOF 文件。此函数还会负责执行 AOF 文件的 FSYNC 操作,确保写入到磁盘的安全性。
- AOF 文件的重写:
- Redis 会定期进行 AOF 文件的重写,以减小 AOF 文件的体积,提高读取效率。相关函数包括 feedAppendOnlyFile 用于重写 AOF 文件。
- AOF 文件的载入:
- 在服务器启动时,会从 AOF 文件中读取命令,将数据还原到内存。loadAppendOnlyFile 函数用于执行这个任务。
- AOF 策略:
- Redis 允许根据不同的 AOF 策略来配置 AOF 文件的刷写方式。相关函数包括 aofCreatePsyncWritePipe 用于创建 AOF 文件的管道,aofPsyncWritePipe 用于执行同步写操作。
总体来说,aof.c 文件实现了 Redis 的 AOF 持久化机制的各个方面,包括文件的打开和关闭、缓冲区管理、AOF 文件的写入、重写和加载等功能。
AOF重写
AOF 重写是指将当前 AOF 文件中的命令以一种更紧凑、优化的格式重新写入新的 AOF 文件,从而达到减小文件体积、提高读写效率的目的。
实现方式: 使用一个简单的缓冲区,用于在后台进程重写 AOF 文件时累积更改。在这个缓冲区中,只需要追加(append)操作,但不能仅仅使用 realloc 来调整大小,因为对于非常大的重新分配可能不会被处理得像预期的那样高效,可能涉及数据的复制。
#define AOF_RW_BUF_BLOCK_SIZE (1024*1024*10) /* 10 MB per block */
/*Redis 能够以块为单位管理 AOF 重写缓冲区中的数据,确保高效的追加和组织 AOF 内容。*/
typedef struct aofrwblock {
/*used 表示已经使用的字节数,free 表示剩余可用的字节数。
这两个字段用于追踪块中的数据状态,以确定当前块是否还有足够的空间来追加新的 AOF 数据。*/
unsigned long used, free;
/*这是一个字符数组,用于存储 AOF 数据。数组大小为 AOF_RW_BUF_BLOCK_SIZE 字节,即 10 MB。*/
char buf[AOF_RW_BUF_BLOCK_SIZE];
} aofrwblock;
缓冲区组织: 使用一个块列表(list of blocks)的结构,每个块的大小为 AOF_RW_BUF_BLOCK_SIZE 字节。这个列表中的每个块都可以容纳一定数量的 AOF 数据。这个设计的优点在于,当 AOF 重写过程中需要追加新的数据时,可以在块之间进行操作,而不必一次性处理整个巨大的缓冲区。这有助于减小内存分配的开销,提高性能。
void aofChildWriteDiffData(aeEventLoop *el, int fd, void *privdata, int mask) {
listNode *ln;
aofrwblock *block;
ssize_t nwritten;
UNUSED(el);
UNUSED(fd);
UNUSED(privdata);
UNUSED(mask);
while(1) {//函数通过循环处理所有待发送的数据块,直到停止发送或缓冲区为空。
ln = listFirst(server.aof_rewrite_buf_blocks);
block = ln ? ln->value : NULL;
/*检查是否需要停止发送差异数据给子进程。如果server.aof_stop_sending_diff为真,表示需要停止发送;
如果block为空,表示没有待发送的差异数据块,也需要停止发送。*/
if (server.aof_stop_sending_diff || !block) {
在套接字中删除事件。
aeDeleteFileEvent(server.el,server.aof_pipe_write_data_to_child,
AE_WRITABLE);
return;
}
/*block->used > 0: 检查当前差异数据块中是否还有数据待发送。
如果有,就使用write函数将数据写入server.aof_pipe_write_data_to_child,
然后通过memmove将已发送的数据从差异数据块中移除,更新数据块的used和free字段。*/
if (block->used > 0) {
nwritten = write(server.aof_pipe_write_data_to_child,
block->buf,block->used);
if (nwritten <= 0) return;
memmove(block->buf,block->buf+nwritten,block->used-nwritten);
block->used -= nwritten;
block->free += nwritten;
}
/*block->used == 0: 当差异数据块中所有数据都已经发送完毕时,通过listDelNode函数从server.aof_rewrite_buf_blocks链表中删除当前的差异数据块节点。*/
if (block->used == 0) listDelNode(server.aof_rewrite_buf_blocks,ln);
}
}
重写的数据通过套接字发送给子进程。
ssize_t aofRewriteBufferWrite(int fd) {
listNode *ln;
listIter li;
ssize_t count = 0;
listRewind(server.aof_rewrite_buf_blocks,&li);
while((ln = listNext(&li))) {
aofrwblock *block = listNodeValue(ln);
ssize_t nwritten;
if (block->used) {
nwritten = write(fd,block->buf,block->used);
if (nwritten != (ssize_t)block->used) {
if (nwritten == 0) errno = EIO;
return -1;
}
count += nwritten;
}
}
return count;
}
这段代码实现了将AOF(Append-Only File)重写缓冲区中的数据写入指定文件描述符(fd)的功能。AOF是Redis用于持久性的一种方式,它将每个写命令追加到一个文件中,以便在Redis重启时重新加载。AOF重写是一种优化技术,用于减小AOF文件的体积,提高性能
int rewriteAppendOnlyFileBackground(void) {
pid_t childpid;
if (hasActiveChildProcess()) return C_ERR;//看是否有重写事件
if (aofCreatePipes() != C_OK) return C_ERR;
openChildInfoPipe();//初始化开启子进程和主进程通信的管道
/*当用户调用 BGREWRITEAOF 命令时,Redis 调用这个函数。
在这个函数中,通过调用 redisFork() 创建一个子进程。*/
if ((childpid = redisFork()) == 0) {//子进程程序
char tmpfile[256];
/* Child */
redisSetProcTitle("redis-aof-rewrite");//设置进程标题
redisSetCpuAffinity(server.aof_rewrite_cpulist);//设置 AOF 重写过程的 CPU 亲和性(CPU Affinity)
// 这行代码构建了一个临时的 AOF 文件名。这个文件名是根据进程 ID 动态生成的,以确保唯一性。
snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof", (int) getpid());
if (rewriteAppendOnlyFile(tmpfile) == C_OK) {// 函数执行 AOF 文件的重写操作。如果操作成功(返回 C_OK)
sendChildCOWInfo(CHILD_INFO_TYPE_AOF, "AOF rewrite");// 这行代码向父进程发送关于 AOF 重写的信息。这可能包括一些统计信息或状态信息。
exitFromChild(0);//子进程以状态码 0 退出。这是为了告知父进程 AOF 重写成功。
} else {
exitFromChild(1);//进程以状态码 1 退出。这是为了告知父进程 AOF 重写失败。
}
} else {
/* Parent */
/*这段代码实现了Redis的AOF(Append Only File)后台重写机制。*/
if (childpid == -1) {
/*if (childpid == -1): 如果 fork 失败,记录错误日志,关闭与子进程通信的管道,关闭AOF相关的管道,返回 C_ERR 表示操作失败。*/
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);/*记录后台AOF重写进程启动的日志。*/
server.aof_rewrite_scheduled = 0;/*将AOF重写的计划标记设为0,表示不再需要进行AOF重写。*/
server.aof_rewrite_time_start = time(NULL);/* 记录AOF重写的开始时间。*/
server.aof_child_pid = childpid;/*记录AOF重写子进程的进程ID。*/
/* 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;/*将数据库选择标记设为-1。这是为了强制下一次调用 */
replicationScriptCacheFlush();/*清空脚本缓存,这是因为AOF重写可能导致脚本缓存的脚本不再一致。*/
return C_OK;
}
return C_OK; /* unreached */
}
这段代码是开启aof重写子进程的具体操作,需要初始化管道,判断是否已经存在aof重写进程等操作,之后通过fork()来生成子进程进行aof重写。
AOF实现
void bioCreateBackgroundJob(int type, void *arg1, void *arg2, void *arg3) {
//这里创建了一个名为 job 的后台任务结构体。
struct bio_job *job = zmalloc(sizeof(*job));
//在任务结构体中,设置 time 字段为当前的时间戳(使用 time(NULL) 获取),以记录任务的创建时间。
job->time = time(NULL);
job->arg1 = arg1;
job->arg2 = arg2;
job->arg3 = arg3;
//这里通过互斥锁(pthread_mutex)来保证在多线程环境中对后台任务队列的访问是线程安全的。根据给定的 type,选择合适的互斥锁 bio_mutex[type] 进行加锁操作。
pthread_mutex_lock(&bio_mutex[type]);
//将创建的后台任务 job 加入指定类型的后台任务队列 bio_jobs[type] 的尾部。这个队列用于存储待执行的后台任务。
listAddNodeTail(bio_jobs[type],job);
//增加与指定类型的后台任务队列相关联的计数器
bio_pending[type]++;
//向与指定类型的后台任务队列相关联的条件变量 bio_newjob_cond[type] 发出信号,通知后台线程有新的任务需要执行。
pthread_cond_signal(&bio_newjob_cond[type]);
//解锁与指定类型的后台任务队列相关联的互斥锁,释放对队列的控制权。
pthread_mutex_unlock(&bio_mutex[type]);
}
void aof_background_fsync(int fd) {
bioCreateBackgroundJob(BIO_AOF_FSYNC,(void*)(long)fd,NULL,NULL);
}
这段代码启动了一个后台任务,该任务在另一个线程中执行fsync()操作,作用于指定的文件描述符(AOF文件的文件描述符)。它调用了bioCreateBackgroundJob函数,该函数用于创建并提交一个后台任务,其中BIO_AOF_FSYNC表示执行AOF文件的同步操作。这样的异步fsync()操作可以提高性能,因为它允许主线程继续处理其他请求,而不必等待磁盘同步完成。在后台线程中执行同步操作可以将磁盘同步的开销分散到不同的线程,减少了对主线程的阻塞。
void killAppendOnlyChild(void) {
int statloc;
/* No AOFRW child? return. */
/*检查是否存在 AOFRW 子进程。如果不存在 (server.aof_child_pid == -1),则直接返回,无需执行终止操作。*/
if (server.aof_child_pid == -1) return;
/* Kill AOFRW child, wait for child exit. */
serverLog(LL_NOTICE,"Killing running AOF rewrite child: %ld",
(long) server.aof_child_pid);
/*通过 kill 函数发送 SIGUSR1 信号给 AOFRW 子进程,请求其终止。然后使用 wait3 等待 AOFRW 子进程的退出,确保子进程正常终止。*/
if (kill(server.aof_child_pid,SIGUSR1) != -1) {
while(wait3(&statloc,0,NULL) != server.aof_child_pid);
}
/* Reset the buffer accumulating changes while the child saves. */
/*在终止子进程后,重置累积子进程保存期间的更改的缓冲区。还会删除用于 IPC 的临时文件,关闭与子进程之间的管道,并更新字典调整策略。*/
aofRewriteBufferReset();//aof变为初始状态
aofRemoveTempFile(server.aof_child_pid);//删除生成的临时文件
server.aof_child_pid = -1;
server.aof_rewrite_time_start = -1;
/* Close pipes used for IPC between the two processes. */
aofClosePipes();//关闭重写期创建的管道
closeChildInfoPipe();//子进程向父进程写入的管道
updateDictResizePolicy();//更新哈希表的策略
}
这个是终止aof进程的操作。
/* Called when the user switches from "appendonly yes" to "appendonly no"
* at runtime using the CONFIG command. */
/*在用户通过运行时使用CONFIG命令将AOF(Append-Only File)持久化选项从"appendonly yes"切换为"appendonly no"时执行。具体操作包括:*/
void stopAppendOnly(void) {
serverAssert(server.aof_state != AOF_OFF);
flushAppendOnlyFile(1);//调用flushAppendOnlyFile函数刷新AOF文件中的内容。这是为了确保所有待写入的数据都被写入AOF文件。
redis_fsync(server.aof_fd);//使用redis_fsync函数执行强制的文件同步,确保AOF文件中的数据被物理地写入磁盘。
close(server.aof_fd);//使用close函数关闭AOF文件描述符(server.aof_fd)。关闭文件描述符是为了确保文件被正确关闭,不再接受写入操作。
server.aof_fd = -1;
server.aof_selected_db = -1;
server.aof_state = AOF_OFF;
server.aof_rewrite_scheduled = 0;
killAppendOnlyChild();
}
/* Called when the user switches from "appendonly no" to "appendonly yes"
* at runtime using the CONFIG command. */
/*
这个函数的作用是在运行时通过 CONFIG 命令将 AOF 持久化选项从 "appendonly no" 切换到 "appendonly yes"。*/
int startAppendOnly(void) {
char cwd[MAXPATHLEN]; /* Current working dir path for error messages. */
int newfd;
/*打开 AOF 文件: 使用 open 函数以写入、追加、创建文件的方式打开 AOF 文件。如果打开失败,记录错误日志,并返回 C_ERR 表示失败。*/
newfd = open(server.aof_filename,O_WRONLY|O_APPEND|O_CREAT,0644);
serverAssert(server.aof_state == AOF_OFF);
if (newfd == -1) {
char *cwdp = getcwd(cwd,MAXPATHLEN);
serverLog(LL_WARNING,
"Redis needs to enable the AOF but can't open the "
"append only file %s (in server root dir %s): %s",
server.aof_filename,
cwdp ? cwdp : "unknown",
strerror(errno));
return C_ERR;
}
/*处理已有后台操作: 如果存在正在运行的子进程(例如 RDB 持久化操作)且没有正在进行的 AOF 重写,
将标记 server.aof_rewrite_scheduled 设置为1,表示稍后会尝试进行 AOF 重写。
如果存在正在进行的 AOF 重写,需要终止它,因为旧的 AOF 文件不再累积 AOF 缓冲。*/
if (hasActiveChildProcess() && server.aof_child_pid == -1) {
server.aof_rewrite_scheduled = 1;
serverLog(LL_WARNING,"AOF was enabled but there is already another background operation. An AOF background was scheduled to start when possible.");
} else {
/* If there is a pending AOF rewrite, we need to switch it off and
* start a new one: the old one cannot be reused because it is not
* accumulating the AOF buffer. */
if (server.aof_child_pid != -1) {
serverLog(LL_WARNING,"AOF was enabled but there is already an AOF rewriting in background. Stopping background AOF and starting a rewrite now.");
killAppendOnlyChild();
}
if (rewriteAppendOnlyFileBackground() == C_ERR) {//AOF重写的开始阶段,将已有的键值对先写入,然后再开始。
close(newfd);
serverLog(LL_WARNING,"Redis needs to enable the AOF but can't trigger a background AOF rewrite operation. Check the above logs for more info about the error.");
return C_ERR;
}
}
/* We correctly switched on AOF, now wait for the rewrite to be complete
* in order to append data on disk. */
/*等待 AOF 重写完成: 将服务器的 AOF 状态设置为 AOF_WAIT_REWRITE,
表示需要等待 AOF 重写完成后才能追加数据到磁盘。同时记录最后一次执行 fsync 的时间,以便之后进行 fsync 操作。*/
server.aof_state = AOF_WAIT_REWRITE;
server.aof_last_fsync = server.unixtime;
server.aof_fd = newfd;
return C_OK;
}
上述两段代码分别是结束aof是需要进行刷新磁盘操作,
在开始aof是需要判断是否有aof进程,是否aof重写已开启。
aof的写入策略
#define AOF_WRITE_LOG_ERROR_RATE 30 /* Seconds between errors logging. */
void flushAppendOnlyFile(int force) {
ssize_t nwritten;
int sync_in_progress = 0;//表示是否当前正在进行同步操作(sync)。
mstime_t latency;//监控写入数据的时间
/*对AOF缓冲区是否为空的检查和是否需要执行fsync的判断*/
if (sdslen(server.aof_buf) == 0) {//这个条件检查AOF缓冲区是否为空。如果AOF缓冲区为空,表示当前没有待写入的AOF数据。
/* Check if we need to do fsync even the aof buffer is empty,
* because previously in AOF_FSYNC_EVERYSEC mode, fsync is
* called only when aof buffer is not empty, so if users
* stop write commands before fsync called in one second,
* the data in page cache cannot be flushed in time. */
/*server.aof_fsync == AOF_FSYNC_EVERYSEC: 这个条件检查是否设置了AOF的fsync策略为每秒一次。
server.aof_fsync_offset != server.aof_current_size: 这个条件检查AOF文件当前的大小是否等于上一次执行fsync时的偏移量。如果AOF文件大小变化了,说明有新的数据写入。
server.unixtime > server.aof_last_fsync: 这个条件检查当前时间是否大于上一次执行fsync的时间。确保fsync不会频繁执行。
!(sync_in_progress = aofFsyncInProgress()): 这个条件检查是否有AOF fsync正在进行。aofFsyncInProgress()函数用于检查是否有正在进行的AOF fsync操作。*/
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;
}
}
if (server.aof_fsync == AOF_FSYNC_EVERYSEC)
sync_in_progress = aofFsyncInProgress();//返回等待处理的后台链表IO的数量
/*它将每个写操作追加到一个文件中,以便在服务器重启时可以重新加载数据。这段代码主要处理在每秒进行一次fsync的情况,即AOF_FSYNC_EVERYSEC。*/
if (server.aof_fsync == AOF_FSYNC_EVERYSEC && !force) {
/* With this append fsync policy we do background fsyncing.
* If the fsync is still in progress we can try to delay
* the write for a couple of seconds. */
/*如果sync_in_progress为真,表示有fsync操作正在进行。
此时可能会进行一些延迟处理,因为在Linux等系统中,一个fsync可能会阻塞后续的写入操作。*/
if (sync_in_progress) {
if (server.aof_flush_postponed_start == 0) {
/* No previous write postponing, remember that we are
* postponing the flush and return. */
/*如果之前没有推迟的写入操作,那么记录当前时间,表示现在开始推迟刷新(flush)操作,并直接返回,不进行实际的写入操作。*/
server.aof_flush_postponed_start = server.unixtime;
return;
} else if (server.unixtime - server.aof_flush_postponed_start < 2) {
/* We were already waiting for fsync to finish, but for less
* than two seconds this is still ok. Postpone again. */
/*如果之前已经在等待fsync操作完成,但等待时间不到两秒,那么可以继续推迟刷新操作。*/
return;
}
/* Otherwise fall trough, and go write since we can't wait
* over two seconds. */
/*如果不满足上述两个条件(之前没有推迟写入,或者已经推迟的时间超过两秒),则执行写入操作,因为此时无法再等待超过两秒。*/
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.");
}
}
/* We want to perform a single write. This should be guaranteed atomic
* at least if the filesystem we are writing is a real physical one.
* While this will save us against the server being killed I don't think
* there is much to do about the whole server stopping for power problems
* or alike */
/*达了对于单个写入操作的期望是原子的,至少在写入的文件系统是真实的物理文件系统时。
这是因为,即使在服务器被强制终止的情况下,这样的操作至少能够保护写入的数据。*/
if (server.aof_flush_sleep && sdslen(server.aof_buf)) {
/*
如果设置了 server.aof_flush_sleep 并且 AOF 缓冲区中有数据,那么会通过 usleep 函数在写入之前进行睡眠。
这是一种人为地引入延迟的方式,用于模拟在真实环境中可能遇到的写入延迟情况。这在某些测试场景下可能是有用的。
*/
usleep(server.aof_flush_sleep);
}
latencyStartMonitor(latency);
/*使用 aofWrite 函数将 AOF 缓冲区的内容写入到 AOF 文件(server.aof_fd)中,并记录实际写入的字节数(nwritten)。*/
nwritten = aofWrite(server.aof_fd,server.aof_buf,sdslen(server.aof_buf));
latencyEndMonitor(latency);
/* We want to capture different events for delayed writes:
* when the delay happens with a pending fsync, or with a saving child
* active, and when the above two conditions are missing.
* We also use an additional event name to save all samples which is
* useful for graphing / monitoring purposes. */
/*如果有挂起的 fsync 操作 (sync_in_progress),则使用 latencyAddSampleIfNeeded 记录 "aof-write-pending-fsync" 事件的延迟。
如果有活动的子进程(例如正在进行 RDB 快照),则使用 latencyAddSampleIfNeeded 记录 "aof-write-active-child" 事件的延迟。
如果既没有挂起的 fsync 操作,也没有活动的子进程,则使用 latencyAddSampleIfNeeded 记录 "aof-write-alone" 事件的延迟。
无论事件类型如何,都使用 latencyAddSampleIfNeeded 记录 "aof-write" 事件的延迟。这个事件类型记录了所有样本,用于综合监控和图形化展示*/
if (sync_in_progress) {
latencyAddSampleIfNeeded("aof-write-pending-fsync",latency);//这个函数将高于一个时间长度的样本放入时间队列中去。
} else if (hasActiveChildProcess()) {
latencyAddSampleIfNeeded("aof-write-active-child",latency);
} else {
latencyAddSampleIfNeeded("aof-write-alone",latency);
}
latencyAddSampleIfNeeded("aof-write",latency);
/* We performed the write so reset the postponed flush sentinel to zero. */
/*已经执行了写入操作,因此将推迟的刷新标志*/
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;
/* Limit logging rate to 1 line per AOF_WRITE_LOG_ERROR_RATE seconds. */
/*限制日志速率: 限制错误日志的记录速率,以免在短时间内多次记录相同的错误。*/
if ((server.unixtime - last_write_error_log) > AOF_WRITE_LOG_ERROR_RATE) {
can_log = 1;
last_write_error_log = server.unixtime;
}
/* Log the AOF write error and record the error code. */
/*记录 AOF 写入错误: 如果写入返回 -1,表示发生了错误,记录错误日志和错误码。*/
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 {
/*处理短写入: 如果发生了短写入,试图截断 AOF 文件,将其大小设置为当前有效数据的大小。
如果截断成功,将 nwritten 设置为 -1,表示已经没有部分数据存在于 AOF 文件中。*/
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));
}
/*截断 AOF 文件: 尝试使用 ftruncate 系统调用截断 AOF 文件,将其大小设置为当前有效数据的大小。
即,如果已经写入了部分数据,将文件截断为有效数据的末尾,丢弃不完整的数据。*/
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 {
/* If the ftruncate() succeeded we can set nwritten to
* -1 since there is no longer partial data into the AOF. */
nwritten = -1;
}
/*设置错误码: 如果截断操作失败,记录相应的错误信息。
无论截断是否成功,都将错误码设置为 ENOSPC,表示没有足够的空间执行请求的操作。*/
server.aof_last_write_errno = ENOSPC;
}
/* Handle the AOF write error. */
if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
/* We can't recover when the fsync policy is ALWAYS since the
* reply for the client is already in the output buffers, and we
* have the contract with the user that on acknowledged write data
* is synced on disk. */
/*如果 AOF 的 fsync 策略被设置为 AOF_FSYNC_ALWAYS,则表示在向客户端回复之前,必须确保写入的数据已经同步到磁盘上。
因此,如果发生 AOF 写入错误,并且 fsync 策略为 AOF_FSYNC_ALWAYS,则认为这是一个严重错误,
无法从中恢复。因此,记录一条警告日志,并强制退出 Redis 服务。*/
serverLog(LL_WARNING,"Can't recover from AOF write error when the AOF fsync policy is 'always'. Exiting...");
exit(1);
} else {
/* Recover from failed write leaving data into the buffer. However
* set an error to stop accepting writes as long as the error
* condition is not cleared. */
server.aof_last_write_status = C_ERR;
/* Trim the sds buffer if there was a partial write, and there
* was no way to undo it with ftruncate(2). */
/*处理其他情况: 如果 fsync 策略不是 AOF_FSYNC_ALWAYS,
则表示可以通过一些手段从错误中恢复。在这种情况下,
设置 server.aof_last_write_status 为 C_ERR,表示当前 AOF 的写入状态为错误。然后,如果发生了部分写入(nwritten > 0),
将当前 AOF 文件的大小增加已写入的字节数,同时修剪 AOF 缓冲区,去除已经写入到文件的部分。*/
if (nwritten > 0) {
server.aof_current_size += nwritten;
sdsrange(server.aof_buf,nwritten,-1);
}
return; /* We'll try again on the next call... */
}
} else {
/* Successful write(2). If AOF was in error state, restore the
* OK state and log the event. */
/*如果写入成功,首先检查之前是否发生过 AOF 写入错误。如果之前处于错误状态 (C_ERR),
则将状态恢复为正常 (C_OK),并记录一条日志表示 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;
}
}
//将 AOF 文件的当前大小增加刚刚写入的字节数。
server.aof_current_size += nwritten;
/* Re-use AOF buffer when it is small enough. The maximum comes from the
* arena size of 4k minus some overhead (but is otherwise arbitrary). */
/*重新利用 AOF 缓冲区: 如果 AOF 缓冲区的大小不超过4k(通过计算 sdslen(server.aof_buf) + sdsavail(server.aof_buf) 判断),
则清空 AOF 缓冲区,以便重用。否则,释放当前的 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:
/*这段代码是AOF缓冲区非空时,执行fsync的逻辑。*/
/* Don't fsync if no-appendfsync-on-rewrite is set to yes and there are
* children doing I/O in the background. */
/*这个条件检查是否设置了no-appendfsync-on-rewrite为yes,并且是否有正在进行I/O的子进程。如果满足这两个条件,表示在重写AOF文件时不执行fsync。*/
if (server.aof_no_fsync_on_rewrite && hasActiveChildProcess())
return;
/* Perform the fsync if needed. */
/*这个条件检查AOF的fsync策略是否为always,即每次写入都执行fsync。如果是,那么执行redis_fsync(server.aof_fd)进行fsync操作。
此处使用了redis_fsync,它在Linux上被定义为fdatasync(),以避免刷新元数据。*/
if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
/* redis_fsync is defined as fdatasync() for Linux in order to avoid
* flushing metadata. */
latencyStartMonitor(latency);
redis_fsync(server.aof_fd); /* Let's try to get this data on the disk */
latencyEndMonitor(latency);
latencyAddSampleIfNeeded("aof-fsync-always",latency);
server.aof_fsync_offset = server.aof_current_size;
server.aof_last_fsync = server.unixtime;
/*这个条件检查AOF的fsync策略是否为everysec,即每秒执行一次fsync,并且距离上一次fsync已经过去了一秒钟。
如果是,且没有正在进行的fsync操作(!sync_in_progress),
则调用aof_background_fsync(server.aof_fd)启动后台线程执行fsync,并更新server.aof_fsync_offset为当前AOF文件的大小。*/
} 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操作放入一个链表中,等待去执行*/
server.aof_fsync_offset = server.aof_current_size;
}
server.aof_last_fsync = server.unixtime;
}
}
AOF写入缓冲区: Redis在内存中维护了一个AOF写入缓冲区,用于累积所有需要写入AOF的数据。这个缓冲区包含了将要追加到AOF文件的命令。
fsync策略: 代码中提到了fsync的策略。在Redis中,可以通过配置项设置fsync的策略,例如,可以选择每秒进行一次fsync。这决定了何时将AOF缓冲区的内容真正写入磁盘。fsync(文件同步)是一种将文件系统缓冲区中的数据强制刷新到磁盘的操作。在一个典型的文件系统中,写入的数据首先被缓存在内存中,以提高性能。然而,这种数据仅在内存中,尚未写入到永久存储(例如硬盘)中。fsync 操作用于确保数据被写入到磁盘,以保证数据的持久性。
force参数: force参数用于控制是否强制将AOF缓冲区的内容写入磁盘。当force为1时,不论后台是否有fsync在进行,都会进行写入。
错误日志: 代码中引入AOF_WRITE_LOG_ERROR_RATE常量,表示在两次错误日志之间的最小时间间隔。如果写入磁盘时发生错误,系统将每隔一定时间记录一次错误日志,以避免在短时间内过多记录相同错误。
void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int argc) {
//创建一个空的SDS(Simple Dynamic String)缓冲区 buf,用于构建AOF文件的内容。
sds buf = sdsempty();
robj *tmpargv[3];
/* The DB this command was targeting is not the same as the last command
* we appended. To issue a SELECT command is needed. */
/*这个命令要查询的数据库不同于我们之前追加的上一个命令所操作的数据库。因此,需要执行一个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;//设置AOF选择该数据库。
}
//如果当前命令是 EXPIRE、PEXPIRE 或 EXPIREAT,会将这些命令转换成 PEXPIREAT 命令,以便在AOF文件中记录。
if (cmd->proc == expireCommand || cmd->proc == pexpireCommand ||
cmd->proc == expireatCommand) {//这些命令是键过期的通用操作,用来设置键的过期时间和发送通知给订阅该键的客户端。
/* Translate EXPIRE/PEXPIRE/EXPIREAT into PEXPIREAT */
buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);//这种命令的字符串。
/*如果当前命令是 SETEX 或 PSETEX,会将它们转换成 SET 命令和 PEXPIREAT 命令,然后记录到AOF文件中。*/
} 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]);//指定键过期时间,构建追加到AOF中的命令。
/*如果当前命令是 SET 命令且带有过期参数("EX" 或 "PX"),会将 SET 命令和对应的 PEXPIREAT 或 EXPIREAT 命令一起记录到AOF文件中。*/
} else if (cmd->proc == setCommand && argc > 3) {
int i;
robj *exarg = NULL, *pxarg = NULL;
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];
}
/*如果同时存在EX和PX选项,这是不合法的,因此通过serverAssert断言确保它们不会同时存在。*/
serverAssert(!(exarg && pxarg));
if (exarg || pxarg) {
/* Translate SET [EX seconds][PX milliseconds] to SET and PEXPIREAT */
/*调用catAppendOnlyExpireAtCommand函数将对应的EXPIREAT或PEXPIREAT命令参数添加到命令字符串中。*/
buf = catAppendOnlyGenericCommand(buf,3,argv);
if (exarg)
buf = catAppendOnlyExpireAtCommand(buf,server.expireCommand,argv[1],
exarg);
if (pxarg)
buf = catAppendOnlyExpireAtCommand(buf,server.pexpireCommand,argv[1],
pxarg);
} else {
buf = catAppendOnlyGenericCommand(buf,argc,argv);
}
/*对于其他命令,会直接将命令和参数记录到AOF文件中,没有额外的转换。*/
} else {
/* All the other commands don't need translation or need the
* same translation already operated in the command vector
* for the replication itself. */
/*对于其他命令而言,它们不需要进行额外的翻译或者已经在用于复制的命令向量中进行了相同的翻译。
在这个上下文中,"translation" 指的是对命令参数的处理,特别是与复制(replication)相关的处理。
在Redis中,对于一些命令,特别是那些涉及到过期时间、持久性、复制等方面的命令,需要将它们的操作记录下来,
以便在进行数据复制(replication)时能够准确地复制这些操作。*/
buf = catAppendOnlyGenericCommand(buf,argc,argv);
}
/* 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. */
/*如果服务器的AOF状态为AOF_ON(AOF日志已启用),则将构建的内容追加到服务器的AOF缓冲区(server.aof_buf)中,以便稍后将其刷新到AOF文件中。*/
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. */
/*如果后台AOF文件重写正在进行中,当前操作将累积到AOF重写缓冲区,以便在AOF重写完成后将差异追加到新的AOF文件中。*/
if (server.aof_child_pid != -1)
aofRewriteBufferAppend((unsigned char*)buf,sdslen(buf));
sdsfree(buf);
}
这个函数是用于将命令和参数追加到AOF(Append-Only File)文件中,以记录Redis数据库的写操作。AOF文件是Redis的一种持久化方式,它记录了每个写操作的命令和参数,用于在服务器重启时恢复数据。这个函数的目的就是为了能设置命令如何如何变成字符串追加到aof中
aof文件载入
int loadAppendOnlyFile(char *filename) {
struct client *fakeClient;/*创建一个指向 struct client 结构的指针,用于模拟执行 AOF 文件中的命令。*/
FILE *fp = fopen(filename,"r");/*打开 AOF 文件,返回文件指针 fp,以便后续读取文件内容。*/
struct redis_stat sb;/*定义一个 struct redis_stat 结构,用于存储文件状态信息,包括文件大小等。*/
int old_aof_state = server.aof_state;/*:保存当前服务器的 AOF 状态,以便在加载完毕后还原。*/
long loops = 0;/*用于记录循环的次数,每循环一定次数会检查是否需要服务其他事件,防止长时间阻塞。*/
off_t valid_up_to = 0; /* Offset of latest well-formed command loaded. *//*记录已加载的最后一个格式正确的命令的偏移量。*/
off_t valid_before_multi = 0; /* Offset before MULTI command loaded. *//*记录加载过程中碰到 MULTI 命令之前的偏移量,用于处理未完成的 MULTI/EXEC 事务。*/
if (fp == NULL) {
serverLog(LL_WARNING,"Fatal error: can't open the append log file for reading: %s",strerror(errno));
exit(1);
}
/* Handle a zero-length AOF file as a special case. An empty AOF file
* is a valid AOF because an empty server with AOF enabled will create
* a zero length file at startup, that will remain like that if no write
* operation is received. */
/*这段代码处理了 AOF 文件长度为零的特殊情况。如果 AOF 文件存在且长度为零,说明这是一个空的 AOF 文件。
对于启用 AOF 的空服务器,它在启动时会创建一个零长度的 AOF 文件,如果在之后没有接收到写操作,这个文件的长度将保持为零。*/
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;
}
/* Temporarily disable AOF, to prevent EXEC from feeding a MULTI
* to the same file we're about to read. */
/*这部分代码的目的是在加载 AOF 文件之前,暂时禁用 AOF 功能,以防止在读取 AOF 文件时,EXEC 命令将事务写入相同的文件。*/
server.aof_state = AOF_OFF;
fakeClient = createAOFClient();//创建客户端
/*该函数用于开始加载文件。fp 是 AOF 文件的文件指针,filename 是 AOF 文件的文件名,RDBFLAGS_AOF_PREAMBLE 是加载 AOF 文件时使用的标志。*/
startLoadingFile(fp, filename, RDBFLAGS_AOF_PREAMBLE);//加载数据并传递消息
/* Check if this AOF file has an RDB preamble. In that case we need to
* load the RDB file and later continue loading the AOF tail. */
/*这段代码用于检查 AOF 文件是否具有 RDB 前导部分。*/
char sig[5]; /* "REDIS" */
/*
如果读取的字节数不等于 5,或者读取的内容与 "REDIS" 不匹配,说明没有 RDB 前导部分。
如果没有 RDB 前导部分,通过 fseek 将文件指针移动回文件开头。
如果读取成功且内容匹配 "REDIS",则说明存在 RDB 前导部分。
这种情况下,创建一个 rio 结构(Redis 的 I/O 结构)并初始化它,然后使用 rdbLoadRio 函数加载 RDB 文件的内容。
如果加载 RDB 文件的过程中出现错误,记录错误日志,并通过 goto 跳转到错误处理的标签 readerr。
如果加载成功,记录日志,表示正在读取 AOF 文件的剩余部分。
*/
if (fread(sig,1,5,fp) != 5 || memcmp(sig,"REDIS",5) != 0) {
/* No RDB preamble, seek back at 0 offset. */
/*这里的代码使用 fseek 将文件指针移动回文件开头(SEEK_SET),以便重新开始读取文件。*/
if (fseek(fp,0,SEEK_SET) == -1) goto readerr;
} else {
/* RDB preamble. Pass loading the RDB functions. */
/*创建 Rio 结构体:创建一个 Rio 结构体(rio rdb),这是 Redis 的 I/O 抽象层,用于在不同的数据源上执行读写操作。*/
rio rdb;
serverLog(LL_NOTICE,"Reading RDB preamble from AOF file...");
/*将文件指针移回文件开头:使用 fseek 将文件指针移回文件开头,以便从头开始读取文件。*/
if (fseek(fp,0,SEEK_SET) == -1) goto readerr;
/*初始化 Rio 结构体:使用 rioInitWithFile 初始化 Rio 结构体,使其与打开的文件关联。*/
rioInitWithFile(&rdb,fp);
/*加载 RDB 数据:使用 rdbLoadRio 函数从 Rio 结构体中加载 RDB 数据。这里的参数 RDBFLAGS_AOF_PREAMBLE 表示加载 RDB 前导部分。*/
if (rdbLoadRio(&rdb,RDBFLAGS_AOF_PREAMBLE,NULL) != 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...");
}
}
/* Read the actual AOF file, in REPL format, command by command. */
/*这段代码是 Redis 在载入 AOF 文件时的处理逻辑。AOF 文件记录了 Redis 服务器接收到的写命令,以便在服务器启动时将数据还原到内存中。*/
while(1) {
int argc, j;
unsigned long len;
robj **argv;
char buf[128];
sds argsds;
struct redisCommand *cmd;
/* Serve the clients from time to time */
if (!(loops++ % 1000)) {/*这个条件每隔 1000 次循环执行一次。*/
loadingProgress(ftello(fp));/*就调用 loadingProgress 函数,将当前 AOF 文件的偏移量传递给它。这个函数可能用于记录加载进度,以便在启动期间向用户报告进度。*/
processEventsWhileBlocked();/*在服务客户端的同时,处理在事件循环中被阻塞的事件。这是一个异步事件处理函数,用于处理非阻塞操作或异步事件。*/
processModuleLoadingProgressEvent(1);/*处理模块加载进度事件。这是一个用于处理模块加载时的异步事件。参数 1 可能表示进度的增量。*/
}
if (fgets(buf,sizeof(buf),fp) == NULL) {/*使用 fgets 从 AOF 文件中逐行读取命令,每一行代表一个 Redis 命令。*/
if (feof(fp))
break;
else
goto readerr;
}
if (buf[0] != '*') goto fmterr;/*检查 AOF 文件中的下一行是否以 * 开头。在 Redis 的 AOF 文件中,* 用于表示命令的参数个数。*/
if (buf[1] == '\0') goto readerr;
argc = atoi(buf+1);
if (argc < 1) goto fmterr;
/* Load the next command in the AOF as our fake client
* argv. */
argv = zmalloc(sizeof(robj*)*argc);/*分配一个数组,用于存储命令的参数。每个参数都是一个 robj* 类型的对象指针。*/
fakeClient->argc = argc;
fakeClient->argv = argv;
for (j = 0; j < argc; j++) {/*历命令的每个参数。*/
/* Parse the argument len. */
char *readres = fgets(buf,sizeof(buf),fp);/*从 AOF 文件中读取一行内容到缓冲区 buf,该行表示参数的长度信息。*/
/*检查读取是否成功,并且该行的第一个字符是否为 $,如果不是,说明 AOF 文件格式有错误,跳转到相应的错误处理*/
if (readres == NULL || buf[0] != '$') {
fakeClient->argc = j; /* Free up to j-1. */
freeFakeClientArgv(fakeClient);
if (readres == NULL)
goto readerr;
else
goto fmterr;
}
len = strtol(buf+1,NULL,10);/*将参数长度信息解析为整数。*/
/* Read it into a string object. */
argsds = sdsnewlen(SDS_NOINIT,len);/*使用参数长度创建一个未初始化的 sds 对象。*/
if (len && fread(argsds,len,1,fp) == 0) {
sdsfree(argsds);
fakeClient->argc = j; /* Free up to j-1. */
freeFakeClientArgv(fakeClient);
goto readerr;
}
argv[j] = createObject(OBJ_STRING,argsds);
/* Discard CRLF. */
if (fread(buf,2,1,fp) == 0) {
fakeClient->argc = j+1; /* Free up to j. */
freeFakeClientArgv(fakeClient);
goto readerr;
}
}
/* Command lookup */
cmd = lookupCommand(argv[0]->ptr);//使用命令名(argv[0]->ptr 中保存)在 Redis 的命令表中查找相应的命令。
if (!cmd) {
serverLog(LL_WARNING,
"Unknown command '%s' reading the append only file",
(char*)argv[0]->ptr);
exit(1);
}
/*如果当前命令是 MULTI 命令,将 valid_up_to 的值保存到 valid_before_multi 中。
这是因为在 Redis 的 AOF 重写过程中,遇到 MULTI 命令时,会开始记录事务执行之前的 AOF 文件位置。*/
if (cmd == server.multiCommand) valid_before_multi = valid_up_to;
/* Run the command in the context of a fake client */
/*将当前命令设置为 fake client 的当前命令和最后一次执行的命令。*/
fakeClient->cmd = fakeClient->lastcmd = cmd;
/*如果 fake client 在一个事务块(MULTI)中,并且当前命令不是 EXEC,那么将该命令加入事务队列,而不是立即执行。*/
if (fakeClient->flags & CLIENT_MULTI &&
fakeClient->cmd->proc != execCommand)
{
queueMultiCommand(fakeClient);
} else {
cmd->proc(fakeClient);
}
/* The fake client should not have a reply */
/*通过断言检查 fake client 的状态,确保在执行命令后,fake client 的输出缓冲区(bufpos)为空,回复链表(reply)中没有任何回复。*/
serverAssert(fakeClient->bufpos == 0 &&
listLength(fakeClient->reply) == 0);
/* The fake client should never get blocked */
/*再次通过断言检查 fake client 的状态,确保 fake client 没有被阻塞。*/
serverAssert((fakeClient->flags & CLIENT_BLOCKED) == 0);
/* Clean up. Command code may have changed argv/argc so we use the
* argv/argc of the client instead of the local variables. */
/*释放 fake client 使用的参数列表。*/
freeFakeClientArgv(fakeClient);
fakeClient->cmd = NULL;
/*如果在 AOF 文件加载时检测到截断,将 valid_up_to 更新为当前文件位置。*/
if (server.aof_load_truncated) valid_up_to = ftello(fp);
/*如果设置了键加载延迟,等待指定的微秒数。这通常是为了模拟在处理大量键时可能发生的延迟*/
if (server.key_load_delay)
usleep(server.key_load_delay);
}
/* This point can only be reached when EOF is reached without errors.
* If the client is in the middle of a MULTI/EXEC, handle it as it was
* a short read, even if technically the protocol is correct: we want
* to remove the unprocessed tail and continue. */
/*
这段代码处理在 AOF 文件结束时可能存在的情况。具体来说,它检查是否在文件末尾遇到了 EOF,
并且客户端仍然处于一个未完成的 MULTI/EXEC 事务中。如果是这样,
它会回滚该事务,将 valid_up_to(AOF 文件中已经处理的位置)回退到进入事务之前的位置,并跳转到 uxeof 标签,以处理未完成的事务。
*/
if (fakeClient->flags & CLIENT_MULTI) {
serverLog(LL_WARNING,
"Revert incomplete MULTI/EXEC transaction in AOF file");
valid_up_to = valid_before_multi;
goto uxeof;
}
loaded_ok: /* DB loaded, cleanup and return C_OK to the caller. */
fclose(fp);//关闭 AOF 文件。
freeFakeClient(fakeClient);//释放用于加载 AOF 文件的虚拟客户端的内存。
server.aof_state = old_aof_state;//将服务器的 AOF 状态还原为之前保存的状态(old_aof_state)。
stopLoading(1);//停止加载操作,参数 1 表示加载成功。
aofUpdateCurrentSize();//更新 AOF 当前文件的大小。
server.aof_rewrite_base_size = server.aof_current_size;//将 AOF 重写基准大小设置为当前 AOF 文件的大小。
server.aof_fsync_offset = server.aof_current_size;//:将 AOF 文件 fsync 偏移量设置为当前 AOF 文件的大小。
return C_OK;
readerr: /* Read error. If feof(fp) is true, fall through to unexpected EOF. */
if (!feof(fp)) {//检查文件是否在读取错误时达到了文件结尾。如果没有到达文件结尾,
//如果虚拟客户端存在,释放虚拟客户端的内存。这是为了避免 valgrind 在退出时发出警告。
if (fakeClient) freeFakeClient(fakeClient); /* avoid valgrind warning */
fclose(fp);
serverLog(LL_WARNING,"Unrecoverable error reading the append only file: %s", strerror(errno));
exit(1);
}
uxeof: /* Unexpected AOF end of file. */
/*检查服务器配置中是否启用了 aof-load-truncated 选项,该选项表示是否在 AOF 文件被截断的情况下继续加载。
如果启用了 aof-load-truncated 选项:
记录警告信息到服务器日志,说明在加载 AOF 文件时发生了短读取(short read)。
记录截断的 AOF 文件偏移量。
尝试截断 AOF 文件到有效的位置,如果截断失败则记录相应错误信息。
如果截断成功,确保 AOF 文件描述符指向文件末尾。
记录 AOF 文件加载完成的信息,跳转到 loaded_ok 标签。
如果未启用 aof-load-truncated 选项:
如果虚拟客户端存在,释放虚拟客户端的内存,避免 valgrind 警告。
关闭 AOF 文件。
记录错误信息到服务器日志,指示 AOF 文件意外结束。
提示用户可能的解决方法,包括使用 redis-check-aof --fix 进行修复或启用 aof-load-truncated 选项。
exit(1);:退出服务器进程,返回退出码 1,表示异常终止。这是因为 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 {
/* Make sure the AOF file descriptor points to the end of the
* file after the truncate call. */
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); /* avoid valgrind warning */
fclose(fp);
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: /* Format error. */
//结束进程。
if (fakeClient) freeFakeClient(fakeClient); /* avoid valgrind warning */
fclose(fp);
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);
}
这段代码的作用是负责加载 AOF 文件中的命令,并模拟执行这些命令,用于将之前的写操作还原到数据库中。在加载的过程中,还包括了一些错误处理和恢复机制。
其主要操作时先打开aof文件,创造一个虚假的客户端,创造一个数据库,使用aof文件前导的rdb文件来恢复数据库。由于执行aof文件命令的操作时间可能很长所以在执行时定期执行其他命令
if (!(loops++ % 1000)) {/*这个条件每隔 1000 次循环执行一次。*/
loadingProgress(ftello(fp));/*就调用 loadingProgress 函数,将当前 AOF 文件的偏移量传递给它。这个函数可能用于记录加载进度,以便在启动期间向用户报告进度。*/
processEventsWhileBlocked();/*在服务客户端的同时,处理在事件循环中被阻塞的事件。这是一个异步事件处理函数,用于处理非阻塞操作或异步事件。*/
processModuleLoadingProgressEvent(1);/*处理模块加载进度事件。这是一个用于处理模块加载时的异步事件。参数 1 可能表示进度的增量。*/
}
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. */
/*打开一个临时文件 temp-rewriteaof-<pid>.aof,其中 <pid> 是当前进程的进程号,用于写入重写后的 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 重写的过程中,父进程将差异数据发送给子进程,子进程使用这个 sds 对象来保存接收到的差异数据。
server.aof_child_diff = sdsempty();
/*这一行代码初始化了一个文件 I/O 结构 rio,并将其关联到文件指针 fp 上。rio 是 Redis 中用于进行数据读写的基本结构,
它提供了一组用于读写的函数,并可以与不同的数据源(如文件、内存等)关联。*/
rioInitWithFile(&aof,fp);//初始化写缓冲区
/*如果配置项 aof_rewrite_incremental_fsync 开启(值为非零),则通过 rioSetAutoSync 函数设置 rio 对象的自动同步参数。
这样可以在写入一定数量的字节后自动执行文件同步操作,确保数据被及时刷写到磁盘。*/
if (server.aof_rewrite_incremental_fsync)//确保数据刷入磁盘
rioSetAutoSync(&aof,REDIS_AUTOSYNC_BYTES);
/*开始保存数据到文件。参数 RDBFLAGS_AOF_PREAMBLE 用于指示这是 AOF 文件的预处理阶段,可能会在保存过程中执行一些预处理操作。*/
startSaving(RDBFLAGS_AOF_PREAMBLE);
/*根据配置项 aof_use_rdb_preamble 的设置,选择是执行 RDB 文件的预处理阶段还是执行 AOF 文件的预处理阶段。*/
if (server.aof_use_rdb_preamble) {
int error;
/*表示启用了 AOF 文件使用 RDB 文件的预处理阶段,即使用 RDB 文件的内容作为 AOF 文件的开头。
此时,调用 rdbSaveRio 函数执行 RDB 文件的保存操作,并将保存的内容写入给定的 rio 结构。
如果保存操作失败,将错误码设置到 errno,并跳转到错误处理标签 werr。*/
if (rdbSaveRio(&aof,&error,RDBFLAGS_AOF_PREAMBLE,NULL) == C_ERR) {
errno = error;
goto werr;
}
} else {
/*
表示不启用 AOF 文件使用 RDB 文件的预处理阶段,即执行 AOF 文件的预处理阶段。
此时,调用 rewriteAppendOnlyFileRio 函数执行 AOF 文件的预处理操作,
并将预处理的内容写入给定的 rio 结构。如果预处理操作失败,同样跳转到错误处理标签 werr。
*/
if (rewriteAppendOnlyFileRio(&aof) == C_ERR) goto werr;
}
/* Do an initial slow fsync here while the parent is still sending
* data, in order to make the next final fsync faster. */
/*这段代码执行了两个文件同步(fsync)操作,用于确保数据写入磁盘。这两个操作的目的是在父进程仍然在发送数据的时候进行初始的慢速文件同步,以使接下来的最终文件同步更加迅速。*/
/*这个操作会刷新文件流(file stream)的缓冲区,将缓冲区中的数据写入文件。在这里,fp是文件指针,指向一个打开的文件。
刷新缓冲区确保了所有在缓冲区中的数据都被写入文件。如果操作成功,fflush 返回零,否则返回 EOF。*/
if (fflush(fp) == EOF) goto werr;
/*这个操作将文件描述符 fileno(fp) 所对应的文件(通过 fp 获取文件描述符)进行同步。这将确保文件系统中的数据被写入磁盘。*/
if (fsync(fileno(fp)) == -1) goto werr;
/* Read again a few times to get more data from the parent.
* We can't read forever (the server may receive data from clients
* faster than it is able to send data to the child), so we try to read
* some more data in a loop as soon as there is a good chance more data
* will come. If it looks like we are wasting time, we abort (this
* happens after 20 ms without new data). */
/*这段代码的目的是在一定的时间内尝试从父进程读取更多的数据。具体解释如下:*/
int nodata = 0;
mstime_t start = mstime();
/*time()-start < 1000 表示在过去的一秒内进行尝试。nodata < 20 表示允许 20 次没有新数据的尝试。
这是为了防止无限制地等待新数据,即使父进程发送数据的速度较慢。*/
//定时读取
while(mstime()-start < 1000 && nodata < 20) {
/*
aeWait: 这是 Redis 的事件循环中的一个函数,用于等待文件描述符变为可读。server.aof_pipe_read_data_from_parent 是与父进程通信的管道的读取端。如果它变得可读,说明父进程有新的数据发送过来。
如果 aeWait 返回的值小于等于 0,表示在等待期间没有可读事件发生,即没有新数据。在这种情况下,增加 nodata 计数器,表示一次没有新数据的尝试。
如果有新数据可读,则调用 aofReadDiffFromParent 函数,将新数据读取并追加到当前子进程的缓冲区。
*/
if (aeWait(server.aof_pipe_read_data_from_parent, AE_READABLE, 1) <= 0)
{
nodata++;
continue;
}
nodata = 0; /* Start counting from zero, we stop on N *contiguous*
timeouts. */
aofReadDiffFromParent();
}
/* Ask the master to stop sending diffs. */
/*这段代码是子进程通知父进程停止发送差异数据(diffs)的过程。*/
/*向父进程发送停止差异数据的请求:
write(server.aof_pipe_write_ack_to_parent, "!", 1): 子进程通过管道向父进程发送一个字符 "!" 表示停止发送差异数据的请求。这个管道是双向的,用于父子进程之间的通信。
anetNonBlock(NULL, server.aof_pipe_read_ack_from_parent): 设置从父进程读取ACK的管道为非阻塞模式。这是为了避免在读取ACK时阻塞。*/
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;
/* We read the ACK from the server using a 10 seconds timeout. Normally
* it should reply ASAP, but just in case we lose its reply, we are sure
* the child will eventually get terminated. */
/*
syncRead(server.aof_pipe_read_ack_from_parent, &byte, 1, 5000): 阻塞等待从父进程读取一个字节的ACK。
超时时间为5秒。如果成功读取,并且读取到的字节是 "!",表示父进程同意停止发送差异数据。
如果等待超时或者读取到的字节不是 "!",则跳转到 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...");*/
serverLog(LL_NOTICE,"Parent agreed to stop sending diffs. Finalizing AOF...");
/* Read the final diff if any. */
/*读取最终的差异数据。这是为了确保子进程获取到了父进程在通知停止差异数据之前发送的所有数据。*/
aofReadDiffFromParent();
/* Write the received diff to the file. */
serverLog(LL_NOTICE,
"Concatenating %.2f MB of AOF diff received from parent.",
(double) sdslen(server.aof_child_diff) / (1024*1024));
/*对象将从父进程接收到的差异数据写入AOF文件。rioWrite返回0表示写入失败,跳转到错误处理标签 werr。*/
if (rioWrite(&aof,server.aof_child_diff,sdslen(server.aof_child_diff)) == 0)
goto werr;
/* Make sure data will not remain on the OS's output buffers */
/*
刷新和同步文件系统缓冲区:
fflush(fp): 刷新文件流,确保数据写入文件。
fsync(fileno(fp)): 将文件的数据和元数据(包括文件大小、权限等)同步到存储设备中。这样可以确保数据不仅在文件系统缓冲中,而且已经写入磁盘。
如果刷新或同步失败,跳转到 werr 处理错误。
*/
if (fflush(fp) == EOF) goto werr;
if (fsync(fileno(fp)) == -1) goto werr;
/*
关闭文件:
fclose(fp): 关闭AOF文件。如果关闭失败,跳转到 werr 处理错误。
*/
if (fclose(fp) == EOF) goto werr;
/* Use RENAME to make sure the DB file is changed atomically only
* if the generate DB file is ok. */
/*使用RENAME操作将临时AOF文件重命名为正式的AOF文件名。这是为了确保在将新AOF文件移动到目标位置之前,不
会影响到旧AOF文件。如果重命名失败,记录警告日志,删除临时文件,并停止保存操作。*/
if (rename(tmpfile,filename) == -1) {
serverLog(LL_WARNING,"Error moving temp append only file on the final destination: %s", strerror(errno));
unlink(tmpfile);
stopSaving(0);
return C_ERR;
}
serverLog(LL_NOTICE,"SYNC append only file rewrite performed");
/*
停止保存操作:
stopSaving(1): 停止保存操作,参数为1表示保存成功。
*/
stopSaving(1);
return C_OK;
werr:
serverLog(LL_WARNING,"Write error writing append only file on disk: %s", strerror(errno));
fclose(fp);
unlink(tmpfile);
stopSaving(0);
return C_ERR;
}
aof重写的全过程:
if (server.aof_use_rdb_preamble) {
int error;
/*表示启用了 AOF 文件使用 RDB 文件的预处理阶段,即使用 RDB 文件的内容作为 AOF 文件的开头。
此时,调用 rdbSaveRio 函数执行 RDB 文件的保存操作,并将保存的内容写入给定的 rio 结构。
如果保存操作失败,将错误码设置到 errno,并跳转到错误处理标签 werr。*/
if (rdbSaveRio(&aof,&error,RDBFLAGS_AOF_PREAMBLE,NULL) == C_ERR) {
errno = error;
goto werr;
}
} else {
/*
表示不启用 AOF 文件使用 RDB 文件的预处理阶段,即执行 AOF 文件的预处理阶段。
此时,调用 rewriteAppendOnlyFileRio 函数执行 AOF 文件的预处理操作,
并将预处理的内容写入给定的 rio 结构。如果预处理操作失败,同样跳转到错误处理标签 werr。
*/
if (rewriteAppendOnlyFileRio(&aof) == C_ERR) goto werr;
}
根据要求将原始数据库中的数据通过rfb存储到aof文件中,或者通过rewriteAppendOnlyFileRio将数据库的各种数据以aof文件特有的形式存储。
while(mstime()-start < 1000 && nodata < 20) {
/*
aeWait: 这是 Redis 的事件循环中的一个函数,用于等待文件描述符变为可读。server.aof_pipe_read_data_from_parent 是与父进程通信的管道的读取端。如果它变得可读,说明父进程有新的数据发送过来。
如果 aeWait 返回的值小于等于 0,表示在等待期间没有可读事件发生,即没有新数据。在这种情况下,增加 nodata 计数器,表示一次没有新数据的尝试。
如果有新数据可读,则调用 aofReadDiffFromParent 函数,将新数据读取并追加到当前子进程的缓冲区。
*/
if (aeWait(server.aof_pipe_read_data_from_parent, AE_READABLE, 1) <= 0)
{
nodata++;
continue;
}
nodata = 0; /* Start counting from zero, we stop on N *contiguous*
timeouts. */
aofReadDiffFromParent();
}
通过aofReadDiffFromParent()函数和定时器来定时读取redis运行时aof产生的数据.
void aofChildPipeReadable(aeEventLoop *el, int fd, void *privdata, int mask) {
char byte;
UNUSED(el);
UNUSED(privdata);
UNUSED(mask);
/*使用 read 函数从管道中读取一个字符。*/
if (read(fd,&byte,1) == 1 && byte == '!') {
/*
如果读取的字符是 '!',表示子进程要求父进程停止发送 AOF 缓冲区的差异数据。
在这种情况下,设置全局变量 server.aof_stop_sending_diff 为 1,表示停止发送。
向子进程发送一个 '!' 作为确认。
*/
serverLog(LL_NOTICE,"AOF rewrite child asks to stop sending diffs.");
server.aof_stop_sending_diff = 1;
if (write(server.aof_pipe_write_ack_to_child,"!",1) != 1) {
/* If we can't send the ack, inform the user, but don't try again
* since in the other side the children will use a timeout if the
* kernel can't buffer our write, or, the children was
* terminated. */
/*记录日志,通知用户 AOF 重写子进程请求停止发送差异数据。*/
serverLog(LL_WARNING,"Can't send ACK to AOF child: %s",
strerror(errno));
}
}
/* Remove the handler since this can be called only one time during a
* rewrite. */
/*由于这个处理器只需要在 AOF 重写期间调用一次,因此在处理完毕后通过 aeDeleteFileEvent 删除这个事件处理器。*/
aeDeleteFileEvent(server.el,server.aof_pipe_read_ack_from_child,AE_READABLE);
}
aof重写时其父子进程通过管道进行通信。