[redis 源码走读] aof 持久化 (下)

[redis 源码走读] aof 持久化,文章篇幅有点长,所以拆分上下为两章,可以先读上一章,再读这一章。

此博客将逐步迁移到作者新的博客,可以点击此处进入。



应用场景

aof 持久化应用场景

启动加载

redis 启动,程序会模拟一个客户端加载从 aof 文件读出的命令。

aof 持久化支持 aof 和 rdb 混合模式,参考上面的 aof 和 rdb 混合结构

int main(int argc, char **argv) {
    ...
    loadDataFromDisk();
    ...
}

void loadDataFromDisk(void) {
    ...
    if (server.aof_state == AOF_ON) {
        if (loadAppendOnlyFile(server.aof_filename) == C_OK)
            serverLog(LL_NOTICE,"DB loaded from append only file: %.3f seconds",(float)(ustime()-start)/1000000);
    }
    ...
}

int loadAppendOnlyFile(char *filename) {
    ...
    // 程序模拟一个客户端执行从 aof 文件读出的命令。
    fakeClient = createAOFClient();
    ...
    // 检查 aof 文件读取数据方式。
    char sig[5];
    if (fread(sig,1,5,fp) != 5 || memcmp(sig,"REDIS",5) != 0) {
        // 通过 aof 方式加载数据。
        if (fseek(fp,0,SEEK_SET) == -1) goto readerr;
    } else {
        ...
        // 通过 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;
        }
    }

    /* Read the actual AOF file, in REPL format, command by command. */
    while(1) {
        // 根据 aof 文件数据结构,取出数据回写内存。
        ...
    }
    ...
}

写命令执行流程

  1. client 向 redis 服务发送写命令。
  2. redis 服务接收命令,进行业务处理。
  3. redis 服务将新的写命令追加到 aof 数据缓冲区。
  4. redis 服务会通过时钟,(eventloop)事件处理前(beforeSleep)等方法将 aof 数据缓冲区落地,然后清空 aof 缓冲区。
  • 流程
call(client * c, int flags) (/Users/wenfh2020/src/other/redis/src/server.c:3266)
processCommand(client * c) (/Users/wenfh2020/src/other/redis/src/server.c:3552)
...
aeProcessEvents(aeEventLoop * eventLoop, int flags) (/Users/wenfh2020/src/other/redis/src/ae.c:457)
aeMain(aeEventLoop * eventLoop) (/Users/wenfh2020/src/other/redis/src/ae.c:515)
main(int argc, char ** argv) (/Users/wenfh2020/src/other/redis/src/server.c:5054)
  • 执行命令,填充 aof 数据缓冲区
/* Command propagation flags, see propagate() function
   + PROPAGATE_NONE (no propagation of command at all)
   + PROPAGATE_AOF (propagate into the AOF file if is enabled)
   + PROPAGATE_REPL (propagate into the replication link)
*/

#define PROPAGATE_NONE 0
#define PROPAGATE_AOF 1
#define PROPAGATE_REPL 2

void call(client *c, int flags) {
    ...
    c->cmd->proc(c);
    ...
    if (propagate_flags != PROPAGATE_NONE && !(c->cmd->flags & CMD_MODULE))
        propagate(c->cmd,c->db->id,c->argv,c->argc,propagate_flags);
    ...
}

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);
    ...
}

// aof 缓冲区
struct redisServer {
    ...
    sds aof_buf;      /* AOF buffer, written before entering the event loop */
    ...
}

// 追加内容到 aof 文件
void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int argc) {
    sds buf = sdsempty();
    robj *tmpargv[3];

    // 命令执行,需要指定到对应数据库。
    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;
    }
    ...
    // 将命令格式化为 redis 命令格式,然后追加到 aof 数据缓冲区。
    buf = catAppendOnlyGenericCommand(buf,argc,argv);
    ...
    if (server.aof_state == AOF_ON)
        server.aof_buf = sdscatlen(server.aof_buf,buf,sdslen(buf));

    // 如果有子进程正在重写,父进程将新的数据发送给正在重写的子进程,使得重写文件数据更完备。
    if (server.aof_child_pid != -1)
        aofRewriteBufferAppend((unsigned char*)buf,sdslen(buf));
    ...
}
  • 重写过程中,父进程接收到新的命令,父进程发送给子进程,对重写数据进行追加。

    父子进程通过管道进行通信交互。

void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int argc) {
    ...
    // 如果有子进程正在重写,父进程将新的数据发送给正在重写的子进程,使得重写文件数据更完备。
    if (server.aof_child_pid != -1)
        aofRewriteBufferAppend((unsigned char*)buf,sdslen(buf));
    ...
}

// 将数据保存到重写缓冲区链表。然后通过父子进程管道进行数据传输
void aofRewriteBufferAppend(unsigned char *s, unsigned long len) {}

// 父进程通过管道把重写缓冲区数据,发送到子进程
void aofChildWriteDiffData(aeEventLoop *el, int fd, void *privdata, int mask) {}

// 子进程读取父进程发送的数据。
ssize_t aofReadDiffFromParent(void) {...}

// 创建父子进程通信管道
int aofCreatePipes(void) {...}

// 父子结束通信
void aofChildPipeReadable(aeEventLoop *el, int fd, void *privdata, int mask) {}

定时保存

主要对延时刷新和写磁盘出现错误回写的检查刷新。

/* Using the following macro you can run code inside serverCron() with the
 * specified period, specified in milliseconds.
 * The actual resolution depends on server.hz. */
#define run_with_period(_ms_)         \
    if ((_ms_ <= 1000 / server.hz) || \
        !(cronloops % ((_ms_) / (1000 / server.hz))))

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
    ...
    // 如果有延时任务,定时检查刷新。
    if (server.aof_flush_postponed_start) flushAppendOnlyFile(0);

    // 刷新缓存到磁盘出现错误(例如:磁盘满了),定时检查回写。
    // hz 频率为 10 ,这里一般每十次时钟检查一次。
    run_with_period(1000) {
        if (server.aof_last_write_status == C_ERR)
            flushAppendOnlyFile(0);
    }
    ...
    server.cronloops++;
    return 1000/server.hz;
}

重写

服务器接收到写入操作命令会追加到 aof 文件,那么 aof 文件相当于一个流水文件。随着时间推移,文件将会越来越大。然而 aof 文件主要目的是为了持久化,并不是为了记录服务器流水。这些流水命令有可能很多是冗余的,需要重新整理——通过重写来减小 aof 文件体积。

例如下面 4 条命令,会追加记录到 aof 文件,因为对同一个 key 操作,内存里最终数据 key1 对应的数据是 4,这样前面 3 条历史命令是冗余的,通过重写功能,aof 文件只留下 key 对应的最新的 value。

set key1 1
set key1 2
set key1 3
set key1 4

重写方式
void bgrewriteaofCommand(client *c) {
    if (server.aof_child_pid != -1) {
        // 当重写正在进行时,返回错误。
        addReplyError(c,"Background append only file rewriting already in progress");
    } else if (hasActiveChildProcess()) {
        // 当有其它子进程正在进行工作时,延后执行。
        server.aof_rewrite_scheduled = 1;
        addReplyStatus(c,"Background append only file rewriting scheduled");
    } else if (rewriteAppendOnlyFileBackground() == C_OK) {
        // 异步执行重写
        addReplyStatus(c,"Background append only file rewriting started");
    } else {
        // 重写操作失败,检查原因。
        addReplyError(c,"Can't execute an AOF background rewriting. "
                        "Please check the server logs for more information.");
    }
}
  • 时钟定期检查 redis 使用内存大小,当超过配置的阈值,触发自动重写。
# redis.conf

# 当前增加的内存超过上一次重写后的内存百分比,才会触发自动重写。
auto-aof-rewrite-percentage 100

# 内存重写下限
auto-aof-rewrite-min-size 64mb
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
    ...
    /* Trigger an AOF rewrite if needed. */
    if (server.aof_state == AOF_ON &&
        !hasActiveChildProcess() &&
        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();
        }
    }
    ...
}

重写实现
  1. 父进程 fork 子进程实现重写逻辑。
  2. 子进程创建 aof 临时文件存储重写子进程fork-on-write 内存到 aof 文件。
  3. 子进程重写完成 fork 内存数据内容后,追加在重写过程中父进程发送的新的内容。
  4. 子进程结束父子进程管道通信。
  5. 更新临时文件覆盖旧的文件。
// 父进程 fork 子进程进行 aof 重写
int rewriteAppendOnlyFileBackground(void) {
    ...
    if ((childpid = redisFork()) == 0) {
        ...
        if (rewriteAppendOnlyFile(tmpfile) == C_OK) {
            sendChildCOWInfo(CHILD_INFO_TYPE_AOF, "AOF rewrite");
            exitFromChild(0);
        } else {
            exitFromChild(1);
        }
    } else {
        /* Parent */
        ...
    }
    return C_OK; /* unreached */
}

// 重写 aof 实现逻辑
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;
    }

    server.aof_child_diff = sdsempty();
    rioInitWithFile(&aof,fp);

    // 逐步将文件缓存刷新到磁盘。
    if (server.aof_rewrite_incremental_fsync)
        rioSetAutoSync(&aof,REDIS_AUTOSYNC_BYTES);

    startSaving(RDBFLAGS_AOF_PREAMBLE);

    // 根据配置,重写文件内容方式,rdb 或者 aof,aof 存储方式支持 rdb 和 aof 内容兼容在同一个 aof 文件。
    if (server.aof_use_rdb_preamble) {
        int error;
        if (rdbSaveRio(&aof,&error,RDBFLAGS_AOF_PREAMBLE,NULL) == C_ERR) {
            errno = error;
            goto werr;
        }
    } else {
        if (rewriteAppendOnlyFileRio(&aof) == C_ERR) goto werr;
    }

    // 进程内存更新完毕,刷新文件到磁盘。
    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; /* Start counting from zero, we stop on N *contiguous*
                       timeouts. */
        aofReadDiffFromParent();
    }

    // 子进程通知父进程不要发新的数据了。
    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;

    // 父进程收到子进程的结束通知,发送确认给子进程。
    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...");

    /* Read the final diff if any. */
    aofReadDiffFromParent();

    // 子进程接收父进程发送的内容缓存在缓冲区,将缓冲区内容追加到重写 aof 文件后。
    serverLog(LL_NOTICE,
        "Concatenating %.2f MB of AOF diff received from parent.",
        (double) sdslen(server.aof_child_diff) / (1024*1024));
    if (rioWrite(&aof,server.aof_child_diff,sdslen(server.aof_child_diff)) == 0)
        goto werr;

    // 内容写入文件完毕,刷新文件缓存到磁盘。
    if (fflush(fp) == EOF) goto werr;
    if (fsync(fileno(fp)) == -1) goto werr;
    if (fclose(fp) == EOF) goto werr;

    // 新的重写 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);
    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;
}

调试

我一直认为:看文档和结合源码调试是理解一个项目的最好方法。

  • gdb 调试,在自己感兴趣的地方设下断点,通过调试熟悉 redis aof 持久化工作流程。

    调试方法可以参考我的帖子: 用 gdb 调试 redis

调试流程

  • 开启日志
# redis.conf

# Specify the server verbosity level.
# This can be one of:
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably)
# warning (only very important / critical messages are logged)
loglevel notice

# Specify the log file name. Also the empty string can be used to force
# Redis to log on the standard output. Note that if you use standard
# output for logging but daemonize, logs will be sent to /dev/null
logfile "redis.log"

总结

  • aof 文件存储 RESP 命令,新数据追加到文件末。
  • aof 存储为了避免冗余,需要设置重写处理。
  • aof 有三种存储策略,默认每秒存盘一次。根据自己的使用场景,选择存储策略。
  • 每秒存盘策略和重写功能通过多线程异步处理,保证主线程高性能。
  • 关注 redis 的博客,多看 redis.conf 配置项,里面有很多信息量。
  • aof 持久化文件支持 aof 和 rdb 方式混合存储,可以快速重写,并且减少 aof 体积。
  • aof 与 rdb 相比文件体积大,但是容灾能力强,出现问题丢失数据少。

参考


<think>我们正在讨论Redis中serverCron的实现原理及其在过期键删除中的作用。根据引用[1]和[2],我们知道Redis使用惰性删除和定期删除两种策略,而定期删除就是由serverCron定时任务触发的。 首先,我们需要了解serverCron是什么。它是Redis服务器中的一个周期性操作函数,负责管理服务器的常规任务,如键过期、持久化、集群同步等。这个函数默认每秒执行10次(可通过配置hz参数调整)。 在serverCron中,过期键删除的具体流程如下: 1. 遍历所有数据库(默认16个)。 2. 对每个数据库,随机抽取一部分设置了过期时间的键(由ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP配置,默认20个)。 3. 检查这些键是否过期,如果过期则删除。 4. 如果本轮删除的过期键数量超过抽取样本的25%,则重复步骤2-3(即继续抽样删除),直到比例低于25%或达到时间限制(避免阻塞主线程)。 这种设计使得Redis在过期键清理上实现了时间与空间的平衡:既不会因为频繁扫描所有键而消耗过多CPU,也不会因清理不及时导致内存占用过高。 接下来,我们结合源码分析serverCron的实现(以Redis 6.0为例): ```c // server.c int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { // ... 其他任务(如更新时钟、处理客户端超时等) // 执行过期键清理 databasesCron(); // ... } void databasesCron(void) { // 如果开启了过期键清理功能 if (server.active_expire_enabled) { // 如果服务器是主节点,或者没有配置延迟监控,则执行主动过期 if (server.masterhost == NULL) { activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW); } else { expireSlaveKeys(); } } // ... 其他数据库维护任务(如resize哈希表) } ``` 其中,`activeExpireCycle`函数实现了定期删除的核心逻辑: ```c void activeExpireCycle(int type) { // ... do { // 随机抽取数据库中的过期键 // 如果当前数据库没有过期键,则跳过 if ((num = dictSize(db->expires)) == 0) { db->avg_ttl = 0; break; } slots = dictSlots(db->expires); now = mstime(); // 当样本数量不足且未超时,继续抽样 while (sampled < num && checked < slots) { // 随机选择一个槽位 de = dictGetRandomKey(db->expires); ttl = dictGetSignedIntegerVal(de)-now; if (ttl < 0) { // 键已过期,删除 deleteExpiredKeyAndPropagate(db,keyptr); expired++; } sampled++; } // 计算过期键比例 if (sampled) { percent = (expired*100)/sampled; } else { percent = 0; } // 如果过期比例超过25%,则继续在当前数据库清理 } while (percent > 25 && !timelimit_exceeded); } ``` 从源码可见,定期删除通过随机抽样和比例控制(25%阈值)实现了自适应清理:过期键越多时清理力度越大,反之则快速退出以节省CPU[^2]。 最后,serverCron的执行频率由`hz`参数控制(默认10,即每秒10次)。提高`hz`会增加CPU使用但提升过期键清理的及时性,需根据业务场景权衡[^1]。 --- §§相关问题§§ 1. 如何配置Redis的`hz`参数以平衡性能与过期键清理效率? 2. Redis的定期删除在集群模式下与主从复制如何协同? 3. 为什么Redis选择25%作为重复清理的阈值? 4. 如何监控Redis过期键的积累情况? [^1]: Redis 键的过期删除策略及缓存淘汰策略 [^2]: Redis(五):关于过期键(2)过期键的删除 [^3]: [redis 源码走读] redis 过期策略 [^4]: [redis 源码走读] - redis 过期策略 [^5]: Redis-数据库、键过期的实现</think>### Redis 中 `serverCron` 的实现原理与过期键删除机制 #### 一、`serverCron` 的核心作用 `serverCron` 是 Redis 的核心定时任务函数,默认**每秒执行 10 次**(通过配置文件 `hz` 参数调整)。它负责维护服务器状态,包括: 1. **过期键清理**(核心功能) 2. 持久化(RDB/AOF)管理 3. 主从复制协调 4. 集群状态同步 5. 客户端超时检测 6. 内存碎片整理[^1] #### 二、`serverCron` 的实现原理 1. **事件驱动架构** Redis 基于事件循环(`aeEventLoop`)运行,`serverCron` 作为**时间事件**注册到事件循环中: ```c // server.c aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL); ``` 其中 `1` 表示初始执行间隔(毫秒),实际间隔由 `hz` 动态调整[^3]。 2. **自适应执行机制** 每次执行时动态计算下次触发时间: ```c int serverCron(struct aeEventLoop *eventLoop, ...) { // 计算耗时并调整下次执行间隔 double elapsed_ms = calc_elapsed_time(); double next_delay = 1000/server.hz - elapsed_ms; return next_delay > 0 ? next_delay : 1; } ``` 这种设计确保**即使单次任务超时,也不会阻塞事件循环**。 3. **分阶段任务处理** 通过 `cronloops` 计数器实现任务分段执行: ```c static int cronloops = 0; // 全局计数器 cronloops++; if (cronloops % 10 == 0) { // 每10次循环执行低频任务 clusterCron(); // 集群维护 resizeReplicationBacklog(); // 复制缓冲区调整 } ``` #### 三、过期键删除在 `serverCron` 中的实现 在 `serverCron` 中调用 `databasesCron()` 处理过期键,核心流程如下: 1. **数据结构基础** 过期键存储在专属字典 `redisDb->expires` 中: ```c typedef struct redisDb { dict *dict; // 键空间(所有键值对) dict *expires; // 过期字典:key→指针, value→毫秒时间戳 } redisDb; // [^5] ``` 2. **定期删除算法** 每次随机抽取部分键检查(源码摘要): ```c void activeExpireCycle(int type) { for (j = 0; j < dbs_per_call; j++) { // 1. 随机抽取 keys_per_loop 个键(默认20) expired = 0; for (i = 0; i < keys_per_loop; i++) { de = dictGetRandomKey(db->expires); // 2. 检查是否过期(当前时间 > expires[de]) if (now > expire_time) { deleteExpiredKey(de); // 删除键 expired++; } } // 3. 自适应重试:若过期比例 >25% 则继续处理 do { if ((expired*100/sampled) <= 25) break; // 再次抽样删除... } while (expired > 0); } } ``` - **随机抽样**:避免全表扫描的 $O(n)$ 开销 - **25%阈值**:平衡 CPU 与内存效率(高过期率时加大清理力度) - **时间限制**:单次最长执行 25ms(防止阻塞主线程)[^2] 3. **协同删除机制** `serverCron` 的定期删除与**惰性删除**互补: - 定期删除:周期性减少内存占用 - 惰性删除:访问键时实时检查(`expireIfNeeded()` 函数) ```mermaid graph LR A[客户端访问键] --> B{检查 expires 字典} B -->|已过期| C[删除键并返回空] B -->|未过期| D[返回数据] ``` #### 四、性能优化设计 1. **渐进式 rehash** 当 `expires` 字典扩容/缩容时,结合定时任务逐步迁移数据: ```c if (dictIsRehashing(db->expires)) { dictRehashMilliseconds(db->expires,1); // 每次迁移1ms } ``` 2. **从库特殊处理** 从库的过期键由主库同步 DEL 命令删除,自身不主动清理(保持数据一致性)[^4]。 3. **内存淘汰联动** 当内存超限时,`serverCron` 会触发 `freeMemoryIfNeeded()` 执行 LRU/LFU 淘汰策略,与过期键清理协同释放内存。 #### 五、配置参数影响 | 参数 | 默认值 | 作用 | 对过期键清理的影响 | |------------------|--------|-------------------------------|----------------------------------| | `hz` | 10 | `serverCron` 每秒执行次数 | 值越大清理越及时,CPU 消耗越高 | | `maxmemory` | 0 | 最大内存限制 | 超限时触发额外淘汰机制 | | `active-expire-effort` | 1 | 清理力度(1-10) | 值越大抽样数量越多,清理更彻底 | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值