关于java:Redis源码剖析之AOF

8次阅读

共计 4423 个字符,预计需要花费 12 分钟才能阅读完成。

书接上回,上回咱们具体解说了 Redis 的 RDB 机制,RDB 解决了 redis 数据长久化一部分的问题,为什么说一部分?因为 rdb 是 redis 中某一时刻的快照,那么在这次快照后如果数据有新的变更,它是不会被长久化下来的,必须得等到下次 rdb 备份。然而,生成 rdb 是和耗费性能的,所以它就不适宜很频繁生成。Redis 为了补救这一有余提供了 AOF。

AOF 的全称是 AppendOnlyFile,源码在 aof.c。其实要害就是 Append(追加),外围原理很简略,就是如果执行完命令(set,del,expire……) 后,发现有数据变动,就将这次操作作为一条日志记录到 aof 文件里,如果有宕机就从新加载 aof 文件,重放所有的改变命令就能够复原数据了。只有日志被残缺刷到了磁盘上,数据就不会失落。

配置

AOF 的配置比较简单,只有如下几项。

appendonly no  # aof 开关,默认敞开
appendfilename "appendonly.aof"  # 保留的文件名,默认 appendonly.aof
# 有三种刷数据的策略
appendfsync always  # always 是只有有数据改变,就把数据刷到磁盘里,最平安但性能也最差
appendfsync everysec  # 每隔一秒钟刷一次数据,数据安全性和性能折中,这也是 redis 默认和举荐的配置。appendfsync no # 不被动刷,什么时候数据刷到磁盘里取决于操作系统,在大多数 Linux 零碎中每 30 秒提交一次,性能最好,但数据安全性最差。

源码

AOF 的触发

aof 如何实现,又是怎么被触发的,让咱们具体看下源码。
server.c 中的 void call(client *c, int flags) 是 redis 承受到 client 申请后处理申请的入口,其中会检测 Redis 中的数据有没有发生变化。如果有变动就会执行 propagate()函数。

    dirty = server.dirty;
    prev_err_count = server.stat_total_error_replies;
    updateCachedTime(0);
    elapsedStart(&call_timer);
    c->cmd->proc(c); // 执行命令
    const long duration = elapsedUs(call_timer);
    c->duration = duration;
    dirty = server.dirty-dirty;
    if (dirty < 0) dirty = 0; 
void propagate(struct redisCommand *cmd, int dbid, robj **argv, int argc,
               int flags)
{if (server.in_exec && !server.propagate_in_transaction)
        execCommandPropagateMulti(dbid);

    /* This needs to be unreachable since the dataset should be fixed during 
     * client pause, otherwise data may be lossed during a failover. */
    serverAssert(!(areClientsPaused() && !server.client_pause_in_transaction));

    if (server.aof_state != AOF_OFF && flags & PROPAGATE_AOF)
        feedAppendOnlyFile(cmd,dbid,argv,argc); // 如果 aof 开启了,就会向 aof 流传该命令。if (flags & PROPAGATE_REPL)
        replicationFeedSlaves(server.slaves,dbid,argv,argc);
}

propagate 函数的作用就是将带来数据改变的命令流传给 slave 和 AOF,这里咱们只关注 AOF,咱们来具体看下 feedAppendOnlyFile() 函数。

AOF 数据生成

void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int argc) {sds buf = sdsempty();
    /* The DB this command was targeting is not the same as the last command
     * we appended. To issue a SELECT command is needed. */
    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) {
        /* 把 EXPIRE/PEXPIRE/EXPIREAT 命令转化为 PEXPIREAT 命令 */
        buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);
    } else if (cmd->proc == setCommand && argc > 3) {
        robj *pxarg = NULL;
        /* When SET is used with EX/PX argument setGenericCommand propagates them with PX millisecond argument.
         * So since the command arguments are re-written there, we can rely here on the index of PX being 3. */
        if (!strcasecmp(argv[3]->ptr, "px")) {pxarg = argv[4];
        }
        /* 把 set 命令的 expired 所带的绝对工夫转化为相对工夫(ms). */
        if (pxarg) {robj *millisecond = getDecodedObject(pxarg);
            long long when = strtoll(millisecond->ptr,NULL,10);
            when += mstime();

            decrRefCount(millisecond);

            robj *newargs[5];
            newargs[0] = argv[0];
            newargs[1] = argv[1];
            newargs[2] = argv[2];
            newargs[3] = shared.pxat;
            newargs[4] = createStringObjectFromLongLong(when);
            buf = catAppendOnlyGenericCommand(buf,5,newargs);
            decrRefCount(newargs[4]);
        } else {buf = catAppendOnlyGenericCommand(buf,argc,argv);
        }
    } else {
        /* 其余的命令都不须要转化 */
        buf = catAppendOnlyGenericCommand(buf,argc,argv);
    }

    /* 追加到 AOF 缓冲区。在从新进入事件循环之前,数据将被刷新到磁盘上,因而在客户端在执行前就会失去回复。*/
    if (server.aof_state == AOF_ON)
        server.aof_buf = sdscatlen(server.aof_buf,buf,sdslen(buf));

    /* 如果后盾正在进行 AOF 重写,咱们心愿将子数据库和以后数据库之间的差别累积到缓冲区中,* 以便在子过程执行其工作时,咱们能够将这些差别追加到新的只追加文件中。*/
    if (server.child_type == CHILD_TYPE_AOF)
        aofRewriteBufferAppend((unsigned char*)buf,sdslen(buf));

    sdsfree(buf);
}

这里没有啥太简单的逻辑,就是将命令转化为 RESP 协定格局的字符串(RESP 协定后续会详解),而后追加到 server.aof_buf 中,这时候 AOF 数据还都在缓冲区中,并没有写入到磁盘中,那 buf 中的数据何时写入磁盘呢?

刷数据

刷数据的外围代码在 flushAppendOnlyFile() 中,flushAppendOnlyFile 在 serverCron、beforeSleep 和 prepareForShutdown 中都有被调用,它的作用就是将缓冲区的数据写到磁盘中,代码比拟长且简单,但大部分都是异样解决和性能监控,疏忽掉这部分后代码也比拟容易了解,这里就不再列举了,详见 aof.c。

RDB vs AOF

最初,咱们来比照下 RDB 和 AOF,他们各自都有啥优缺点,该如何选用。

RDB 的劣势

  1. RDB 是压缩的后紧凑数据格式,比拟很适宜备份,
  2. 同样的数据量下,rdb 的文件大小会很小,比拟适宜传输和数据恢复。
  3. RDB 对 Redis 的读写性能影响小,生成 RDB 的时 redis 主过程会 fork 出一个子过程,不会影响到主过程的读写。
  4. RDB 数据加载更快,复原起来更快。

RDB 的毛病

  1. RDB 是定期备份,如果备份前产生宕机,数据可能会失落。
  2. RDB 的生成依赖于 linux 的 fork,如果数据量比拟大的话,很影响服务器性能。

AOF 的劣势

  1. AOF 是持续性备份,能够尽可能保证数据不失落。
  2. Redis 太大时,Redis 能够在后盾主动重写 AOF。重写是齐全平安的,因为 Redis 持续追加到旧文件时,会生成一个全新的文件,其中蕴含创立以后数据集所需的起码操作集,一旦筹备好第二个文件,Redis 会切换这两个文件并开始追加到新的那一个。
  3. AOF 文件格式简略,易于解析。

    AOF 的毛病

  4. 对于同一数据集,AOF 文件大小通常大于等效的 RDB 文件。
  5. 如果应用 fsync 策略,AOF 可能比 RDB 慢。

RDB 和 AOF 该如何选

如果是要求极致的性能,但对数据恢复不敏感,二者能够都不要,如果是关注性能且关注数据可用性,但不要求数据完整性,能够选用 RDB。如果说十分关注数据完整性和宕机复原的能力,能够 RDB+AOF 同时开启。

参考资料

  1. Redis persistence demystified
  2. Redis Persistence

本文是 Redis 源码分析系列博文,同时也有与之对应的 Redis 中文正文版,有想深刻学习 Redis 的同学,欢送 star 和关注。
Redis 中文注解版仓库:https://github.com/xindoo/Redis
Redis 源码分析专栏:https://zxs.io/s/1h
如果感觉本文对你有用,欢送 一键三连

正文完
 0