我们都知道,redis的数据是存储在内存之中的,好处是读写快,坏处就是进程一旦停止所有数据都会丢失,所以为了避免这种问题的产生,redis提供了两种不同的缓存方案。其中之一就是AOF(Append Only File)日志。
Redis在每一次写命令执行成功之后,才会记录日志,这样存在两个好处。
第一个好处,执行失败的命令不会被记录下来。执行命令的过程是一定会进行命令解析的,执行失败的命令自然不需要重新解析。
第二个好处,记录日志毕竟要和硬盘进行交互,硬盘的读写速度是要远低于内存的,这种耗时操作不会阻塞到当前命令的执行。
但是同时也存在两个风险。
如果一个命令执行成功了,但是此时发生了宕机,导致日志记录失败,这样就会导致日志中保存的数据和真实数据不一致。
在高频执行命令时,每一次记录日志都会阻塞后续的命令的执行。
为了解决这些问题,Redis提供了三种不同的写入策略。
三种写入策略由配置项appendfsync指定,它总共有三种可选值:
Always。这种策略下是在每一次都使用同步写入文件的方式完成日志文件的记录,同步写入的好处就是可以第一时间得到是否写入成功的结果,缺点就是IO占据了大量的时间,会很慢。
EverySec。这种策略下会单独开辟一块内存空间,用于缓存需要写入的命令,这一步的操作只需要操作内存。然后每一秒钟会将此内存内的命令使用异步的方式一次性的追加到日志文件末尾。异步提高了效率的同时,一次性写入大量数据也会更好的应用硬盘带宽。
No。这种策略下并不会主动的将命令记录到日志文件中,而是将需要记录的命令拷贝到系统内核的内存中,具体何时完成实际的落盘,由操作系统决定。
使用Always策略可靠性最高,性能最低,No策略可靠性最低,性能最高。一般来讲,使用EverySec是比较均衡的。
在记录一些带超时的命令时,比如SETEX、PSETEX命令,会先将其拆分为两条命令:SET命令以及PEXPIREAT命令,之后再分别记录两条命令。
如果本身期望记录的就是EXPIRE、PEXPIRE、EXPIREAT命令,也会统一的转换为PEXPIREAT命令再记录。
不同版本实现有一定差异,但大体思路不变,相关实现在/src/aof.c
文件中的feedAppendOnlyFile
函数。
每一次重新启动都需要恢复宕机前的数据,AOF日志文件中就存储了redis实例第一次启动到宕机(或主动停机)时的完整操作记录。理论上,我们只需要把这些命令完整的执行一遍,就可以恢复之前的数据了。
但是实际上,有些命令是会失效的,比如多次SET同一个key,后执行的命令会覆盖掉之前的结果,或者是多条命令操作同一个ZSET,多次操作后,理论上我们只需要一条命令就可以生成一个完整的ZSET。即,有些命令是可以删掉的,是没必要执行的。
具体实现上的细节是,Redis会在后台任务(server.c
文件中的serverCron
函数)中判断是否需要对现在的AOF日志文件进行压缩:
// 不存在子进程 且 已安排aof重写 且 aof重写没有受到限制
if (!hasActiveChildProcess() &&
server.aof_rewrite_scheduled &&
!aofRewriteLimited()) // 实现在 aof.c 文件中
{
// 执行aof文件重写
rewriteAppendOnlyFileBackground();
}
准备必备文件和创建子进程的代码在rewriteAppendOnlyFileBackground
函数中,大致思路如下:
通过fork调用产生一个子进程,子进程会将原有的AOF文件重写在一个临时文件中,父进程同时创建一个新的文件来写入新的命令。
创建子进程前的大致流程:
子进程完成实际日志文件写入的函数是rewriteAppendOnlyFile
-> rewriteAppendOnlyFileRio
。
此函数做的事情就简单了,遍历所有的哈希桶,取出其中的节点,根据节点类型及其过期时间来生成不同的命令,写入到指定的文件中。
当子进程结束运行后,父进程会检查子进程是否正常退出,如果返回的是OK,那么就将父进程这段时间使用的临时文件的内容追加到子进程压缩后的文件中,并将之前的日志文件,以及父进程临时记录的日志文件都用异步的方式删除,并将合并后的文件作为新的AOF文件。
相关代码在checkChildrenDone
->backgroundRewriteDoneHandler
,大致流程如下:
评论