Redis AOF持久化机制
1. AOF持久化介绍
Redis中支持RDB
和AOF
这两种持久化机制,目的都是避免因进程退出,造成的数据丢失问题。
- RDB持久化:把当前进程数据生成时间点快照(point-in-time snapshot)保存到硬盘的过程,避免数据意外丢失。
- AOF持久化:以独立日志的方式记录每次写命令,重启时在重新执行AOF文件中的命令达到恢复数据的目的。
AOF
的使用:在redis.conf
配置文件中,将appendonly
设置为yes
,默认的为no
。
2. AOF持久化的实现
AOF持久化所有注释:Redis AOF持久化机制源码注释
2.1 命令写入磁盘
2.1.1 命令写入缓冲区
- 命令问什么先写入缓冲区
由于Redis
是单线程响应命令,所以每次写AOF
文件都直接追加到硬盘中,那么写入的性能完全取决于硬盘的负载,所以Redis
会将命令写入到缓冲区中,然后执行文件同步操作,再将缓冲区内容同步到磁盘中,这样就很好的保持了高性能。
那么缓冲区定义如下,它是一个简单动态字符串(sds),因此很好的和C语言的字符串想兼容。
struct redisServer {
// AOF缓冲区,在进入事件loop之前写入
sds aof_buf; /* AOF buffer, written before entering the event loop */
};
- 命令的写入格式
Redis命令写入的内容直接就是文本协议格式,例如:
*2\r\n$6\r\nSELECT\r\n$1\r\n0\r\n*5\r\n$4\r\nSADD\r\n$3\r\nkey\r\n$2\r\nm3\r\n$2\r\nm2\r\n$2\r\nm1\r\n
根据协议内容,大致可以得出:这是第0
号数据库,执行了一个SADD key m1 m2 m3
命令。这就是Redis采用文件协议格式的原因之一,文本协议具有很高的可读性,可以直接进行修改。而且,文本协议还具有很好的兼容性,而且协议采用了\r\n
换行符,所以每次写入命令只需执行追加操作。
既然是追加操作,因此,源码中的函数名字也是如此,catAppendOnlyGenericCommand()
函数实现了追加命令到缓冲区中,从这个函数中,可以清楚的看到协议是如何生成的。
// 根据传入的命令和命令参数,将他们还原成协议格式
sds catAppendOnlyGenericCommand(sds dst, int argc, robj **argv) {
char buf[32];
int len, j;
robj *o;
// 格式:"*<argc>\r\n"
buf[0] = '*';
len = 1+ll2string(buf+1,sizeof(buf)-1,argc);
buf[len++] = '\r';
buf[len++] = '\n';
// 拼接到dst的后面
dst = sdscatlen(dst,buf,len);
// 遍历所有的参数,建立命令的格式:$<command_len>\r\n<command>\r\n
for (j = 0; j < argc; j++) {
o = getDecodedObject(argv[j]); //解码成字符串对象
buf[0] = '$';
len = 1+ll2string(buf+1,sizeof(buf)-1,sdslen(o->ptr));
buf[len++] = '\r';
buf[len++] = '\n';
dst = sdscatlen(dst,buf,len);
dst = sdscatlen(dst,o->ptr,sdslen(o->ptr));
dst = sdscatlen(dst,"\r\n",2);
decrRefCount(o);
}
return dst; //返回还原后的协议内容
}
这个函数只是追加一个普通的键,然而一个过期命令的键,需要全部转换为PEXPIREAT
,因为必须将相对时间设置为绝对时间,否则还原数据库时,就无法得知该键是否过期,Redis的catAppendOnlyExpireAtCommand()
函数实现了这个功能。
// 用sds表示一个 PEXPIREAT 命令,seconds为生存时间,cmd为指定转换的指令
// 这个函数用来转换 EXPIRE and PEXPIRE 命令成 PEXPIREAT ,以便在AOF时,时间总是一个绝对值
sds catAppendOnlyExpireAtCommand(sds buf, struct redisCommand *cmd, robj *key, robj *seconds) {
long long when;
robj *argv[3];
/* Make sure we can use strtoll */
// 解码成字符串对象,以便使用strtoll函数
seconds = getDecodedObject(seconds);
// 取出过期值,long long类型
when = strtoll(seconds->ptr,NULL,10);
/* Convert argument into milliseconds for EXPIRE, SETEX, EXPIREAT */
// 将 EXPIRE, SETEX, EXPIREAT 参数的秒转换成毫秒
if (cmd->proc == expireCommand || cmd->proc == setexCommand ||
cmd->proc == expireatCommand)
{
when *= 1000;
}
/* Convert into absolute time for EXPIRE, PEXPIRE, SETEX, PSETEX */
// 将 EXPIRE, PEXPIRE, SETEX, PSETEX 命令的参数,从相对时间设置为绝对时间
if (cmd->proc == expireCommand || cmd->proc == pexpireCommand ||
cmd->proc == setexCommand || cmd->proc == psetexCommand)
{
when += mstime();
}
decrRefCount(seconds);
// 创建一个 PEXPIREAT 命令对象
argv[0] = createStringObject("PEXPIREAT",9);
argv[1] = key;
argv[2] = createStringObjectFromLongLong(when);
// 将命令还原成协议格式,追加到buf
buf = catAppendOnlyGenericCommand(buf, 3, argv);
decrRefCount(argv[0]);
decrRefCount(argv[2]);
// 返回buf
return buf;
}
那么,这两个函数都是实现的底层功能,因此他们都被feedAppendOnlyFile()
函数最终调用。
这个函数,创建一个空的简单动态字符串(sds),将当前所有追加命令操作都追加到这个sds中,最终将这个sds追加到server.aof_buf
。。还有就是,这个函数在写入键之前,需要显式的写入一个SELECT
命令,以正确的将所有键还原到正确的数据库中。
// 将命令追加到AOF文件中
void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int argc) {
sds buf = sdsempty(); //设置一个空sds
robj *tmpargv[3];
// 使用SELECT命令,显式的设置当前数据库
if (dictid != server.aof_selected_db) {
char seldb[64];
snprintf(seldb,sizeof(seldb),"%d",dictid);
// 构造SELECT命令的协议格式
buf = sdscatprintf(buf,"*2\r\n$6\r\nSELECT\r\n$%lu\r\n%s\r\n",
(unsigned long)strlen(seldb),seldb);
// 执行AOF时,当前的数据库ID
server.aof_selected_db = dictid;
}
// 如果是 EXPIRE/PEXPIRE/EXPIREAT 三个命令,则要转换成 PEXPIREAT 命令
if (cmd->proc == expireCommand || cmd->proc == pexpireCommand ||
cmd->proc == expireatCommand) {
/* Translate EXPIRE/PEXPIRE/EXPIREAT into PEXPIREAT */
buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);
// 如果是 SETEX/PSETEX 命令,则转换成 SET and PEXPIREAT
} else if (cmd->proc == setexCommand || cmd->proc == psetexCommand) {
/* Translate SETEX/PSETEX to SET and PEXPIREAT */
// SETEX key seconds value
// 构建SET命令对象
tmpargv[0] = createStringObject("SET",3);
tmpargv[1] = argv[1];
tmpargv[2] = argv[3];
// 将SET命令按协议格式追加到buf中
buf = catAppendOnlyGenericCommand(buf,3,tmpargv);
decrRefCount(tmpargv[0]);
// 将SETEX/PSETEX命令和键对象按协议格式追加到buf中
buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);
// 其他命令直接按协议格式转换,然后追加到buf中
} else {
buf = catAppendOnlyGenericCommand(buf,argc,argv);
}
// 如果正在进行AOF,则将命令追加到AOF的缓存中,在重新进入事件循环之前,这些命令会被冲洗到磁盘上,并向client回复
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);
}
2.1.2 缓冲区同步到文件
既然缓冲区提供了高性能的保障,那么缓冲区中的数据安全问题如何解决呢?只要数据存在于缓冲区,那么就有丢失的危险。那么,如果控制同步的频率呢?Redis中给出了3中缓冲区同步文件的策略。
可配置值 | 说明 |
---|---|
AOF_FSYNC_ALWAYS | 命令写入aof_buf后调用系统fsync和操作同步到AOF文件,fsync完成后进程程返回 |
AOF_FSYNC_EVERYSEC | 命令写入aof_buf后调用系统write操作,write完成后线程返回。fsync同步文件操作由进程每秒调用一次 |
AOF_FSYNC_NO | 命令写入aof_buf后调用系统write操作,不对AOF文件做fsync同步,同步硬盘由操作由操作系统负责 |
我们来了解一下,write和fsync操作,在系统中都做了哪些事:
- write操作:会触发延迟写(delayed write)机制。Linux在内核提供页缓冲区用来提高IO性能,因此,write操作在将数据写入操作系统的缓冲区后就直接返回,而不一定触发同步到磁盘的操作。只有在页空间写满,或者达到特定的时间周期,才会同步到磁盘。因此单纯的write操作也是有数据丢失的风险。
- fsync操作:针对单个文件操作,做强制硬盘同步,fsync将阻塞直到写入硬盘完成后返回。
虽然Redis提供了三种同步策略,兼顾安全和性能的同步策略是:AOF_FSYNC_EVERYSEC。但是仍有丢失数据的风险,而且不是一秒而是两秒的数据,接下来就看同步的源码实现:
// 将AOF缓存写到磁盘中
// 因为我们需要在回复client之前对AOF执行写操作,唯一的机会是在事件loop中,因此累计所有的AOF到缓存中,在下一次重新进入事件loop之前将缓存写到AOF文件中
// 关于force参数
// 当fsync被设置为每秒执行一次,如果后台仍有线程正在执行fsync操作,我们可能会延迟flush操作,因为write操作可能会被阻塞,当发生这种情况时,说明需要尽快的执行flush操作,会调用 serverCron() 函数。
// 然而如果force被设置为1,我们会无视后台的fsync,直接进行写入操作
#define AOF_WRITE_LOG_ERROR_RATE 30 /* Seconds between errors logging. */
// 将AOF缓存冲洗到磁盘中
void flushAppendOnlyFile(int force) {
ssize_t nwritten;
int sync_in_progress = 0;
mstime_t latency;
// 如果缓冲区中没有数据,直接返回
if (sdslen(server.aof_buf) == 0) return;
// 同步策略是每秒同步一次
if (server.aof_fsync == AOF_FSYNC_EVERYSEC)
// AOF同步操作是否在后台正在运行
sync_in_progress = bioPendingJobsOfType(BIO_AOF_FSYNC) != 0;
// 同步策略是每秒同步一次,且不是强制同步的
if (server.aof_fsync == AOF_FSYNC_EVERYSEC && !