Redis AOF 重写
+--------------------------------------------------------+
| Redis主进程 |
| |
| +---------------+ 写命令 +----------------------+ |
| | |----------->| | |
| | 命令处理程序 | | AOF缓冲区(aof_buf) | |
| | | | | |
| +---------------+ +----------------------+ |
| | | |
| | 写命令 | fsync |
| v v |
| +---------------+ +----------------------+ |
| | | | | |
| | AOF重写缓冲区 | | AOF文件 | |
| | (aof_rewrite | | | |
| | _buf) | +----------------------+ |
| +---------------+ |
| | |
| | fork |
| v |
| +---------------+ |
| | 发送信号给 | |
| | 父进程 | |
| +---------------+ |
| |
+--------------------------------------------------------+
|
| fork
v
+--------------------------------------------------------+
| AOF重写子进程 |
| |
| +---------------+ 读取 +----------------------+ |
| | |<----------| | |
| | AOF重写程序 | | 内存数据库副本 | |
| | (rewrite | | | |
| | AppendOnly | | | |
| | File) |---------->| | |
| | | 写入 | | |
| +---------------+ +----------------------+ |
| | |
| | 写入 |
| v |
| +------------------+ |
| | | |
| | 临时AOF文件 | |
| | (temp-rewriteaof)| |
| | | |
| +------------------+ |
| |
+--------------------------------------------------------+
完成后:
1. 子进程通知父进程
2. 父进程将AOF重写缓冲区内容追加到新AOF文件
3. 父进程原子性地重命名临时文件,替换旧AOF文件
AOF重写的必要性
AOF(Append Only File)持久化通过记录所有修改数据库的写命令来保存数据状态。随着Redis运行时间的增长,AOF文件会不断膨胀,导致以下问题:
- 磁盘空间占用过大:大量冗余命令会占用不必要的存储空间
- 恢复速度变慢:Redis重启时需要执行AOF文件中的所有命令,文件越大恢复越慢
- 潜在的性能影响:过大的AOF文件可能影响Redis整体性能
AOF重写的实现原理
尽管名为”重写”,Redis并不会读取、分析现有的AOF文件,而是通过以下步骤创建一个新的AOF文件:
- 读取当前数据库状态:直接从内存中的数据库读取所有键值对
- 生成恢复命令:为每个键值对生成能够恢复其当前状态的最少命令
- 优化命令结构:尽可能使用能处理多个元素的命令(如RPUSH、SADD、ZADD等)
- 原子性替换:通过rename()系统调用原子性地用新文件替换旧文件
这种实现方式确保了新AOF文件的紧凑性,且包含恢复数据库状态所需的最小命令集。
重写过程的核心代码
rewriteAppendOnlyFile
函数是AOF重写的核心实现:
int rewriteAppendOnlyFile(char *filename) {
// 创建临时文件
snprintf(tmpfile, 256, "temp-rewriteaof-%d.aof", (int) getpid());
fp = fopen(tmpfile, "w");
// 遍历所有数据库
for (j = 0; j < server.dbnum; j++) {
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;
// 遍历数据库中的所有键
while((de = dictNext(di)) != NULL) {
// 获取键、值和过期时间
keystr = dictGetKey(de);
o = dictGetVal(de);
expiretime = getExpire(db, &key);
// 跳过已过期的键
if (expiretime != -1 && expiretime < now) continue;
// 根据值类型生成相应命令
if (o->type == REDIS_STRING) {
// 生成SET命令
// ...
} else if (o->type == REDIS_LIST) {
// 生成LIST相关命令
// ...
}
// 其他数据类型...
// 保存过期时间
if (expiretime != -1) {
// 生成PEXPIREAT命令
// ...
}
}
}
// 原子性替换旧文件
if (rename(tmpfile, filename) == -1) {
// 处理错误...
return REDIS_ERR;
}
return REDIS_OK;
}
后台重写机制
为了避免阻塞主进程,Redis采用子进程进行AOF重写:
子进程重写:
- 不阻塞主服务:子进程进行重写时,父进程继续处理客户端请求
- 内存安全:子进程拥有父进程数据的副本,无需加锁即可安全访问
- 写时复制:由于Linux的写时复制(Copy-On-Write)机制,子进程创建时不会实际复制所有内存
数据一致性问题
子进程重写期间,父进程仍在处理写命令,可能导致新AOF文件与当前数据库状态不一致。
AOF重写缓冲区
Redis巧妙地解决了这个问题,具体流程如下:
- 创建重写缓冲区:Redis创建子进程开始AOF重写时,同时创建一个AOF重写缓冲区
- 双向写入:每次执行写命令后,父进程同时将命令写入:
- 普通AOF缓冲区(用于常规AOF追加)
- AOF重写缓冲区(用于后续合并到新AOF文件)
- 合并过程:当子进程完成重写后:
- 向父进程发送信号
- 父进程调用信号处理函数
backgroundRewriteDoneHandler
- 将AOF重写缓冲区中积累的命令追加到新AOF文件末尾
- 原子性地重命名新AOF文件,替换旧文件
重写触发机制
Redis提供了两种触发AOF重写的方式:
- 手动触发:通过
BGREWRITEAOF
命令 - 自动触发:基于配置参数:
auto-aof-rewrite-percentage
:当前AOF文件大小相对上次重写时的增长百分比auto-aof-rewrite-min-size
:触发重写所需的最小AOF文件大小
例如,设置auto-aof-rewrite-percentage 100
和auto-aof-rewrite-min-size 64mb
,表示当AOF文件体积比上次重写后增长100%且超过64MB时,自动触发重写。
重写过程中的性能影响
尽管AOF重写设计为尽量减少对主服务的影响,但仍可能出现以下性能问题:
- fork时的阻塞:创建子进程时,父进程会短暂暂停
- 内存占用增加:子进程会复制父进程的页表,写时复制机制可能导致内存用量增加
- 磁盘I/O压力:重写过程涉及大量磁盘写入操作
最佳实践
- 合理配置自动重写参数:根据实际应用场景调整触发条件
- 选择合适的时间执行手动重写:在业务低峰期执行
BGREWRITEAOF
- 监控重写过程:关注
INFO
命令输出中的AOF相关指标 - 考虑硬件配置:使用SSD存储和足够的内存以减轻重写压力
- 合理使用RDB和AOF:在某些场景下,可以考虑RDB+AOF混合使用
AOF重写是Redis持久化的重要组成部分,通过优化AOF文件大小,既保证了数据安全性,又提高了系统恢复速度,是Redis长期稳定运行的关键机制之一。