当前位置: 首页 > news >正文

Redis AOF持久化和ReWrite

前言

Redis 的 RDB 持久化机制简单直接,把某一时刻的所有键值对以二进制的方式写入到磁盘,特点是恢复速度快,尤其适合数据备份、主从复制场景。但如果你的目的是要保证数据可靠性,RDB 就不太适合了,因为 RDB 持久化不宜频繁触发,如果 Redis 触发 RDB 后又有新的数据写入,且还没来得及触发下一次 RDB 就宕机了,中间的数据就会丢失。
在这种场景下,我们就急需一种增量备份的方式,只记录上一次 RDB 到现在为止所有的变更记录就好了,相较于全量备份,增量备份的数据量就小得多了。所以,Redis 还提供了 AOF 持久化机制。

特性RDBAOF
文件格式二进制文本
内存效率较低
恢复速度
文件大小
运行效率

Redis 会把服务端执行的所有写命令,以 RESP 协议的方式追加到 AOF 日志文件中,数据恢复时只要读取 AOF 文件,重放所有的日志即可。
image.png

开启AOF

开启 AOF 你需要关心下面几个参数:

appendonly yes #开启AOF
appendfilename "appendonly.aof" #AOF文件名#AOF文件刷盘策略
appendfsync always
# appendfsync everysec
# appendfsync no#AOF触发机制
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb#是否开启RDB前导 混合持久化
aof-use-rdb-preamble yes

appendfsync 配置 AOF 文件刷盘策略:

  • always:每个写命令执行完都立即同步到磁盘,可靠性高、效率低
  • everysec:AOF 日志先写入内存缓冲区,再由定时任务每秒刷一次盘
  • no:AOF 日志只写入操作系统文件缓存,由操作系统决定刷盘时机

建议设置为 everysec,兼顾性能和可靠性,最多丢失一秒的数据;always 虽然足够可靠,但是会严重拉低性能;no 会让 AOF 文件刷盘的时机脱离 Redis 的控制,并不推荐。

开启 AOF 不代表就可以高枕无忧了,因为 AOF 是日志追加的形式,意味着随着不断运行,AOF 日志会越来越大。AOF 日志文件膨胀会带来两个问题:

  • AOF 文件大小超过文件系统的限制会发生错误
  • AOF 文件太大会影响数据恢复效率

所以,Redis 会对 AOF 文件重写,你可以通过bgrewriteaof命令手动触发重写,也可以配置当条件满足时自动触发重写。

  • auto-aof-rewrite-percentage:AOF 文件的增长比例,默认增长为原来的一倍大小就开始重写
  • auto-aof-rewrite-min-size:允许 AOF 重写的文件最小值

为什么重写可以缩小 AOF 文件呢?因为可以把多条命令合并成一条命令,AOF 只需要记录 Key 最新的 Value 即可,而不用记录修改的历史记录。

最后是aof-use-rdb-preamble参数,它是 Redis4 才开始支持的混合持久化机制,开启后 AOF 重写时将会在 AOF 文件的前半部分先写入全量的 RDB 数据,再把增量 AOF 日志追加在后半部分,同时兼顾了性能和可靠性,建议开启。

AOF机制

先测试下,开启 AOF 后,我们写入数据:

127.0.0.1:6379> set name Jackson
OK
127.0.0.1:6379> lpush names Lisa
(integer) 1
127.0.0.1:6379> lpush names Tom
(integer) 2

因为 AOF 文件是文本形式的,所以我们可以直接查看。AOF 日志是按照 RESP 协议写入的,所以你需要先了解一下 RESP 协议,这个协议很简单。如下:

  • *代表数组,后面跟着数组长度
  • $代表字符串,后面跟着长度

我们来解读一下这段日志,首先是SELECT 0代表后续操作的是 0 号数据库,紧接着就是各个命令和对应的参数了。

*2
$6
SELECT
$1
0
*3
$3
set
$4
name
$7
Jackson
*3
$5
lpush
$5
names
$4
Lisa
*3
$5
lpush
$5
names
$3
Tom

注意:读命令不会对数据有影响,是不会记录到 AOF 文件的。

此时我们执行bgrewriteaof命令重写 AOF 文件,再次查看发现 AOF 文件确实变小了,两次lpush合并为一次RPUSH了。

*2
$6
SELECT
$1
0
*3
$3
SET
$4
name
$7
Jackson
*4
$5
RPUSH
$5
names
$3
Tom
$4
Lisa

如果开启了混合持久化,我们再执行bgrewriteaof命令重写 AOF 文件,会发现 AOF 文件不可读了,因为此时里面的内容是二进制的 RDB 数据。

REDIS0009�	redis-ver6.2.13�
redis-bits�@�ctime·�"eused-mem�`�aof-preamble���nameJacksonnamesTomLisa��=O{�v�*2

再执行写命令:

127.0.0.1:6379> del name
(integer) 1

再查看 AOF 文件会发现,前半段是不可读的 RDB 数据,后半段是可读的 AOF 日志,这就是混合持久化。

REDIS0009�	redis-ver6.2.13�
redis-bits�@�ctime·�"eused-mem�`�aof-preamble���nameJacksonnamesTomLisa��=O{�v�*2
$6
SELECT
$1
0
*2
$3
del
$4
name

AOF流程

Redis 服务端在执行完写命令后,还会有一套命令传播机制,用于追加 AOF 日志和主从数据复制。命令被传播到 AOF 后会,Redis 会根据命令生成对应的 RESP 协议的文本字符串,然后追加到缓冲区,最后根据刷盘策略来把缓冲区的日志刷新到磁盘。
image.png
源码中,Redis 在处理命令时,如果开启了 AOF 或有主从数据复制,命令就需要传播,此时会调用propagate()

if (propagate_flags != PROPAGATE_NONE && !(c->cmd->flags & CMD_MODULE))propagate(c->cmd,c->db->id,c->argv,c->argc,propagate_flags);

传播命令时会进一步调用feedAppendOnlyFile()来追加 AOF 日志:

void propagate(struct redisCommand *cmd, int dbid, robj **argv, int argc,int flags)
{// AOF开启 要追加AOF日志if (server.aof_state != AOF_OFF && flags & PROPAGATE_AOF)feedAppendOnlyFile(cmd,dbid,argv,argc);if (flags & PROPAGATE_REPL)replicationFeedSlaves(server.slaves,dbid,argv,argc);
}

有了写命令,怎么生成 AOF 日志呢?是把命令直接记录下来吗?
当然不是,Redis 会把写命令按照 RESP 协议的方式序列化成文本再记录。其次,记录 AOF 日志是一个非常频繁的操作,如果每次都直接写文件,会严重影响 Redis 性能。所以,Redis 会先写到 sds 缓冲区里,变量是aof_buf

void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int argc) {// 命令用sds承载sds buf = sdsempty();// 命令操作的数据库和上次数据库不同,此时要先写入一个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日志if (cmd->proc == expireCommand || cmd->proc == pexpireCommand ||cmd->proc == expireatCommand) {/* Translate EXPIRE/PEXPIRE/EXPIREAT into PEXPIREAT */buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);}......// 追加到AOF缓冲区if (server.aof_state == AOF_ON)server.aof_buf = sdscatlen(server.aof_buf,buf,sdslen(buf));// 如果有AOF子进程在重写,还要把增量部分追加到AOF重写缓冲区if (server.child_type == CHILD_TYPE_AOF)aofRewriteBufferAppend((unsigned char*)buf,sdslen(buf));// 释放sdsfree(buf);
}

光写到内存缓冲区是不安全的,如果宕机数据就丢了。所以接下来还会调用flushAppendOnlyFile()来根据同步策略把缓冲区的日志同步到磁盘。

void flushAppendOnlyFile(int force) {ssize_t nwritten;int sync_in_progress = 0;mstime_t latency;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;}}// 每秒刷盘 判断是否在处理中if (server.aof_fsync == AOF_FSYNC_EVERYSEC)sync_in_progress = aofFsyncInProgress();// 每秒刷盘 且不强制刷盘if (server.aof_fsync == AOF_FSYNC_EVERYSEC && !force) {if (sync_in_progress) {if (server.aof_flush_postponed_start == 0) {// 记录延迟刷盘的开始时间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.");}}// AOF缓冲区写入到文件latencyStartMonitor(latency);nwritten = aofWrite(server.aof_fd,server.aof_buf,sdslen(server.aof_buf));latencyEndMonitor(latency);// 写入字节数不等于缓冲区大小 写入异常的处理if (nwritten != (ssize_t)sdslen(server.aof_buf)) {......} 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;// 缓冲区小于4K就重用,否则释放申请新的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: // 尝试刷盘if (server.aof_no_fsync_on_rewrite && hasActiveChildProcess())return;if (server.aof_fsync == AOF_FSYNC_ALWAYS) {// 每次都刷盘// 刷盘if (redis_fsync(server.aof_fd) == -1) {serverLog(LL_WARNING,"Can't persist AOF for fsync error when the ""AOF fsync policy is 'always': %s. Exiting...", strerror(errno));exit(1);}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;}
}

AOF重写

随着运行,AOF 文件会不断膨胀,AOF 文件过大会影响数据恢复效率,所以 Redis 会不时的重写 AOF 文件。
AOF 重写触发的时机主要有两种:

  • 手动执行bgrewriteaof命令
  • AOF 文件增长超过阈值,由定时任务自动触发

image.png
AOF 重写不关心键值对的历史修改记录,只需要记录最新的值即可,所以重写后它几乎总是会更小。但是 AOF 重写会面临两个问题:

  • AOF 重写要遍历数据库,且需要写磁盘文件,会导致阻塞
  • 如果无法容忍阻塞,异步写的话,期间发生的数据变更怎么办?

Redis 给出的解决方案是:一次拷贝、两处日志。

  • 一次拷贝:fork 子进程时,需要拷贝父进程的页表
  • 日志一:AOF 日志缓冲区正常写,哪怕 AOF 重写失败也不影响现有日志文件
  • 日志二:再多写一份 AOF 重写缓冲区日志,用来发送给子进程追加到重写后的 AOF 文件

Redis 在 AOF 重写时,首先会创建三个管道,用于和子进程发送增量日志和ACK;然后 fork 子进程,子进程开始遍历数据库生成新的 AOF 文件;同时,主进程在执行写命令时,会把 AOF 日志同时追加到 AOF 缓冲区和 AOF 重写缓冲区,再通过管道把增量日志发送给子进程;子进程重写好 AOF 日志后再把接收到的父进程发送过来的增量日志也一并追加到新的 AOF 文件,子进程退出;父进程把最后剩余的增量日志追加到新的 AOF 文件,然后替换旧文件,AOF 重写完成。
image.png

为什么还要用到 Linux 管道???
如果不用管道,子进程在 AOF 重写期间,父进程会积压很多增量日志,子进程重写完成后,父进程要把这些增量日志追加到新的 AOF 文件后,才算重写完成,这势必会发生阻塞。
有了管道,父进程可以实时的把增量日志发送给子进程,由子进程把增量日志追加到重写后的 AOF 文件里,这样就不会阻塞父进程。最终父进程只需要把最后一点点剩余的增量日志追加到新的 AOF 文件即可,这个阻塞时间就可以忽略不计了,归根结底是为了避免阻塞父进程。

一起看下 AOF 重写的源码吧,bgrewriteaofCommand()方法用来处理bgrewriteaof命令:

  • 如果 AOF 已经在重写,这里直接跳过
  • 如果在执行 RDB 持久化,AOF 也要等待稍后被调度执行
void bgrewriteaofCommand(client *c) {if (server.child_type == CHILD_TYPE_AOF) {// AOF重写子进程处理中addReplyError(c,"Background append only file rewriting already in progress");} else if (hasActiveChildProcess()) {// AOF和RDB子进程不能同时进行,此时把AOF重写任务设为等待调度执行,由定时任务触发server.aof_rewrite_scheduled = 1;addReplyStatus(c,"Background append only file rewriting scheduled");} else if (rewriteAppendOnlyFileBackground() == C_OK) {// AOF子进程开始启动addReplyStatus(c,"Background append only file rewriting started");}
}

rewriteAppendOnlyFileBackground()会在后台 fork 一个子进程来处理 AOF 重写:

  • 创建管道
  • fork AOF 子进程
  • 根据进程 ID 生成临时文件
  • AOF重写到临时文件
int rewriteAppendOnlyFileBackground(void) {pid_t childpid;// RDB子进程在工作,返回错误if (hasActiveChildProcess()) return C_ERR;// 创建三个管道 用于父子进程通信if (aofCreatePipes() != C_OK) return C_ERR;// fork子进程if ((childpid = redisFork(CHILD_TYPE_AOF)) == 0) {// 子进程char tmpfile[256];redisSetProcTitle("redis-aof-rewrite");redisSetCpuAffinity(server.aof_rewrite_cpulist);// 进程ID生成临时文件snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof", (int) getpid());// AOF重写到临时文件if (rewriteAppendOnlyFile(tmpfile) == C_OK) {sendChildCowInfo(CHILD_INFO_TYPE_AOF_COW_SIZE, "AOF rewrite");exitFromChild(0);} else {exitFromChild(1);}} else {// 父进程if (childpid == -1) {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 %ld",(long) childpid);server.aof_rewrite_scheduled = 0;server.aof_rewrite_time_start = time(NULL);server.aof_selected_db = -1;replicationScriptCacheFlush();return C_OK;}return C_OK; /* unreached */
}

父进程首先会调用aofCreatePipes()创建三个管道,用于后续和子进程传输数据:

  • pipe1:父给子传输增量日志的管道
  • pipe2:子给父发送 ACK 的管道,让父进程停止发送增量日志,自己已经重写完要退出了
  • pipe3:父给子发送 ACK 的管道
int aofCreatePipes(void) {int fds[6] = {-1, -1, -1, -1, -1, -1};int j;// 创建三个管道if (pipe(fds) == -1) goto error; // 父子进程数据传输管道 传输重写期间的增量AOF日志if (pipe(fds+2) == -1) goto error; // 子父进程ACK管道if (pipe(fds+4) == -1) goto error; // 父子进程ACK管道// 父子进程的数据传输管道设为非阻塞if (anetNonBlock(NULL,fds[0]) != ANET_OK) goto error;if (anetNonBlock(NULL,fds[1]) != ANET_OK) goto error;// 注册可读事件监听,子进程重写完成会发送ACK,父进程停止发送增量AOF日志if (aeCreateFileEvent(server.el, fds[2], AE_READABLE, aofChildPipeReadable, NULL) == AE_ERR) goto error;server.aof_pipe_write_data_to_child = fds[1];server.aof_pipe_read_data_from_parent = fds[0];server.aof_pipe_write_ack_to_parent = fds[3];server.aof_pipe_read_ack_from_child = fds[2];server.aof_pipe_write_ack_to_child = fds[5];server.aof_pipe_read_ack_from_parent = fds[4];server.aof_stop_sending_diff = 0;return C_OK;
error:serverLog(LL_WARNING,"Error opening /setting AOF rewrite IPC pipes: %s",strerror(errno));for (j = 0; j < 6; j++) if(fds[j] != -1) close(fds[j]);return C_ERR;
}

管道创建完开始重写,rewriteAppendOnlyFile()会先打开临时文件,然后判断是否开启混合持久化模式来选择是写 RDB 数据还是 AOF 日志:

if (server.aof_use_rdb_preamble) {int error;// RDB二进制全量写入if (rdbSaveRio(&aof,&error,RDBFLAGS_AOF_PREAMBLE,NULL) == C_ERR) {errno = error;goto werr;}
} else {// 仅使用AOF日志if (rewriteAppendOnlyFileRio(&aof) == C_ERR) goto werr;
}

rewriteAppendOnlyFileRio()是仅写 AOF 日志,和 RDB 类似也是遍历数据库,再遍历键值对,生成对应的 AOF 日志写入文件。

int rewriteAppendOnlyFileRio(rio *aof) {dictIterator *di = NULL;dictEntry *de;size_t processed = 0;int j;long key_count = 0;long long updated_time = 0;// 遍历数据库for (j = 0; j < server.dbnum; j++) {// 选择数据库的命令char selectcmd[] = "*2\r\n$6\r\nSELECT\r\n";redisDb *db = server.db+j;dict *d = db->dict;if (dictSize(d) == 0) continue; // 数据库空的,则跳过// 获取哈希表迭代器di = dictGetSafeIterator(d);// 写入选择数据库的命令 例如:*2\r\n$6\r\nSELECT\r\n$1\r\n0\r\nif (rioWrite(aof,selectcmd,sizeof(selectcmd)-1) == 0) goto werr;if (rioWriteBulkLongLong(aof,j) == 0) goto werr;// 遍历全局哈希表while((de = dictNext(di)) != NULL) {sds keystr;robj key, *o;long long expiretime;// 获取Key、Valuekeystr = dictGetKey(de);o = dictGetVal(de);initStaticStringObject(key,keystr);// 获取过期时间expiretime = getExpire(db,&key);// 根据不同的数据类型生成对应的AOF日志.....// 保存过期时间if (expiretime != -1) {char cmd[]="*3\r\n$9\r\nPEXPIREAT\r\n";if (rioWrite(aof,cmd,sizeof(cmd)-1) == 0) goto werr;if (rioWriteBulkObject(aof,&key) == 0) goto werr;if (rioWriteBulkLongLong(aof,expiretime) == 0) goto werr;}// AOF重写文件每写入10KB就从尝试管道中读取父进程发送过来的增量AOF日志if (aof->processed_bytes > processed+AOF_READ_DIFF_INTERVAL_BYTES) {processed = aof->processed_bytes;aofReadDiffFromParent();}}dictReleaseIterator(di);di = NULL;}return C_OK;
werr:if (di) dictReleaseIterator(di);return C_ERR;
}

子进程在 AOF 重写期间会不时地读取父进程通过管道发送过来的增量日志,在 AOF 重写完以后,也会再读取一下增量日志,尽可能地让自己重写的 AOF 文件更全面一些,方法是aofReadDiffFromParent(),读取到的增量日志会先缓存到server.aof_child_diff

ssize_t aofReadDiffFromParent(void) {char buf[65536]; /* Default pipe buffer size on most Linux systems. */ssize_t nread, total = 0;while ((nread =read(server.aof_pipe_read_data_from_parent,buf,sizeof(buf))) > 0) {server.aof_child_diff = sdscatlen(server.aof_child_diff,buf,nread);total += nread;}return total;
}

子进程重写完以后,把 AOF 文件刷新到磁盘就可以退出了。父进程的定时任务监听到子进程退出后,会触发backgroundRewriteDoneHandler()方法对 AOF 重写做一个收尾的工作

  • 打开重写后的临时文件
  • 把最后还剩余的增量日志写入到临时文件
  • 替换旧的 AOF 文件
  • 清理管道等资源
void backgroundRewriteDoneHandler(int exitcode, int bysignal) {// 打开子进程重写后的临时文件snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof",(int)server.child_pid);// 把aof_rewrite_buf_blocks的剩余数据写入AOF文件if (aofRewriteBufferWrite(newfd) == -1) {serverLog(LL_WARNING,"Error trying to flush the parent diff to the rewritten AOF: %s", strerror(errno));close(newfd);goto cleanup;}......// 替换AOF文件rename(tmpfile,server.aof_filename);......
}

至此,AOF 重写就算彻底结束了。

现在还有一个问题,父进程把 AOF 重写期间的增量日志写在哪里呢?
父进程写完 AOF 日志缓冲区后,会继续判断当前是否有子进程在重写 AOF,如果有就把增量日志再写一份到 AOF 重写缓冲区:

void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int argc) {......// 追加到AOF缓冲区if (server.aof_state == AOF_ON)server.aof_buf = sdscatlen(server.aof_buf,buf,sdslen(buf));// 如果有AOF子进程在重写,还要把增量部分追加到AOF重写缓冲区if (server.child_type == CHILD_TYPE_AOF)aofRewriteBufferAppend((unsigned char*)buf,sdslen(buf));
}

aofRewriteBufferAppend()方法会把增量日志写入到 AOF 重写缓冲区,重写缓冲区是由若干个aofrwblock串联成的一条链表,单个 block 容量是 10MB。

typedef struct aofrwblock {unsigned long used, free; // 已使用空间、空闲空间char buf[AOF_RW_BUF_BLOCK_SIZE]; // 缓冲区 默认10MB
} aofrwblock;

Redis 会先判断当前 block 是否能容纳增量日志,如果空间不够会申请新的 block 再写入。如果重写期间有大量写入,重写缓冲区会变得很大,占用大量内存。

void aofRewriteBufferAppend(unsigned char *s, unsigned long len) {// AOF重写缓冲区由若干个aofrwblock组成,单个block默认10MBlistNode *ln = listLast(server.aof_rewrite_buf_blocks);aofrwblock *block = ln ? ln->value : NULL;while(len) {if (block) {unsigned long thislen = (block->free < len) ? block->free : len;if (thislen) {// 当前已经分配了block,且block可以容纳增量AOF日志,直接写入即可memcpy(block->buf+block->used, s, thislen);block->used += thislen;block->free -= thislen;s += thislen;len -= thislen;}}if (len) { // 还没有分配block,或block容量不够,需要分配新的int numblocks;// 分配blockblock = zmalloc(sizeof(*block));block->free = AOF_RW_BUF_BLOCK_SIZE;block->used = 0;// block加入链表listAddNodeTail(server.aof_rewrite_buf_blocks,block);// block数量超过10或100的倍数时记录日志numblocks = listLength(server.aof_rewrite_buf_blocks);if (((numblocks+1) % 10) == 0) {int level = ((numblocks+1) % 100) == 0 ? LL_WARNING :LL_NOTICE;serverLog(level,"Background AOF buffer size: %lu MB",aofRewriteBufferSize()/(1024*1024));}}}if (!server.aof_stop_sending_diff &&aeGetFileEvents(server.el,server.aof_pipe_write_data_to_child) == 0){aeCreateFileEvent(server.el, server.aof_pipe_write_data_to_child,AE_WRITABLE, aofChildWriteDiffData, NULL);}
}

注意最后几行代码,Redis 给管道 fd 注册了可写事件。因为父进程光写入重写缓冲区还不够,还要把重写缓冲区里的增量日志发给子进程,让子进程尽可能多的写入日志,自己最后就能少写一点了,监听方法是aofChildWriteDiffData(),父进程发送后,子进程就可以读到了。

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;if (server.aof_stop_sending_diff || !block) {aeDeleteFileEvent(server.el,server.aof_pipe_write_data_to_child,AE_WRITABLE);return;}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;}if (block->used == 0) listDelNode(server.aof_rewrite_buf_blocks,ln);}
}

尾巴

AOF 主要解决 Redis 数据持久化的实时性问题,现在已经成为了主流的持久化方式。它会把写命令生成 RESP 协议文本的方式追加到 AOF 文件中,为了避免 AOF 文件过度膨胀,Redis 会按照一定的策略对 AOF 文件做重写。重写时为了避免阻塞主线程,Redis 会 fork 子进程处理,同时主进程会记录下重写期间的增量日志,通过管道发送给子进程,让子进程尽可能多的记录日志,子进程重写完毕后会通过管道发送 ACK 要求父进程停止发送增量日志,子进程退出,父进程做最后的收尾工作,把最后剩余的一点增量日志追加到新的 AOF 文件后并完成替换,整个 AOF 重写就结束了。

相关文章:

Redis AOF持久化和ReWrite

前言 Redis 的 RDB 持久化机制简单直接&#xff0c;把某一时刻的所有键值对以二进制的方式写入到磁盘&#xff0c;特点是恢复速度快&#xff0c;尤其适合数据备份、主从复制场景。但如果你的目的是要保证数据可靠性&#xff0c;RDB 就不太适合了&#xff0c;因为 RDB 持久化不…...

Flink学习之旅:(一)Flink部署安装

1.本地搭建 1.1.下载Flink 进入Flink官网&#xff0c;点击Downloads 往下滑动就可以看到 Flink 的所有版本了&#xff0c;看自己需要什么版本点击下载即可。 1.2.上传解压 上传至服务器&#xff0c;进行解压 tar -zxvf flink-1.17.1-bin-scala_2.12.tgz -C ../module/ 1.3.启…...

Go语言入门心法(六): HTTP面向客户端|服务端编程

Go语言入门心法(一): 基础语法 Go语言入门心法(二): 结构体 Go语言入门心法(三): 接口 Go语言入门心法(四): 异常体系 Go语言入门心法(五): 函数 一:go语言面向web编程认知 Go语言的最大优势在于并发与性能,其性能可以媲美C和C,并发在网络编程中更是至关重要 使用http发送请…...

为什么非const静态成员变量一定要在类外定义

当我们如下声明了一个类&#xff1a; class A{public:static int sti_data;// 这个语句在c11前不能通过编译&#xff0c;在c11的新标准下&#xff0c;已经能够在声明一个普通变量是就对其进行初始化。int a 10&#xff1b;static const int b 1;//...其他member };// 在类外…...

千兆光模块和万兆光模块的区别?

在网络通信领域&#xff0c;千兆光模块和万兆光模块是最为常见且广泛应用的两种光模块。不同之处在于传输速率、封装、传输距离、功耗、发射光功率、接收光功率和应用场景等。 千兆光模块的传输速率为1 Gbps&#xff0c;万兆光模块的传输速率为10 Gbps&#xff0c;这意味着万…...

中断:Zynq Uart中断的流程和例程~UG585的CH.19

Zynq里的uart UART 控制器是全双工异步接收器和发送器&#xff0c;支持多种可编程波特率和 I/O 信号格式。该控制器可以适应自动奇偶校验生成和多主机检测模式。 UART 操作由配置和模式寄存器控制。使用状态、中断状态和调制解调器状态寄存器读取 FIFO、调制解调器信号…...

计算机补码能够减法转加法的原因

...

软件工程与计算总结(十九)软件测试

目录 ​编辑 一.引言 1.验证与确认 2.目标 3.测试用例 4.桩与驱动 5.缺陷、错误与失败 二.测试层次 1.测试层次的划分 2.单元测试 3.集成测试 4.系统测试 三.测试技术 1.测试用例的选择 2.随机测试 3.基于规格的技术&#xff08;黑盒测试&#xff09; 4.基于代…...

Tomcat设置IP黑名单和白名单server.xml

方式一&#xff1a; -- 只允许192.168.1.2和192.168.2.3 <Context path"" docBase"xxxAdmin" debug"0" reloadable"true" ><Valve className"org.apache.catalina.valves.RemoteAddrValve" allow"192.168.1.…...

【AI视野·今日NLP 自然语言处理论文速览 第五十五期】Mon, 16 Oct 2023

AI视野今日CS.NLP 自然语言处理论文速览 Mon, 16 Oct 2023 Totally 53 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Computation and Language Papers PromptRE: Weakly-Supervised Document-Level Relation Extraction via Prompting-Based Data Programming Au…...

k8s crd设置额外header

可以通过设置crd.spec.additionalPrinterColumns来实现&#xff1a; apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata:name: crontabs.stable.example.com spec:group: stable.example.comscope: Namespacednames:plural: crontabssingular: cr…...

电容笔好还是触屏笔好?便宜又好用的电容笔推荐

目前有哪些电容笔值得买&#xff1f;相比于之前的电容笔&#xff0c;现在的电容笔增加了很多新的特性功能&#xff0c;例如防误触、避免手指不小心触碰屏幕造成书写错误、笔画粗细可以自由调整等。苹果最初的Pencil现在售价一直高居不下。所以&#xff0c;如果你没有过多的预算…...

列表作为条件查询的参数

<if test"secucodeList ! null and secucodeList.size() > 0">...

elementui中el-select和el-tree实现下拉树形多选功能

实现效果如下&#xff1a; 代码如下&#xff1a; html中 <el-col :lg"12"><el-form-item label"可用单位" prop"useOrgListTemp"><div class"departAll"><el-selectref"selectTree"v-model"valu…...

手机怎么监控电脑?

随着企业对电脑监控需求的增加&#xff0c;越来越多的管理者意识到使用电脑监控电脑的不便性&#xff0c;一旦外出就无法实时查看监控。其实可以用手机实现监控电脑的需求&#xff0c;只需在被监控端安装电脑监控软件后&#xff0c;将电脑设备和员工信息进行绑定&#xff0c;使…...

职场题:有一件特别紧急的事,群众要办理,且联系不上领导,你怎么办?(2)

接1所写 如果无法联系上领导且有一项特别紧急的事情需要办理&#xff0c;以下是进一步的建议&#xff1a; 11. 尝试其他沟通渠道&#xff1a;除了直接联系领导外&#xff0c;尝试通过其他沟通渠道与领导取得联系。这可能包括电子邮件、即时通讯工具或其他内部通信平台。确保详…...

《深入理解java虚拟机 第三版》学习笔记一

第 2 章 Java 内存区域与内存溢出异常 2.2 运行时数据区域 Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域有各自的用途&#xff0c;以及创建和销毁的时间&#xff0c;有的区域随着虚拟机进程的启动而一直存在&#xff0c;有些…...

webGL编程指南 第三章 旋转三角形

我会持续更新关于wegl的编程指南中的代码。 当前的代码不会使用书中的缩写&#xff0c;每一步都是会展开写。希望能给后来学习的一些帮助 git代码地址 接着 上一节 接着做平移的转化。本案例是三角形的旋转 <!DOCTYPE html> <html lang"en"><head…...

网络安全是什么?一文认识网络安全

一、网络安全 1.概念 网络安全从其本质上讲就是网络上的信息安全&#xff0c;指网络系统的硬件、软件及数据受到保护。不遭受破坏、更改、泄露&#xff0c;系统可靠正常地运行&#xff0c;网络服务不中断。 &#xff08;1&#xff09;基本特征 网络安全根据其本质的界定&#…...

LeetCode 2897. 对数组执行操作使平方和最大【贪心,位运算,哈希表】2301

本文属于「征服LeetCode」系列文章之一&#xff0c;这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁&#xff0c;本系列将至少持续到刷完所有无锁题之日为止&#xff1b;由于LeetCode还在不断地创建新题&#xff0c;本系列的终止日期可能是永远。在这一系列刷题文章…...

linux加密安全和时间同步

sudo实现授权 添加 vim /etc/sudoers luo ALL(root) /usr/bin/mount /deb/cdrom /mnt/ test ALL(root:ALL) ALL 在所有主机上 提权为root用户&#xff0c; 可以执行所有命令 户"test"被授权以"root"用户身份在任意主机上执行任意命令 切换luo用户使用 su…...

在Go中处理异常

引言 程序遇到的错误大致分为两类:程序员预料到的错误和程序员没有预料到的错误。我们在前两篇关于[错误处理]的文章中介绍的error接口主要处理我们在编写Go程序时预期的错误。error接口甚至允许我们承认函数调用发生错误的罕见可能性&#xff0c;因此我们可以在这些情况下进行…...

rust 全局变量

文章目录 编译期初始化静态常量静态变量原子类型 运行期初始化lazy_staticBox::leak从函数中返回全局变量 标准库中的 OnceCell 编译期初始化 静态常量 const MAX_ID: usize usize::MAX / 2; fn main() {println!("用户ID允许的最大值是{}",MAX_ID); }关键字是co…...

使用Python的qrcode库生成二维码

使用Python的qrcode库生成二维码 此二维码直接跳转对应的网址。 1、首先安装qrcode包 pip install qrcode2、运行代码 import qrcode# 需要跳转的URL url "https://blog.csdn.net/weixin_45092662?typeblog" img qrcode.make(url) img.save("qrcode.png&…...

MSQL系列(四) Mysql实战-索引分析Explain命令详解

Mysql实战-索引分析Explain命令详解 前面我们讲解了索引的存储结构&#xff0c;我们知道了BTree的索引结构&#xff0c;也了解了索引最左侧匹配原则&#xff0c;到底最左侧匹配原则在我们的项目中有什么用&#xff1f;或者说有什么影响&#xff1f;今天我们来实战操作一下&…...

FPGA软件【紫光】

软件&#xff1a;编程软件。 注册账号需要用到企业邮箱 可以使用【企业微信】的邮箱 注册需要2~3天&#xff0c;会收到激活邮件 授权&#xff1a; 找到笔记本网卡的MAC&#xff0c; 软件授权选择ADS 提交申请后&#xff0c;需要2~3天等待邮件通知。 使用授权&#xff1a; 文…...

饲料化肥经营商城小程序的作用是什么

我国农牧业规模非常高&#xff0c;各种农作物和养殖物种类多&#xff0c;市场呈现大好趋势&#xff0c;随着近些年科学生产养殖逐渐深入到底层&#xff0c;专业的肥料及饲料是不少从业者需要的&#xff0c;无论城市还是农村都有不少经销店。 但在实际经营中&#xff0c;经营商…...

AI系统ChatGPT源码+详细搭建部署教程+支持GPT4.0+支持ai绘画(Midjourney)/支持OpenAI GPT全模型+国内AI全模型

一、AI创作系统 SparkAi创作系统是基于OpenAI很火的ChatGPT进行开发的Ai智能问答系统AI绘画系统&#xff0c;支持OpenAI GPT全模型国内AI全模型。本期针对源码系统整体测试下来非常完美&#xff0c;可以说SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。那么如何搭建部署…...

vue项目优雅降级,es6降为es5,适应低版本浏览器渲染

非vue项目 ECMAScript 6(ES6)的发展速度非常之快&#xff0c;但现代浏览器对ES6新特性支持度不高&#xff0c;所以要想在浏览器中直接使用ES6的新特性就得借助别的工具来实现。 Babel是一个广泛使用的转码器&#xff0c;babel可以将ES6代码完美地转换为ES5代码&#xff0c;所…...

运放供电设计

文章目录 运放供电设计如何产生负电压BUCK电路BOOST电路产生负电压FLYBUCK产生负电压 运放供电设计 注&#xff1a;使用0.1u跟10u并联 如何产生负电压 问题&#xff1a;电流小&#xff0c;使用并联方式改善&#xff0c;缺点价格贵&#xff0c;淘宝上买的都是假货ICL7662多是用…...

建设集团网站方案设计/在线推广

看Outlook的截图&#xff1a; 系统的报警邮件要是发成这样&#xff0c;只能在邮箱里面设置规则&#xff0c;直接永久删除了。。。 转载于:https://www.cnblogs.com/vwxyzh/archive/2011/11/06/2238023.html...

武汉汉阳建设局官方网站/温州seo排名公司

原文&#xff1a;http://coolketang.com/staticPhotoshop/5a98d43c17d009003595ee49.html 1. 本节课程将为您演示&#xff0c;[曝光度]命令的使用。依次点击[图像 > 调整 > 曝光度]命令&#xff0c;弹出[曝光度]窗口。 2. 3. [曝光度]是用来控制图片的色调强弱的工具。跟…...

web网站建设报价/微信公众号seo

基本概念所谓完美哈希函数。就是指没有冲突的哈希函数。即对随意的 key1 ! key2 有h(key1) ! h(key2)。设定义域为X&#xff0c;值域为Y, n|X|,m|Y|。那么肯定有m>n,假设对于不同的key1,key2属于X,有h(key1)!h(key2)&#xff0c;那么称h为完美哈希函数&#xff0c;当mn时&am…...

南宁公司做网站/百度竞价关键词

汽车市场瞬息万变汽车市场这些年发展真的很快&#xff0c;变化也非常的大。有些车子曾经是王者&#xff0c;转眼就变青铜。这个现象就好像非智能手机时代&#xff0c;诺基亚是全球最为强大的手机品牌&#xff0c;转眼智能手机全面发展&#xff0c;诺基亚没能跟上浪潮&#xff0…...

上海网站建设高端定制/网站关键词优化软件

TLK2711高速串行协议是一种基于点对点的单工协议&#xff0c;它以16 bit为一个基本的传输单位&#xff0c;数据被分成了高8位和低8位&#xff0c;因此每个控制字符都定义成了2 B(分别由D码和K码组成)&#xff0c;编码方式由2个控制信号TKMSB/RKMSB和TKLSB/RKLSB决定。协议的工作…...

手机网站用什么软件/网络营销最新案例

【实例简介】java 简单 购物车 JSP Servlet JAVAbeenjavaee 简单 购物车【实例截图】【核心代码】medicineStore2└── medicineStore2├── MedicineStore│ ├── src│ │ ├── goods│ │ │ ├── addGoodsServlet.java│ │ │ ├── AddToCart…...