Redis 持久化策略

Redis 持久化策略详解

概述

Redis是一个内存数据库,为了保证数据的持久性,Redis提供了两种主要的持久化方式:RDB(Redis Database)AOF(Append Only File)。这两种方式可以单独使用,也可以组合使用,以满足不同场景下的数据安全需求。

1. RDB(Redis Database)持久化

1.1 基本概念

RDB持久化是通过生成数据快照的方式将某个时间点上的所有数据保存到磁盘上的一个RDB文件中。这是Redis默认的持久化方式。

1.2 RDB文件格式

RDB文件是一个二进制文件,包含以下部分:

REDIS0011φ      # 文件头(REDIS + 版本号 + 校验位)
Database 0      # 数据库选择器
Key-Value Data  # 键值对数据
EOF             # 文件结束标记
CRC64 Checksum  # 校验和

1.3 RDB触发方式

1.3.1 自动触发

通过配置文件设置自动保存条件:

# redis.conf 配置示例
save 900 1      # 900秒内至少1个key被修改
save 300 10     # 300秒内至少10个key被修改  
save 60 10000   # 60秒内至少10000个key被修改

# 其他相关配置
rdbcompression yes          # 启用压缩
rdbchecksum yes            # 启用校验
dbfilename dump.rdb        # RDB文件名
dir /var/lib/redis         # 存储目录

1.3.2 手动触发

# 阻塞式生成RDB文件
SAVE

# 非阻塞式生成RDB文件(推荐)
BGSAVE

# 获取最后一次保存时间
LASTSAVE

1.4 RDB生成过程

1.4.1 SAVE命令执行流程

// 伪代码:SAVE命令执行
void saveCommand(client *c) {
    // 1. 阻塞所有客户端
    if (rdbSave(server.rdb_filename, NULL) == C_OK) {
        addReply(c, shared.ok);
    } else {
        addReplyError(c, "Background save failed");
    }
}

int rdbSave(char *filename, rdbSaveInfo *rsi) {
    // 1. 创建临时文件
    snprintf(tmpfile, 256, "temp-%d.rdb", (int)getpid());
    fp = fopen(tmpfile, "w");
    
    // 2. 写入RDB头部
    rdbSaveRaw(fp, "REDIS", 5);
    rdbSaveLen(fp, RDB_VERSION);
    
    // 3. 遍历所有数据库
    for (j = 0; j < server.dbnum; j++) {
        redisDb *db = server.db + j;
        dict *d = db->dict;
        if (dictSize(d) == 0) continue;
        
        // 写入数据库选择器
        rdbSaveType(fp, RDB_OPCODE_SELECTDB);
        rdbSaveLen(fp, j);
        
        // 遍历数据库中的所有键
        dictIterator *di = dictGetSafeIterator(d);
        dictEntry *de;
        while((de = dictNext(di)) != NULL) {
            sds keystr = dictGetKey(de);
            robj key, *o = dictGetVal(de);
            long long expire;
            
            // 写入过期时间(如果有)
            expire = getExpire(db, &key);
            if (expire != -1) {
                rdbSaveType(fp, RDB_OPCODE_EXPIRETIME_MS);
                rdbSaveMillisecondTime(fp, expire);
            }
            
            // 写入键值对
            rdbSaveKeyValuePair(fp, &key, o, expire, now);
        }
        dictReleaseIterator(di);
    }
    
    // 4. 写入结束标记和校验和
    rdbSaveType(fp, RDB_OPCODE_EOF);
    rdbSaveRaw(fp, rdbver, 8);
    
    // 5. 原子性替换文件
    fclose(fp);
    rename(tmpfile, filename);
    
    return C_OK;
}

1.4.2 BGSAVE命令执行流程

// 伪代码:BGSAVE命令执行
void bgsaveCommand(client *c) {
    if (server.rdb_child_pid != -1) {
        addReplyError(c, "Background save already in progress");
        return;
    }
    
    rdbSaveBackground(server.rdb_filename, NULL);
    addReplyStatus(c, "Background saving started");
}

int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
    pid_t childpid;
    
    if ((childpid = fork()) == 0) {
        // 子进程执行实际的保存工作
        closeListeningSockets(0);
        redisSetProcTitle("redis-rdb-bgsave");
        retval = rdbSave(filename, rsi);
        exitFromChild((retval == C_OK) ? 0 : 1);
    } else {
        // 父进程记录子进程信息
        server.rdb_save_time_start = time(NULL);
        server.rdb_child_pid = childpid;
        server.rdb_child_type = RDB_CHILD_TYPE_DISK;
        updateDictResizePolicy();
        return C_OK;
    }
}

1.5 RDB数据编码格式

1.5.1 字符串编码

// 字符串保存格式
int rdbSaveStringObject(rio *rdb, robj *obj) {
    if (obj->encoding == OBJ_ENCODING_INT) {
        // 整数字符串:直接保存数值
        return rdbSaveLongLongAsStringObject(rdb, (long)obj->ptr);
    } else {
        // 普通字符串:长度 + 内容
        return rdbSaveRawString(rdb, obj->ptr, sdslen(obj->ptr));
    }
}

1.5.2 列表编码

// 列表保存格式
int rdbSaveListObject(rio *rdb, robj *o) {
    // 1. 保存列表长度
    if (rdbSaveLen(rdb, listTypeLength(o)) == -1) return -1;
    
    // 2. 遍历保存每个元素
    listTypeIterator *li = listTypeInitIterator(o, 0, LIST_TAIL);
    listTypeEntry entry;
    while (listTypeNext(li, &entry)) {
        if (rdbSaveStringObject(rdb, entry.value) == -1) {
            listTypeReleaseIterator(li);
            return -1;
        }
    }
    listTypeReleaseIterator(li);
    return 1;
}

1.5.3 哈希编码

// 哈希保存格式
int rdbSaveHashObject(rio *rdb, robj *o) {
    hashTypeIterator *hi;
    sds field, value;
    
    // 1. 保存哈希表大小
    if (rdbSaveLen(rdb, hashTypeLength(o)) == -1) return -1;
    
    // 2. 遍历保存每个字段和值
    hi = hashTypeInitIterator(o);
    while (hashTypeNext(hi) != C_ERR) {
        field = hashTypeCurrentFromHashTable(hi, OBJ_HASH_KEY);
        value = hashTypeCurrentFromHashTable(hi, OBJ_HASH_VALUE);
        
        if (rdbSaveRawString(rdb, field, sdslen(field)) == -1) return -1;
        if (rdbSaveRawString(rdb, value, sdslen(value)) == -1) return -1;
    }
    hashTypeReleaseIterator(hi);
    return 1;
}

1.6 RDB优缺点

优点:

  1. 性能高:RDB文件紧凑,加载速度快
  2. 备份方便:单文件,便于备份和传输
  3. 恢复快:相比AOF,RDB恢复速度更快
  4. 对性能影响小:通过fork子进程,不阻塞主进程

缺点:

  1. 数据丢失风险:两次备份间的数据可能丢失
  2. fork耗时:数据量大时fork可能阻塞服务器
  3. 兼容性问题:不同版本的RDB文件可能不兼容

2. AOF(Append Only File)持久化

2.1 基本概念

AOF持久化通过记录Redis服务器执行的所有写操作命令,并在服务器启动时重新执行这些命令来还原数据。

2.2 AOF工作原理

2.2.1 AOF三个步骤

  1. 命令追加(Append):将写命令追加到AOF缓冲区
  2. 文件同步(Sync):将AOF缓冲区内容同步到AOF文件
  3. 文件重写(Rewrite):定期重写AOF文件以压缩大小

2.2.2 AOF配置

# redis.conf AOF配置
appendonly yes                    # 启用AOF
appendfilename "appendonly.aof"   # AOF文件名
appendfsync everysec             # 同步策略

# AOF重写配置
auto-aof-rewrite-percentage 100  # 重写触发百分比
auto-aof-rewrite-min-size 64mb   # 重写最小文件大小

# 混合持久化(Redis 4.0+)
aof-use-rdb-preamble yes         # AOF文件以RDB格式开头

2.3 AOF同步策略

2.3.1 三种同步策略

// AOF同步策略实现
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)
        {
            goto try_fsync;
        } else {
            return;
        }
    }
    
    // 写入AOF缓冲区到文件
    latencyStartMonitor(latency);
    nwritten = aofWrite(server.aof_fd, server.aof_buf, sdslen(server.aof_buf));
    latencyEndMonitor(latency);
    
    // 根据策略决定是否同步
    if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
        // always:每次写入都同步
        latencyStartMonitor(latency);
        redis_fsync(server.aof_fd);
        latencyEndMonitor(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) {
        // everysec:每秒同步一次
        if (!sync_in_progress) {
            aof_background_fsync(server.aof_fd);
            server.aof_last_fsync = server.unixtime;
        }
    }
    // no:不主动同步,由操作系统决定
    
    // 清空AOF缓冲区
    sdsfree(server.aof_buf);
    server.aof_buf = sdsempty();
}
  1. always:每个写命令都立即同步到磁盘

    • 优点:数据安全性最高,最多丢失1个命令
    • 缺点:性能最低,每次写入都有磁盘IO
  2. everysec:每秒同步一次(默认)

    • 优点:性能和数据安全性的平衡
    • 缺点:最多丢失1秒的数据
  3. no:由操作系统决定何时同步

    • 优点:性能最高
    • 缺点:数据安全性最低,可能丢失较多数据

2.4 AOF重写机制

2.4.1 重写的必要性

随着时间推移,AOF文件会越来越大,包含大量冗余命令:

# 原始AOF可能包含
SET msg "hello"
SET msg "world"
SET msg "redis"

# 重写后只需要
SET msg "redis"

2.4.2 重写实现

// AOF重写主要函数
int rewriteAppendOnlyFile(char *filename) {
    dictIterator *di = NULL;
    dictEntry *de;
    rio aof;
    FILE *fp;
    char tmpfile[256];
    int j;
    long long now = mstime();
    char byte;
    size_t processed = 0;
    
    // 1. 创建临时文件
    snprintf(tmpfile, 256, "temp-rewriteaof-%d.aof", (int) getpid());
    fp = fopen(tmpfile, "w");
    
    // 2. 遍历所有数据库
    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 (o->type == OBJ_STRING) {
                if (rewriteStringObject(&aof, &key, o) == 0) goto werr;
            } else if (o->type == OBJ_LIST) {
                if (rewriteListObject(&aof, &key, o) == 0) goto werr;
            } else if (o->type == OBJ_SET) {
                if (rewriteSetObject(&aof, &key, o) == 0) goto werr;
            } else if (o->type == OBJ_ZSET) {
                if (rewriteSortedSetObject(&aof, &key, o) == 0) goto werr;
            } else if (o->type == OBJ_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;
            }
        }
        dictReleaseIterator(di);
        di = NULL;
    }
    
    // 3. 原子替换文件
    fclose(fp);
    if (rename(tmpfile, filename) == -1) {
        goto werr;
    }
    
    return C_OK;
}

2.4.3 后台重写

// 后台AOF重写
int rewriteAppendOnlyFileBackground(void) {
    pid_t childpid;
    long long start;
    
    if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;
    
    if (aofCreatePipes() != C_OK) return C_ERR;
    
    start = ustime();
    if ((childpid = fork()) == 0) {
        // 子进程
        char tmpfile[256];
        
        closeListeningSockets(0);
        redisSetProcTitle("redis-aof-rewrite");
        snprintf(tmpfile, 256, "temp-rewriteaof-bg-%d.aof", (int) getpid());
        if (rewriteAppendOnlyFile(tmpfile) == C_OK) {
            size_t private_dirty = zmalloc_get_private_dirty(-1);
            exitFromChild(0);
        } else {
            exitFromChild(1);
        }
    } else {
        // 父进程
        server.stat_fork_time = ustime()-start;
        server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024*1024*1024);
        
        server.aof_rewrite_time_start = time(NULL);
        server.aof_child_pid = childpid;
        updateDictResizePolicy();
        server.aof_selected_db = -1;
        replicationScriptCacheFlush();
        return C_OK;
    }
    return C_OK;
}

2.5 AOF载入过程

// AOF载入函数
int loadAppendOnlyFile(char *filename) {
    struct client *fakeClient;
    FILE *fp = fopen(filename,"r");
    struct redis_stat sb;
    int old_aof_state = server.aof_state;
    long loops = 0;
    off_t valid_up_to = 0;
    off_t valid_before_multi = 0;
    
    if (fp == NULL) return C_ERR;
    
    // 创建伪客户端
    fakeClient = createFakeClient();
    server.aof_state = AOF_OFF;
    
    // 逐行读取AOF文件
    while(1) {
        int argc, j;
        unsigned long len;
        robj **argv;
        char buf[128];
        sds argsds;
        struct redisCommand *cmd;
        
        // 读取命令
        if (fgets(buf, sizeof(buf), fp) == NULL) {
            if (feof(fp)) break;
            else goto readerr;
        }
        
        // 解析命令格式 *<argc>\r\n
        if (buf[0] != '*') goto fmterr;
        if (buf[1] == '\0') goto readerr;
        argc = atoi(buf+1);
        if (argc < 1) goto fmterr;
        
        // 读取参数
        argv = zmalloc(sizeof(robj*)*argc);
        fakeClient->argc = argc;
        fakeClient->argv = argv;
        
        for (j = 0; j < argc; j++) {
            // 读取参数长度 $<len>\r\n
            if (fgets(buf, sizeof(buf), fp) == NULL) goto readerr;
            if (buf[0] != '$') goto fmterr;
            len = strtol(buf+1, NULL, 10);
            
            // 读取参数内容
            argsds = sdsnewlen(SDS_NOINIT, len);
            if (len && fread(argsds, len, 1, fp) == 0) {
                sdsfree(argsds);
                goto readerr;
            }
            argv[j] = createObject(OBJ_STRING, argsds);
            
            // 读取\r\n
            if (fread(buf, 2, 1, fp) == 0) goto readerr;
        }
        
        // 执行命令
        cmd = lookupCommand(argv[0]->ptr);
        if (!cmd) {
            goto unknown_cmd;
        }
        
        // 调用命令处理函数
        fakeClient->cmd = cmd;
        cmd->proc(fakeClient);
        
        // 清理
        for (j = 0; j < argc; j++) decrRefCount(argv[j]);
        zfree(argv);
        fakeClient->argc = 0;
        fakeClient->argv = NULL;
        
        loops++;
        if (loops % 1000 == 0) {
            loadingProgress(ftello(fp));
            processEventsWhileBlocked();
        }
    }
    
    // 载入完成
    freeFakeClient(fakeClient);
    server.aof_state = old_aof_state;
    fclose(fp);
    return C_OK;
}

2.6 AOF优缺点

优点:

  1. 数据安全性高:可配置不同的同步策略
  2. 日志文件可读:AOF文件是纯文本,便于分析和修复
  3. 重写机制:可以压缩文件大小
  4. 兼容性好:格式稳定,向后兼容

缺点:

  1. 文件体积大:相比RDB,AOF文件通常更大
  2. 恢复速度慢:需要重放所有命令
  3. 性能开销:每次写操作都需要记录日志

3. 混合持久化(Redis 4.0+)

3.1 基本概念

混合持久化结合了RDB和AOF的优点,在AOF重写时将当前数据以RDB格式写入AOF文件开头,后续的增量数据以AOF格式追加。

3.2 混合持久化文件格式

+-------------------+
| RDB格式的数据快照  |  ← 重写时的完整数据
+-------------------+
| AOF格式的增量数据  |  ← 重写后的新命令
+-------------------+

3.3 混合持久化实现

// 混合持久化重写
int rewriteAppendOnlyFileBackground(void) {
    // ... 前面的代码相同
    
    if ((childpid = fork()) == 0) {
        char tmpfile[256];
        
        snprintf(tmpfile, 256, "temp-rewriteaof-bg-%d.aof", (int) getpid());
        
        if (server.aof_use_rdb_preamble) {
            // 混合模式:先写RDB格式
            int error;
            if (rdbSaveRio(&aof, &error, RDB_SAVE_AOF_PREAMBLE, NULL) == C_ERR) {
                errno = error;
                goto werr;
            }
        } else {
            // 传统模式:纯AOF格式
            if (rewriteAppendOnlyFile(tmpfile) == C_ERR) goto werr;
        }
        
        exitFromChild(0);
    }
    // ... 父进程处理
}

3.4 混合持久化优势

  1. 快速恢复:RDB部分可以快速加载
  2. 数据完整性:AOF部分保证数据不丢失
  3. 文件相对较小:比纯AOF文件小
  4. 向后兼容:可以关闭混合模式

4. 持久化策略选择

4.1 不同场景的选择

4.1.1 高可用性要求

# 配置示例:数据安全性优先
appendonly yes
appendfsync everysec
save 900 1
save 300 10
save 60 10000
aof-use-rdb-preamble yes

4.1.2 高性能要求

# 配置示例:性能优先
appendonly no
save 900 1
save 300 10
save 60 10000

4.1.3 平衡模式

# 配置示例:性能与安全性平衡
appendonly yes
appendfsync everysec
save 300 10
save 60 10000
aof-use-rdb-preamble yes
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

4.2 持久化策略对比

特性 RDB AOF 混合持久化
文件大小 中等
恢复速度 中等偏快
数据安全性
性能影响 中等 中等
兼容性 Redis 4.0+

4.3 推荐配置

4.3.1 生产环境推荐配置

# redis.conf 生产环境配置
# RDB配置
save 300 10
save 60 10000
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb

# AOF配置
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

# 混合持久化
aof-use-rdb-preamble yes

# 目录配置
dir /var/lib/redis/

4.3.2 开发环境配置

# 开发环境:快速启动,数据丢失不敏感
appendonly no
save 60 1000
save 300 100
save 900 1

5. 持久化监控和运维

5.1 监控指标

# 获取持久化相关信息
INFO persistence

# 关键指标
rdb_last_save_time          # 最后一次RDB保存时间
rdb_last_bgsave_status      # 最后一次后台保存状态
rdb_last_bgsave_time_sec    # 最后一次后台保存耗时
aof_enabled                 # AOF是否启用
aof_rewrite_in_progress     # AOF是否正在重写
aof_last_rewrite_time_sec   # 最后一次AOF重写耗时
aof_current_size            # 当前AOF文件大小
aof_base_size              # AOF重写时的基础大小

5.2 常见问题和解决方案

5.2.1 RDB备份失败

# 检查磁盘空间
df -h

# 检查权限
ls -la /var/lib/redis/

# 检查错误日志
tail -f /var/log/redis/redis-server.log

# 手动触发备份测试
redis-cli BGSAVE

5.2.2 AOF文件损坏

# 检查AOF文件
redis-check-aof appendonly.aof

# 修复AOF文件
redis-check-aof --fix appendonly.aof

# 截断损坏部分
redis-check-aof --truncate-to-timestamp 1609459200 appendonly.aof

5.2.3 AOF文件过大

# 手动触发重写
redis-cli BGREWRITEAOF

# 调整重写参数
CONFIG SET auto-aof-rewrite-percentage 50
CONFIG SET auto-aof-rewrite-min-size 32mb

5.3 备份策略

5.3.1 定期备份脚本

#!/bin/bash
# redis-backup.sh

BACKUP_DIR="/backup/redis"
DATE=$(date +%Y%m%d_%H%M%S)
REDIS_DIR="/var/lib/redis"

# 创建备份目录
mkdir -p "$BACKUP_DIR/$DATE"

# 触发RDB备份
redis-cli BGSAVE

# 等待备份完成
while [ $(redis-cli LASTSAVE) -eq $(redis-cli LASTSAVE) ]; do
    sleep 1
done

# 复制RDB文件
cp "$REDIS_DIR/dump.rdb" "$BACKUP_DIR/$DATE/"

# 复制AOF文件
cp "$REDIS_DIR/appendonly.aof" "$BACKUP_DIR/$DATE/"

# 压缩备份
tar -czf "$BACKUP_DIR/redis_backup_$DATE.tar.gz" -C "$BACKUP_DIR" "$DATE"

# 删除原始备份目录
rm -rf "$BACKUP_DIR/$DATE"

# 清理7天前的备份
find "$BACKUP_DIR" -name "redis_backup_*.tar.gz" -mtime +7 -delete

echo "Redis backup completed: redis_backup_$DATE.tar.gz"

5.3.2 远程备份

#!/bin/bash
# 备份到远程服务器
rsync -av --delete /var/lib/redis/ backup-server:/backup/redis/$(hostname)/

6. 最佳实践总结

6.1 持久化配置原则

  1. 根据业务需求选择:数据敏感度决定持久化策略
  2. 性能与安全平衡:不要过度配置影响性能
  3. 定期测试恢复:确保备份文件可用
  4. 监控磁盘空间:避免磁盘满导致备份失败
  5. 设置告警机制:及时发现持久化问题

6.2 常见配置模式

# 高安全性模式
appendonly yes
appendfsync always
save ""  # 禁用RDB自动保存

# 高性能模式  
appendonly no
save 900 1

# 平衡模式(推荐)
appendonly yes
appendfsync everysec
save 300 10
aof-use-rdb-preamble yes

Redis的持久化机制为数据安全提供了多重保障,正确配置和使用这些机制是保证Redis数据可靠性的关键。选择合适的持久化策略需要综合考虑业务需求、性能要求和运维成本。

posted @ 2025-08-18 14:46  MadLongTom  阅读(32)  评论(0)    收藏  举报