← 返回文章列表

Redis底层原理(五):持久化机制详解

2020-02-18·9 分钟阅读

前言

Redis 作为内存数据库,数据存储在内存中,一旦服务器退出,内存中的数据就会丢失。为了保证数据安全,Redis 提供了两种持久化机制:RDB(Redis Database)快照和 AOF(Append Only File)日志。本章将深入分析这两种持久化机制的实现原理。

一、持久化概述

1.1 为什么需要持久化?

┌──────────────────────────────────────────────────────────────┐
│                    持久化的必要性                             │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  内存数据                                                    │
│  ┌─────────────────────────────────────────────────────────┐│
│  │  Key1 → Value1                                          ││
│  │  Key2 → Value2                                          ││
│  │  Key3 → Value3                                          ││
│  │  ...                                                    ││
│  └─────────────────────────────────────────────────────────┘│
│                           │                                  │
│                           ▼                                  │
│  ┌─────────────────────────────────────────────────────────┐│
│  │           服务重启/崩溃 → 内存数据丢失                   ││
│  └─────────────────────────────────────────────────────────┘│
│                           │                                  │
│                           ▼                                  │
│  ┌─────────────────────────────────────────────────────────┐│
│  │              持久化机制 → 数据持久保存                   ││
│  │                                                         ││
│  │   方案一:RDB 快照       方案二:AOF 日志               ││
│  │   定时保存内存快照       记录每个写命令                 ││
│  └─────────────────────────────────────────────────────────┘│
│                                                              │
└──────────────────────────────────────────────────────────────┘

1.2 两种持久化方式对比

┌──────────────────────────────────────────────────────────────┐
│                    RDB vs AOF 对比                            │
├─────────────────┬──────────────────┬────────────────────────┤
│     特性        │       RDB        │         AOF            │
├─────────────────┼──────────────────┼────────────────────────┤
│ 持久化方式      │ 全量快照          │ 增量日志               │
│ 文件大小        │ 较小(压缩)      │ 较大                   │
│ 恢复速度        │ 快               │ 慢                     │
│ 数据安全性      │ 可能丢失几分钟数据 │ 最多丢失 1 秒数据      │
│ 系统资源消耗    │ fork 开销         │ 持续写入开销           │
│ 适用场景        │ 备份、容灾        │ 数据安全要求高         │
└─────────────────┴──────────────────┴────────────────────────┘

推荐配置:RDB + AOF 混合使用

二、RDB 持久化

2.1 RDB 简介

RDB 持久化是将当前内存中的数据生成快照保存到磁盘的二进制文件中。

┌──────────────────────────────────────────────────────────────┐
│                    RDB 工作原理                               │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  触发条件                                                    │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ 1. 手动触发:SAVE / BGSAVE 命令                         ││
│  │ 2. 自动触发:配置文件中设置 save 规则                   ││
│  │ 3. 主从复制:主节点自动执行 BGSAVE                      ││
│  │ 4. 关闭服务:SHUTDOWN 时(默认开启)                    ││
│  └─────────────────────────────────────────────────────────┘│
│                           │                                  │
│                           ▼                                  │
│  ┌─────────────────────────────────────────────────────────┐│
│  │                    BGSAVE 执行流程                       ││
│  │                                                         ││
│  │  Redis 主进程                                           ││
│  │  ┌───────────────────────────────────────────────────┐ ││
│  │  │  1. fork() 创建子进程                              │ ││
│  │  │         ↓                                         │ ││
│  │  │  ┌─────────────┐    ┌─────────────┐              │ ││
│  │  │  │  主进程      │    │  子进程      │              │ ││
│  │  │  │  继续处理    │    │  生成 RDB    │              │ ││
│  │  │  │  客户端请求  │    │  文件        │              │ ││
│  │  │  └─────────────┘    └─────────────┘              │ ││
│  │  │                              │                     │ ││
│  │  │                              ▼                     │ ││
│  │  │                    ┌─────────────────┐            │ ││
│  │  │                    │ dump.rdb 文件    │            │ ││
│  │  │                    └─────────────────┘            │ ││
│  │  └───────────────────────────────────────────────────┘ ││
│  └─────────────────────────────────────────────────────────┘│
│                                                              │
└──────────────────────────────────────────────────────────────┘

2.2 RDB 文件结构

┌──────────────────────────────────────────────────────────────┐
│                    RDB 文件结构                               │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌──────────┬──────────┬──────────┬────────────────────────┐│
│  │  REDIS   │  VERSION │  SELECT  │   DATABASE DATA        ││
│  │  5 bytes │  4 bytes │  DB ID   │   ...                  ││
│  └──────────┴──────────┴──────────┴────────────────────────┘│
│  ┌──────────┬──────────┬──────────┬────────────────────────┐│
│  │  EOF     │  CHECKSUM│          │                        ││
│  │  1 byte  │  8 bytes │          │                        ││
│  └──────────┴──────────┴──────────┴────────────────────────┘│
│                                                              │
│  详细结构:                                                  │
│                                                              │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ REDIS      │ 魔数,固定为 "REDIS"                       ││
│  ├────────────┼────────────────────────────────────────────┤│
│  │ VERSION    │ RDB 版本号,如 "0009"                      ││
│  ├────────────┼────────────────────────────────────────────┤│
│  │ SELECT     │ 数据库编号                                ││
│  ├────────────┼────────────────────────────────────────────┤│
│  │ RESIZEDB   │ 数据库大小信息(可选)                     ││
│  ├────────────┼────────────────────────────────────────────┤│
│  │ KEY-VALUE  │ 键值对数据                                ││
│  │   ├── KEY  │ 键名                                      ││
│  │   ├── TYPE │ 值类型                                    ││
│  │   └── VALUE│ 值数据                                    ││
│  ├────────────┼────────────────────────────────────────────┤│
│  │ EXPIRE     │ 过期时间(可选)                           ││
│  ├────────────┼────────────────────────────────────────────┤│
│  │ EOF        │ 结束标志                                  ││
│  ├────────────┼────────────────────────────────────────────┤│
│  │ CHECKSUM   │ 校验和                                    ││
│  └────────────┴────────────────────────────────────────────┘│
│                                                              │
└──────────────────────────────────────────────────────────────┘

2.3 RDB 源码分析

2.3.1 BGSAVE 命令实现

// rdb.c
int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
    pid_t childpid;
    
    // 已有子进程在执行
    if (server.rdb_child_pid != -1) return C_ERR;
    
    // 创建管道用于父子进程通信
    int fds[2];
    if (pipe(fds)) return C_ERR;
    
    server.child_type = CHILD_TYPE_RDB;
    
    // fork 子进程
    if ((childpid = redisFork(CHILD_TYPE_RDB)) == 0) {
        // 子进程
        int retval;
        closeListeningSockets(0);
        redisSetProcTitle("redis-rdb-bgsave");
        
        // 执行 RDB 保存
        retval = rdbSave(filename, rsi);
        
        // 发送完成信号给父进程
        if (retval == C_OK) {
            sendChildCOWInfo(CHILD_TYPE_RDB, 1, "RDB");
        }
        
        exitFromChild((retval == C_OK) ? 0 : 1);
    } else {
        // 父进程
        close(fds[1]);
        server.rdb_pipe_read = fds[0];
        server.rdb_child_pid = childpid;
        server.rdb_save_time_start = time(NULL);
        server.rdb_filename = zstrdup(filename);
        
        return C_OK;
    }
    return C_OK;
}

2.3.2 RDB 保存实现

// rdb.c
int rdbSave(char *filename, rdbSaveInfo *rsi) {
    char tmpfile[256];
    FILE *fp;
    rio rdb;
    int error = 0;
    
    // 生成临时文件名
    snprintf(tmpfile, 256, "temp-%d.rdb", (int)getpid());
    
    // 打开文件
    fp = fopen(tmpfile, "w");
    if (!fp) {
        serverLog(LL_WARNING, "Failed opening .rdb for saving: %s",
                  strerror(errno));
        return C_ERR;
    }
    
    // 初始化 rio 结构
    rioInitWithFile(&rdb, fp);
    
    // 设置自动同步
    if (server.rdb_save_incremental_fsync)
        rioSetAutoSync(&rdb, REDIS_AUTOSYNC_BYTES);
    
    // 写入 RDB 数据
    if (rdbSaveRio(&rdb, &error, RDBFLAGS_NONE, rsi) == C_ERR) {
        errno = error;
        goto werr;
    }
    
    // 确保数据写入磁盘
    if (fflush(fp)) goto werr;
    if (fsync(fileno(fp))) goto werr;
    if (fclose(fp)) goto werr;
    
    // 重命名文件
    if (rename(tmpfile, filename) == -1) {
        serverLog(LL_WARNING, "Error moving temp DB file on the final destination: %s",
                  strerror(errno));
        unlink(tmpfile);
        return C_ERR;
    }
    
    serverLog(LL_NOTICE, "DB saved on disk");
    server.dirty = 0;
    server.lastsave = time(NULL);
    server.lastbgsave_status = C_OK;
    
    return C_OK;

werr:
    serverLog(LL_WARNING, "Write error saving DB on disk: %s", strerror(errno));
    fclose(fp);
    unlink(tmpfile);
    return C_ERR;
}

2.3.3 写入键值对

// rdb.c
int rdbSaveRio(rio *rdb, int *error, int rdbflags, rdbSaveInfo *rsi) {
    // 写入魔数和版本
    if (rdbWriteRaw(rdb, magic, 9) == -1) goto werr;
    
    // 写入辅助字段
    if (rdbSaveInfoAuxFields(rdb, rdbflags, rsi) == -1) goto werr;
    
    // 遍历所有数据库
    for (j = 0; j < server.dbnum; j++) {
        redisDb *db = server.db + j;
        
        if (db->dict->used == 0) continue;
        
        // 写入 SELECTDB
        if (rdbSaveType(rdb, RDB_OPCODE_SELECTDB) == -1) goto werr;
        if (rdbSaveLen(rdb, j) == -1) goto werr;
        
        // 写入数据库大小
        uint64_t db_size = dictSize(db->dict);
        uint64_t expires_size = dictSize(db->expires);
        if (rdbSaveType(rdb, RDB_OPCODE_RESIZEDB) == -1) goto werr;
        if (rdbSaveLen(rdb, db_size) == -1) goto werr;
        if (rdbSaveLen(rdb, expires_size) == -1) goto werr;
        
        // 遍历并写入键值对
        while((de = dictNext(di)) != NULL) {
            sds keystr = dictGetKey(de);
            robj key, *o = dictGetVal(de);
            initStaticStringObject(key, keystr);
            
            // 写入过期时间
            expiretime = getExpire(db, &key);
            if (expiretime != -1) {
                if (rdbSaveType(rdb, RDB_OPCODE_EXPIRETIME_MS) == -1) goto werr;
                if (rdbSaveMillisecondTime(rdb, expiretime) == -1) goto werr;
            }
            
            // 写入键值对
            if (rdbSaveKeyValuePair(rdb, &key, o, expiretime) == -1) goto werr;
        }
    }
    
    // 写入 EOF
    if (rdbSaveType(rdb, RDB_OPCODE_EOF) == -1) goto werr;
    
    // 写入校验和
    uint64_t checksum = rdb->processed_bytes;
    if (rioWrite(rdb, &checksum, 8) == 0) goto werr;
    
    return C_OK;

werr:
    if (error) *error = errno;
    return C_ERR;
}

2.4 RDB 配置

# redis.conf

# RDB 自动触发条件
# 格式:save <秒数> <修改次数>
save 900 1      # 900 秒内至少 1 次修改
save 300 10     # 300 秒内至少 10 次修改
save 60 10000   # 60 秒内至少 10000 次修改

# 禁用 RDB
# save ""

# RDB 文件名
dbfilename dump.rdb

# RDB 文件存储目录
dir ./

# 压缩(默认开启)
rdbcompression yes

# 校验和(默认开启)
rdbchecksum yes

# 后台保存失败时停止写入
stop-writes-on-bgsave-error yes

2.5 RDB 的优缺点

┌──────────────────────────────────────────────────────────────┐
│                    RDB 优缺点分析                             │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  优点:                                                      │
│  ✅ 文件紧凑,适合备份和传输                                  │
│  ✅ 恢复速度快,直接加载到内存                               │
│  ✅ 对性能影响小,fork 后主进程继续服务                       │
│  ✅ 适合灾难恢复                                             │
│                                                              │
│  缺点:                                                      │
│  ❌ 数据安全性低,可能丢失几分钟数据                          │
│  ❌ fork 大数据集时可能阻塞(Copy-on-Write)                  │
│  ❌ 不适合实时持久化                                          │
│                                                              │
│  适用场景:                                                  │
│  • 允许少量数据丢失                                          │
│  • 数据量较大,对恢复速度有要求                               │
│  • 作为 AOF 的补充                                           │
│                                                              │
└──────────────────────────────────────────────────────────────┘

三、AOF 持久化

3.1 AOF 简介

AOF(Append Only File)通过记录所有写命令来实现持久化。

┌──────────────────────────────────────────────────────────────┐
│                    AOF 工作原理                               │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  客户端发送命令                                              │
│  ┌─────────────────────────────────────────────────────────┐│
│  │  SET key1 value1                                        ││
│  │  LPUSH list a b c                                       ││
│  │  SADD set1 member1                                      ││
│  └─────────────────────────────────────────────────────────┘│
│                           │                                  │
│                           ▼                                  │
│  ┌─────────────────────────────────────────────────────────┐│
│  │                    AOF 写入流程                         ││
│  │                                                         ││
│  │  命令执行 ──► AOF 缓冲区 ──► AOF 文件                   ││
│  │                   │                                     ││
│  │                   ▼                                     ││
│  │              根据策略刷盘                                ││
│  │         ┌───────────────────────────┐                   ││
│  │         │ always  │ everysec │ no   │                   ││
│  │         │ 每次写入 │ 每秒     │系统决定│                   ││
│  │         └───────────────────────────┘                   ││
│  └─────────────────────────────────────────────────────────┘│
│                           │                                  │
│                           ▼                                  │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ appendonly.aof 文件内容:                               ││
│  │                                                         ││
│  │ *3                                                      ││
│  │ $3                                                      ││
│  │ SET                                                      ││
│  │ $4                                                      ││
│  │ key1                                                     ││
│  │ $6                                                      ││
│  │ value1                                                   ││
│  │ ...                                                      ││
│  └─────────────────────────────────────────────────────────┘│
│                                                              │
└──────────────────────────────────────────────────────────────┘

3.2 AOF 文件格式

AOF 文件使用 RESP 协议格式存储命令:

┌──────────────────────────────────────────────────────────────┐
│                    AOF 文件格式(RESP 协议)                  │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  命令:SET mykey myvalue                                     │
│                                                              │
│  AOF 文件内容:                                              │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ *3\r\n                   # 参数数量 = 3                  ││
│  │ $3\r\n                   # 第一个参数长度 = 3            ││
│  │ SET\r\n                  # 第一个参数值                  ││
│  │ $5\r\n                   # 第二个参数长度 = 5            ││
│  │ mykey\r\n                # 第二个参数值                  ││
│  │ $7\r\n                   # 第三个参数长度 = 7            ││
│  │ myvalue\r\n              # 第三个参数值                  ││
│  └─────────────────────────────────────────────────────────┘│
│                                                              │
│  批量命令示例:                                              │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ *3\r\n$3\r\nSET\r\n$4\r\ncity\r\n$7\r\nBeijing\r\n      ││
│  │ *4\r\n$5\r\nLPUSH\r\n$4\r\nlist\r\n$1\r\na\r\n$1\r\nb\r\n││
│  └─────────────────────────────────────────────────────────┘│
│                                                              │
└──────────────────────────────────────────────────────────────┘

3.3 AOF 源码分析

3.3.1 命令追加

// aof.c
void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int argc) {
    sds buf = sdsempty();
    robj *tmpargv[3];
    
    // 如果正在 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;
    }
    
    // 过期时间处理
    if (cmd->proc == expireCommand || cmd->proc == pexpireCommand ||
        cmd->proc == expireatCommand || cmd->proc == pexpireatCommand) {
        // 将 EXPIRE 转换为 PEXPIREAT
        buf = catAppendOnlyExpireAtCommand(buf, cmd, argv[1], argv[2]);
    } else {
        // 普通命令,序列化为 RESP 格式
        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.aof_child_pid != -1) {
        aofRewriteBufferAppend((unsigned char *)buf, sdslen(buf));
    }
    
    sdsfree(buf);
}

// 将命令序列化为 RESP 格式
sds catAppendOnlyGenericCommand(sds dst, int argc, robj **argv) {
    char buf[32];
    int len, j;
    robj *o;
    
    // 写入参数数量
    len = ll2string(buf, sizeof(buf), argc);
    dst = sdscatlen(dst, "*", 1);
    dst = sdscatlen(dst, buf, len);
    dst = sdscatlen(dst, "\r\n", 2);
    
    // 写入每个参数
    for (j = 0; j < argc; j++) {
        o = getDecodedObject(argv[j]);
        len = ll2string(buf, sizeof(buf), sdslen(o->ptr));
        dst = sdscatlen(dst, "$", 1);
        dst = sdscatlen(dst, buf, len);
        dst = sdscatlen(dst, "\r\n", 2);
        dst = sdscatlen(dst, o->ptr, sdslen(o->ptr));
        dst = sdscatlen(dst, "\r\n", 2);
        decrRefCount(o);
    }
    
    return dst;
}

3.3.2 AOF 刷盘

// aof.c
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 + 1) {
            aof_fsync(server.aof_fd);
            server.aof_fsync_offset = server.aof_current_size;
        }
        return;
    }
    
    // 检查是否有正在进行的同步
    if (server.aof_fsync == AOF_FSYNC_EVERYSEC) {
        sync_in_progress = aofFsyncInProgress();
    }
    
    // 如果有同步在进行且不需要强制
    if (sync_in_progress && !force) {
        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) {
            return;
        }
        server.aof_delayed_fsync++;
    }
    
    // 写入 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)) {
        // 写入失败处理
        // ...
    }
    
    server.aof_current_size += nwritten;
    
    // 清空缓冲区
    if ((nwritten + server.aof_rewrite_submitted) == 0) {
        sdsfree(server.aof_buf);
        server.aof_buf = sdsempty();
    } else {
        server.aof_buf = sdsrange(server.aof_buf, nwritten, -1);
    }
    
    // 根据策略同步
    if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
        // 每次写入都同步
        aof_fsync(server.aof_fd);
        server.aof_fsync_offset = server.aof_current_size;
    } else if (server.aof_fsync == AOF_FSYNC_EVERYSEC) {
        // 每秒同步一次
        if (server.unixtime > server.aof_last_fsync + 1) {
            aof_background_fsync(server.aof_fd);
            server.aof_fsync_offset = server.aof_current_size;
        }
    }
    // AOF_FSYNC_NO: 由操作系统决定
}

3.4 AOF 重写

随着写操作的增加,AOF 文件会越来越大。Redis 提供了 AOF 重写机制来压缩文件。

┌──────────────────────────────────────────────────────────────┐
│                    AOF 重写原理                               │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  原始 AOF 文件:                                             │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ SET count 1                                             ││
│  │ INCR count              ──►  count = 2                  ││
│  │ INCR count              ──►  count = 3                  ││
│  │ INCR count              ──►  count = 4                  ││
│  │ LPUSH list a                                            ││
│  │ LPUSH list b            ──►  list = [b, a]              ││
│  │ LPUSH list c            ──►  list = [c, b, a]           ││
│  └─────────────────────────────────────────────────────────┘│
│                           │                                  │
│                           ▼                                  │
│  重写后的 AOF 文件:                                         │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ SET count 4              # 直接保存最终值                ││
│  │ RPUSH list c b a         # 一条命令重建列表              ││
│  └─────────────────────────────────────────────────────────┘│
│                                                              │
│  重写特点:                                                  │
│  • 读取当前内存数据生成新文件                                │
│  • 不需要分析原 AOF 文件                                    │
│  • 合并多条命令为一条                                        │
│  • 过期数据不会写入                                          │
│                                                              │
└──────────────────────────────────────────────────────────────┘

3.4.1 AOF 重写实现

// aof.c
int rewriteAppendOnlyFile(char *filename) {
    rio aof;
    FILE *fp;
    char tmpfile[256];
    
    // 创建临时文件
    snprintf(tmpfile, 256, "temp-rewriteaof-%d.aof", (int)getpid());
    fp = fopen(tmpfile, "w");
    if (!fp) return C_ERR;
    
    // 初始化 rio
    rioInitWithFile(&aof, fp);
    
    // 设置自动同步
    if (server.aof_rewrite_incremental_fsync)
        rioSetAutoSync(&aof, REDIS_AUTOSYNC_BYTES);
    
    // 遍历所有数据库
    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;
        
        // 写入 SELECT 命令
        if (rioWrite(&aof, selectcmd, sizeof(selectcmd) - 1) == 0) goto werr;
        if (rioWriteBulkLongLong(&aof, j) == 0) goto werr;
        
        // 遍历键值对
        di = dictGetSafeIterator(d);
        while ((de = dictNext(di)) != NULL) {
            sds keystr;
            robj key, *o;
            long long expiretime;
            
            keystr = dictGetKey(de);
            o = dictGetVal(de);
            initStaticStringObject(key, keystr);
            
            // 获取过期时间
            expiretime = getExpire(db, &key);
            
            // 跳过已过期的键
            if (expiretime != -1 && expiretime < server.unixtime) continue;
            
            // 根据类型写入命令
            if (o->type == OBJ_STRING) {
                // String 类型
                char cmd[] = "*3\r\n$3\r\nSET\r\n";
                if (rioWrite(&aof, cmd, sizeof(cmd) - 1) == 0) goto werr;
                if (rioWriteBulkObject(&aof, &key) == 0) goto werr;
                if (rioWriteBulkObject(&aof, o) == 0) goto werr;
            } else if (o->type == OBJ_LIST) {
                // List 类型
                if (rewriteListObject(&aof, &key, o) == 0) goto werr;
            } else if (o->type == OBJ_SET) {
                // Set 类型
                if (rewriteSetObject(&aof, &key, o) == 0) goto werr;
            } else if (o->type == OBJ_ZSET) {
                // ZSet 类型
                if (rewriteSortedSetObject(&aof, &key, o) == 0) goto werr;
            } else if (o->type == OBJ_HASH) {
                // Hash 类型
                if (rewriteHashObject(&aof, &key, o) == 0) goto werr;
            }
            
            // 写入过期时间
            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;
            }
        }
    }
    
    // 写入结束
    if (fflush(fp)) goto werr;
    if (fsync(fileno(fp))) goto werr;
    if (fclose(fp)) goto werr;
    
    // 重命名文件
    if (rename(tmpfile, filename) == -1) {
        unlink(tmpfile);
        return C_ERR;
    }
    
    return C_OK;

werr:
    fclose(fp);
    unlink(tmpfile);
    return C_ERR;
}

3.4.2 AOF 重写期间的命令处理

┌──────────────────────────────────────────────────────────────┐
│                AOF 重写期间的命令处理                         │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌─────────────────────────────────────────────────────────┐│
│  │                      主进程                             ││
│  │                                                         ││
│  │  客户端命令 ──► 执行命令                                ││
│  │                    │                                    ││
│  │          ┌────────┴────────┐                           ││
│  │          ▼                 ▼                           ││
│  │    AOF 缓冲区      AOF 重写缓冲区                       ││
│  │    (追加命令)      (记录重写期间的新命令)                ││
│  │          │                 │                           ││
│  │          ▼                 │                           ││
│  │    原有 AOF 文件           │                           ││
│  └─────────────────────────────────────────────────────────┘│
│                              │                               │
│                              │ fork                          │
│                              ▼                               │
│  ┌─────────────────────────────────────────────────────────┐│
│  │                      子进程                             ││
│  │                                                         ││
│  │  遍历内存数据 ──► 生成新 AOF 文件                       ││
│  │                                                         ││
│  └─────────────────────────────────────────────────────────┘│
│                              │                               │
│                              │ 完成                          │
│                              ▼                               │
│  ┌─────────────────────────────────────────────────────────┐│
│  │                    重写完成                             ││
│  │                                                         ││
│  │  1. 将重写缓冲区命令追加到新 AOF 文件                   ││
│  │  2. 原子性地用新文件替换旧文件                          ││
│  │                                                         ││
│  └─────────────────────────────────────────────────────────┘│
│                                                              │
└──────────────────────────────────────────────────────────────┘

3.5 AOF 配置

# redis.conf

# 开启 AOF
appendonly yes

# AOF 文件名
appendfilename "appendonly.aof"

# 刷盘策略
# always: 每次写入都同步,最安全但最慢
# everysec: 每秒同步一次,推荐(默认)
# no: 由操作系统决定,最快但最不安全
appendfsync everysec

# AOF 重写期间是否禁用 fsync
no-appendfsync-on-rewrite no

# AOF 重写触发条件
auto-aof-rewrite-percentage 100   # 文件大小比上次重写后增长 100%
auto-aof-rewrite-min-size 64mb    # 文件最小 64MB 才触发重写

# 加载损坏的 AOF 文件
aof-load-truncated yes

# 使用 RDB-AOF 混合持久化
aof-use-rdb-preamble yes

四、混合持久化

4.1 混合持久化原理

Redis 4.0 引入了 RDB-AOF 混合持久化,结合了两者的优点:

┌──────────────────────────────────────────────────────────────┐
│                    混合持久化原理                             │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  混合持久化文件结构:                                        │
│  ┌─────────────────────────────────────────────────────────┐│
│  │                    混合 AOF 文件                        ││
│  │  ┌─────────────────────────┬─────────────────────────┐ ││
│  │  │      RDB 格式数据       │    AOF 格式增量命令     │ ││
│  │  │    (基础数据快照)      │  (重写期间的新命令)    │ ││
│  │  └─────────────────────────┴─────────────────────────┘ ││
│  └─────────────────────────────────────────────────────────┘│
│                                                              │
│  重写过程:                                                  │
│                                                              │
│  Step 1: fork 子进程                                        │
│  ┌─────────────────────────────────────────────────────────┐│
│  │  主进程:继续处理命令,记录到重写缓冲区                  ││
│  │  子进程:生成 RDB 格式快照写入新文件                     ││
│  └─────────────────────────────────────────────────────────┘│
│                                                              │
│  Step 2: RDB 快照完成                                       │
│  ┌─────────────────────────────────────────────────────────┐│
│  │  子进程:将重写缓冲区的 AOF 命令追加到新文件             ││
│  └─────────────────────────────────────────────────────────┘│
│                                                              │
│  Step 3: 原子替换                                           │
│  ┌─────────────────────────────────────────────────────────┐│
│  │  主进程:用新文件替换旧 AOF 文件                         ││
│  └─────────────────────────────────────────────────────────┘│
│                                                              │
│  优势:                                                      │
│  • RDB 部分加载快,快速恢复基础数据                          │
│  • AOF 部分保证数据完整性                                    │
│  • 文件大小适中                                              │
│                                                              │
└──────────────────────────────────────────────────────────────┘

4.2 混合持久化配置

# redis.conf

# 开启混合持久化(需要同时开启 AOF)
aof-use-rdb-preamble yes

五、持久化恢复

5.1 恢复流程

┌──────────────────────────────────────────────────────────────┐
│                    Redis 启动恢复流程                         │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  Redis 启动                                                 │
│       │                                                      │
│       ▼                                                      │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ 检查是否开启 AOF                                         ││
│  │ (appendonly = yes?)                                      ││
│  └─────────────────────────────────────────────────────────┘│
│       │                                                      │
│       ├── 是 ──► 加载 AOF 文件恢复数据                       │
│       │                  │                                   │
│       │                  ▼                                   │
│       │           ┌───────────────────────────────────┐     │
│       │           │ 检查 AOF 文件开头是否为 RDB 格式   │     │
│       │           │ (混合持久化)                      │     │
│       │           └───────────────────────────────────┘     │
│       │                    │                                 │
│       │                    ├── 是 ──► 先加载 RDB 部分        │
│       │                    │          再加载 AOF 部分        │
│       │                    │                                 │
│       │                    └── 否 ──► 直接加载 AOF 命令      │
│       │                                                      │
│       └── 否 ──► 加载 RDB 文件恢复数据                       │
│                                                              │
│  注意:                                                      │
│  • AOF 优先级高于 RDB                                        │
│  • 文件不存在时正常启动                                       │
│  • 文件损坏时会启动失败                                       │
│                                                              │
└──────────────────────────────────────────────────────────────┘

5.2 数据恢复时间对比

数据量RDB 恢复AOF 恢复混合恢复
1GB~10s~30s~15s
10GB~100s~300s~150s
100GB~1000s~3000s~1500s

六、生产环境最佳实践

6.1 持久化策略选择

┌──────────────────────────────────────────────────────────────┐
│                    持久化策略选择指南                         │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  场景一:允许分钟级数据丢失                                  │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ 推荐配置:只开 RDB                                       ││
│  │ save 900 1                                               ││
│  │ save 300 10                                              ││
│  │ appendonly no                                            ││
│  │                                                         ││
│  │ 优点:性能最优,文件最小                                 ││
│  │ 缺点:可能丢失几分钟数据                                 ││
│  └─────────────────────────────────────────────────────────┘│
│                                                              │
│  场景二:数据安全要求高                                      │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ 推荐配置:AOF + 混合持久化                               ││
│  │ appendonly yes                                           ││
│  │ appendfsync everysec                                     ││
│  │ aof-use-rdb-preamble yes                                 ││
│  │                                                         ││
│  │ 优点:最多丢失 1 秒数据                                  ││
│  │ 缺点:文件较大,恢复稍慢                                 ││
│  └─────────────────────────────────────────────────────────┘│
│                                                              │
│  场景三:兼顾性能和数据安全                                  │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ 推荐配置:RDB + AOF 混合                                 ││
│  │ save 900 1                                               ││
│  │ save 300 10                                              ││
│  │ save 60 10000                                            ││
│  │ appendonly yes                                           ││
│  │ appendfsync everysec                                     ││
│  │ aof-use-rdb-preamble yes                                 ││
│  │                                                         ││
│  │ 优点:RDB 用于快速恢复,AOF 用于数据完整性               ││
│  └─────────────────────────────────────────────────────────┘│
│                                                              │
└──────────────────────────────────────────────────────────────┘

6.2 监控指标

# 查看 RDB 状态
INFO persistence
# 关注指标:
# rdb_last_save_time: 上次保存时间
# rdb_changes_since_last_save: 上次保存后的修改次数
# rdb_last_bgsave_status: 上次 BGSAVE 状态
# rdb_last_bgsave_time_sec: 上次 BGSAVE 耗时

# 查看 AOF 状态
INFO persistence
# 关注指标:
# aof_enabled: AOF 是否开启
# aof_current_size: AOF 文件大小
# aof_base_size: 上次重写时的大小
# aof_pending_rewrite: 是否有待执行的重写
# aof_last_rewrite_time_sec: 上次重写耗时

6.3 故障恢复流程

┌──────────────────────────────────────────────────────────────┐
│                    故障恢复流程                               │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  1. AOF 文件损坏                                            │
│     ┌─────────────────────────────────────────────────────┐ │
│     │ # 使用 redis-check-aof 工具修复                     │ │
│     │ redis-check-aof --fix appendonly.aof                │ │
│     │                                                     │ │
│     │ # 如果有 RDB 备份,可以使用 RDB 恢复                │ │
│     │ cp dump.rdb dump.rdb.bak                            │ │
│     │ cp backup/dump.rdb ./                               │ │
│     └─────────────────────────────────────────────────────┘ │
│                                                              │
│  2. RDB 文件损坏                                            │
│     ┌─────────────────────────────────────────────────────┐ │
│     │ # 使用 redis-check-rdb 检查                         │ │
│     │ redis-check-rdb dump.rdb                            │ │
│     │                                                     │ │
│     │ # RDB 损坏通常无法修复,需要使用备份                │ │
│     └─────────────────────────────────────────────────────┘ │
│                                                              │
│  3. 完全灾难恢复                                            │
│     ┌─────────────────────────────────────────────────────┐ │
│     │ # 1. 停止 Redis                                     │ │
│     │ # 2. 从备份恢复数据文件                             │ │
│     │ # 3. 启动 Redis                                     │ │
│     │ # 4. 验证数据完整性                                 │ │
│     └─────────────────────────────────────────────────────┘ │
│                                                              │
└──────────────────────────────────────────────────────────────┘

七、总结

本章深入分析了 Redis 的持久化机制:

特性RDBAOF混合持久化
数据安全
恢复速度较快
文件大小中等
系统开销低(fork)中(持续写入)
推荐场景备份/容灾数据安全综合

下一章将深入分析 Redis 的事件驱动模型。

参考资料

分享: