Redis AOF持久化机制的工作原理与实现
2022-08-265.9k 阅读
Redis AOF持久化机制概述
Redis作为一款高性能的键值对数据库,为了保证数据在断电或系统崩溃等情况下不丢失,提供了两种持久化机制:RDB(Redis Database)和AOF(Append - Only - File)。AOF持久化机制以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式追加到文件的末尾。
当Redis重启时,会重新执行AOF文件中的命令来恢复数据。AOF的优点在于数据的完整性和一致性更好,因为即使发生故障,也只会丢失故障发生前未写入AOF文件的部分数据,而RDB可能会丢失最近一段时间的数据。但AOF文件通常会比RDB文件大,因为它记录的是每一个写操作。
AOF工作原理
- 命令追加(Append)
- Redis服务器执行写命令时,会将该命令以文本协议的格式追加到AOF缓冲区中。例如,执行
SET key value
命令,该命令会以类似*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n
的格式追加到缓冲区。这种格式是Redis的文本协议格式,*
后面的数字表示参数的个数,$
后面的数字表示每个参数的长度。 - 这样设计的好处是,文本协议格式可读性强,方便人工查看和分析AOF文件,同时也易于解析和重放。
- Redis服务器执行写命令时,会将该命令以文本协议的格式追加到AOF缓冲区中。例如,执行
- 文件写入(Write)和同步(Sync)
- AOF缓冲区中的内容会根据配置的策略定期写入到AOF文件中。Redis提供了三种写入和同步策略,通过
appendfsync
配置项进行设置:- always:每次执行写命令后,都立即将AOF缓冲区的内容写入并同步到AOF文件。这种策略数据安全性最高,因为一旦命令执行成功,数据就已经持久化到磁盘,但由于每次都进行磁盘I/O操作,性能相对较低。
- everysec:每秒将AOF缓冲区的内容写入并同步到AOF文件。这是默认的配置策略,在性能和数据安全性之间做了较好的平衡。每秒执行一次磁盘I/O操作,在系统崩溃的情况下,最多可能丢失1秒的数据。
- no:由操作系统决定何时将AOF缓冲区的内容写入和同步到AOF文件。这种策略性能最高,因为Redis不主动进行磁盘I/O操作,而是依赖操作系统的缓冲区管理机制。但数据安全性最低,在系统崩溃时可能丢失大量未同步的数据。
- AOF缓冲区中的内容会根据配置的策略定期写入到AOF文件中。Redis提供了三种写入和同步策略,通过
- 文件重写(Rewrite)
- 随着Redis不断处理写操作,AOF文件会越来越大。为了避免AOF文件过大导致占用过多磁盘空间以及重放AOF文件恢复数据时性能下降的问题,Redis提供了AOF文件重写机制。
- AOF重写并不是对原AOF文件进行直接修改,而是通过读取当前数据库中的所有键值对,然后用一条命令(例如
SET
命令对于普通键值对,HSET
命令对于哈希类型等)来表示每个键值对,生成一个新的AOF文件。例如,如果原AOF文件中有多次对同一个键的INCR
操作,重写后只会保留最终的结果,用一条SET
命令表示。 - 重写过程分为手动重写和自动重写:
- 手动重写:可以通过执行
BGREWRITEAOF
命令来手动触发AOF重写。该命令会在后台启动一个子进程进行重写操作,不会影响主进程的正常工作。 - 自动重写:通过配置
auto - aof - rewrite - min - size
和auto - aof - rewrite - percentage
两个参数来实现自动重写。auto - aof - rewrite - min - size
表示AOF文件最小达到多大时才会触发自动重写,默认是64MB。auto - aof - rewrite - percentage
表示当前AOF文件大小相比于上一次重写后的大小增长的百分比,当增长的百分比超过该值且AOF文件大小大于auto - aof - rewrite - min - size
时,会自动触发重写。例如,auto - aof - rewrite - percentage
设置为100,auto - aof - rewrite - min - size
设置为64MB,当AOF文件大小达到128MB(64MB的200%,增长了100%)时,会自动触发重写。
- 手动重写:可以通过执行
AOF实现细节
- AOF缓冲区管理
- 在Redis的代码实现中,AOF缓冲区是一个动态分配的内存空间。在
server.h
文件中,定义了struct redisServer
结构体,其中包含了AOF相关的字段:
- 在Redis的代码实现中,AOF缓冲区是一个动态分配的内存空间。在
struct redisServer {
// AOF缓冲区
sds aof_buf;
// AOF重写缓冲区
sds aof_rewrite_buf;
// AOF写入策略
int aof_fsync; /* Appendfsync policy */
// 上次AOF重写时间
time_t aof_last_rewrite_time;
// 更多其他字段...
};
- 当执行写命令时,会调用
feedAppendOnlyFile
函数将命令追加到AOF缓冲区。例如,对于SET
命令,相关代码片段如下:
void feedAppendOnlyFile(redisClient *c) {
sds cmd = sdsnew("*3\r\n$3\r\nSET\r\n$%zu\r\n%s\r\n$%zu\r\n%s\r\n",
sdslen(c->argv[1]), c->argv[1]->ptr,
sdslen(c->argv[2]), c->argv[2]->ptr);
sdscat(server.aof_buf, cmd);
sdsfree(cmd);
}
- 这段代码将
SET
命令及其参数按照Redis文本协议格式组装成字符串,然后追加到AOF缓冲区server.aof_buf
中。
- 文件写入和同步
- 写入和同步操作由
flushAppendOnlyFile
函数负责。根据不同的appendfsync
策略,会有不同的处理逻辑。 - 对于
always
策略:
- 写入和同步操作由
int flushAppendOnlyFile(int force) {
if (server.aof_fsync == AOF_FSYNC_ALWAYS || force) {
if (write(ae_fd(server.aof_fd), server.aof_buf, sdslen(server.aof_buf)) == (ssize_t)-1) {
serverLog(LL_WARNING, "Write to AOF file error: %s", strerror(errno));
return 0;
}
if (syncWrite(ae_fd(server.aof_fd)) == -1) {
serverLog(LL_WARNING, "fsync() to AOF file error: %s", strerror(errno));
return 0;
}
sdsrange(server.aof_buf, 0, -1);
return 1;
}
// 其他策略处理逻辑...
}
- 在
always
策略下,首先调用write
函数将AOF缓冲区的内容写入到AOF文件描述符server.aof_fd
,如果写入失败,记录警告日志并返回0。然后调用syncWrite
函数(实际是fsync
函数的封装)将文件数据同步到磁盘,如果同步失败,同样记录警告日志并返回0。最后清空AOF缓冲区。 - 对于
everysec
策略,会在后台线程中每秒调用一次flushAppendOnlyFile
函数,并且在一些特殊情况下(如AOF缓冲区达到一定大小等)也会调用。 - 对于
no
策略,write
函数会将数据写入操作系统的缓冲区,而Redis不会主动调用fsync
进行同步,由操作系统自行决定何时将缓冲区数据写入磁盘。
- AOF文件重写
- 手动重写:当执行
BGREWRITEAOF
命令时,主进程会调用rewriteAppendOnlyFileBackground
函数。该函数会创建一个子进程,子进程负责进行实际的重写操作。
- 手动重写:当执行
void rewriteAppendOnlyFileBackground(void) {
pid_t childpid;
long long start;
if (server.aof_child_pid != -1) return;
start = ustime();
childpid = fork();
if (childpid == 0) {
// 子进程重写逻辑
rewriteAppendOnlyFile(0);
_exit(0);
} else if (childpid > 0) {
// 主进程更新相关状态
server.aof_child_pid = childpid;
server.aof_rewrite_scheduled = 0;
serverLog(LL_NOTICE, "Background AOF rewrite started by pid %d", childpid);
} else {
serverLog(LL_WARNING, "Can't fork() to start AOF rewrite: %s", strerror(errno));
}
}
- 子进程通过调用
rewriteAppendOnlyFile
函数来读取当前数据库中的所有键值对,并生成新的AOF文件。在重写过程中,子进程会遍历数据库中的每个键,根据键的类型生成相应的重写命令。例如,对于哈希类型的键:
void rewriteHashObject(rio *aof, robj *o) {
dict *ht = o->ptr;
dictIterator *di = dictGetSafeIterator(ht);
dictEntry *de;
addHashCommand(aof, o, "HSET");
while ((de = dictNext(di)) != NULL) {
sds field = dictGetKey(de);
robj *value = dictGetVal(de);
addHashField(aof, field, value);
}
dictReleaseIterator(di);
}
- 这段代码首先添加
HSET
命令的前缀,然后遍历哈希对象的每个字段和值,添加相应的字段和值到重写的AOF文件中。 - 自动重写:在Redis的主循环中,每次处理完命令后会调用
serverCron
函数,该函数会检查是否满足自动重写的条件。
void serverCron(void) {
// 其他逻辑...
if (server.aof_state == AOF_ON &&
server.aof_rewrite_perc &&
server.aof_current_size > server.aof_rewrite_min_size &&
(server.aof_current_size - server.aof_base_size) > (server.aof_base_size * server.aof_rewrite_perc / 100)) {
rewriteAppendOnlyFileBackground();
}
// 其他逻辑...
}
- 如果AOF处于开启状态,并且满足
auto - aof - rewrite - percentage
和auto - aof - rewrite - min - size
配置的条件,就会触发自动重写。
AOF配置与优化
- 配置参数说明
- appendonly:是否开启AOF持久化,默认值为
no
。如果要开启AOF,将其设置为yes
。 - appendfsync:前面提到的写入和同步策略,可选值为
always
、everysec
、no
。 - no - appendfsync - on - rewrite:在AOF重写期间,是否暂停
appendfsync
。默认值为no
,即不暂停。如果设置为yes
,在重写期间,新的写命令会先写入AOF重写缓冲区,重写完成后再合并到新的AOF文件中,这样可以避免在重写期间频繁的磁盘I/O操作影响重写性能,但可能会在重写期间丢失部分数据。 - auto - aof - rewrite - min - size:AOF文件最小达到多大时才会触发自动重写,默认值为64MB。可以根据实际情况调整该值,如果服务器数据量较小,可以适当减小该值;如果数据量较大且增长缓慢,可以适当增大该值。
- auto - aof - rewrite - percentage:当前AOF文件大小相比于上一次重写后的大小增长的百分比,默认值为100。可以根据AOF文件的增长趋势调整该值,如果AOF文件增长较快,可以适当增大该值,减少重写频率;如果增长较慢,可以适当减小该值,及时重写AOF文件。
- appendonly:是否开启AOF持久化,默认值为
- 优化建议
- 选择合适的写入策略:根据应用对数据安全性和性能的要求选择合适的
appendfsync
策略。如果应用对数据安全性要求极高,如金融交易系统,建议使用always
策略;如果对性能要求较高,且能接受一定的数据丢失风险,如一般的缓存应用,可以使用everysec
或no
策略。 - 合理设置重写参数:通过监控AOF文件的增长情况,合理调整
auto - aof - rewrite - min - size
和auto - aof - rewrite - percentage
参数。避免频繁重写导致性能下降,同时也要防止AOF文件过大影响恢复时间。 - 定期清理AOF文件:虽然AOF重写机制会自动清理冗余命令,但在某些情况下,如大量过期键的删除操作,可能会导致AOF文件中仍然存在一些无用的命令。可以定期手动执行
BGREWRITEAOF
命令,确保AOF文件的紧凑性。 - 监控AOF相关指标:使用Redis的
INFO
命令监控AOF相关指标,如aof_current_size
(当前AOF文件大小)、aof_base_size
(上一次重写后的AOF文件大小)、aof_last_rewrite_time_sec
(上一次重写花费的时间)等。根据这些指标来调整AOF的配置和优化策略。
- 选择合适的写入策略:根据应用对数据安全性和性能的要求选择合适的
AOF故障恢复
- 故障场景
- 当Redis服务器发生故障(如断电、系统崩溃等)后重启时,需要通过AOF文件来恢复数据。但在某些情况下,AOF文件可能会损坏,例如在写入过程中系统突然断电,导致AOF文件内容不完整。
- 恢复过程
- 检查AOF文件完整性:Redis在启动时会首先检查AOF文件的完整性。如果发现AOF文件损坏,会尝试进行修复。修复过程是通过
redis - check - aof
工具来实现的。该工具会读取AOF文件,尝试解析其中的命令,跳过无法解析的部分,并生成一个新的、完整的AOF文件。 - 重放AOF文件:如果AOF文件完整或者经过修复后完整,Redis会按照AOF文件中的命令顺序依次重放。从文件开头开始,解析每个命令并执行,从而恢复数据库到故障前的状态。在重放过程中,Redis会重新构建数据库中的键值对,对于过期键,会根据AOF文件中的记录进行相应的处理(如删除过期键)。
- 检查AOF文件完整性:Redis在启动时会首先检查AOF文件的完整性。如果发现AOF文件损坏,会尝试进行修复。修复过程是通过
AOF与RDB对比
- 数据完整性
- AOF通过记录每一个写操作,在故障恢复时可以保证数据的完整性更高,最多只会丢失最近一次写入但未同步到AOF文件的数据。而RDB是定期快照,可能会丢失最近一次快照之后到故障发生期间的所有写操作数据。
- 文件大小
- AOF文件由于记录了所有的写操作,通常会比RDB文件大。特别是在执行大量写操作的情况下,AOF文件会快速增长。RDB文件是对整个数据库的快照,文件大小相对较小,尤其是在数据量较大且数据变化相对稳定的情况下。
- 恢复速度
- RDB恢复数据时,是直接加载快照文件,速度相对较快。因为它只需要将文件中的数据一次性加载到内存中。而AOF恢复数据时,需要重放文件中的所有写命令,在AOF文件较大时,恢复速度会较慢。
- 性能影响
- AOF的
always
策略会对性能有较大影响,因为每次写操作都要进行磁盘I/O。everysec
策略在性能和数据安全性之间做了较好平衡。RDB由于是定期快照,对正常读写操作的性能影响相对较小,只有在进行快照时可能会对性能有一定影响(取决于快照方式,如SAVE
命令会阻塞主进程,BGSAVE
命令会在后台进行)。
- AOF的
在实际应用中,可以根据具体需求选择使用AOF、RDB或者两者结合的方式。如果对数据完整性要求极高,对性能要求不是特别苛刻,可以优先选择AOF;如果对恢复速度要求较高,对数据完整性要求相对较低,可以优先选择RDB;如果希望兼顾数据完整性和恢复速度,可以同时开启AOF和RDB。
通过深入了解Redis AOF持久化机制的工作原理、实现细节、配置优化以及与RDB的对比,开发人员可以更好地根据应用场景来选择和使用Redis的持久化功能,确保数据的安全性和可靠性。