为了解决持久化文件很庞大以及会阻塞服务器的 情况,redis提出一种新的持久化方案:AOF持久化。AOF持久化是redis保存数据的另外一种方式,全称Append Only File,与RDB持久化不同的是,AOF持久化是只保存从客户端键入的命令,而RDB持久化是单纯的保存数据。 AOF持久化的实现分为3个步骤:命令的追加、缓冲区写入文件、文件同步。
AOF持久化的触发
- 服务器配置默认开启AOF持久化:服务器默认开启AOF持久化的时候,会在服务器重启或者关闭时默认采用AOF持久化,而不是RDB持久化;
- 手动键入BGWRITEAOF:手动输入BGWRITEAOF命令,启动AOF持久化;
- 时间事件的定期操作:在redis中有三种策略来将AOF缓冲区里面的内容写入到AOF文件中去。
AOF文件协议
AOF持久化采用自己独特的字符串协议方式,在写入AOF文件时,程序会将AOF缓冲区里面的命令字符串转化为该协议方式下的字符串。该协议格式如下:
*<number>\r\n$<length>\r\n<commnd>\r\n$<length>\r\n<key>\r\n$<length>\r\n<value>......
首先开头会用*与后面的number数字表示这是第几条命令,然后接\r\n这一部分表示结束,再接$开头length表明接下来的命令的字节大小,再是命令字符,后面接若干个键值对。加入输入如下指令:
SADD msg 1 name 2
则该字符串转化后如下:
*2\r\n$4\r\nSADD\r\n$3\r\nmsg\r\n$1\r\n$4\r\n$1\r\n2\r\n
之所以开头是2,是因为在之前redis会默认执行选择数据库的命令。上面是没有过期时间的键,如果某个键设置了过期时间,那么redis会从中取出过期事件,创建PEXPIREAT命令来添加:
......
/*
* 如果过期值的格式为相对值,那么将它转换为绝对值
*/
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);
// 追加到 AOF 缓存中
buf = catAppendOnlyGenericCommand(buf, 3, argv);
......
下面列出该协议的转换代码:
sds catAppendOnlyGenericCommand(sds dst, int argc, robj **argv) {
char buf[32];
int len, j;
robj *o;
// 重建命令的个数,格式为 *<count>\r\n
// 例如 *3\r\n
buf[0] = '*';
len = 1+ll2string(buf+1,sizeof(buf)-1,argc);
buf[len++] = '\r';
buf[len++] = '\n';
dst = sdscatlen(dst,buf,len);
// 重建命令和命令参数,格式为 $<length>\r\n<content>\r\n
// 例如 $3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n
for (j = 0; j < argc; j++) {
o = getDecodedObject(argv[j]);
// 组合 $<length>\r\n
buf[0] = '$';
len = 1+ll2string(buf+1,sizeof(buf)-1,sdslen(o->ptr));
buf[len++] = '\r';
buf[len++] = '\n';
dst = sdscatlen(dst,buf,len);
// 组合 <content>\r\n
dst = sdscatlen(dst,o->ptr,sdslen(o->ptr));
dst = sdscatlen(dst,"\r\n",2);
decrRefCount(o);
}
// 返回重建后的协议内容
return dst;
}
AOF缓冲区
AOF缓冲区是用来存贮需要写入AOF文件的命令的,因为服务端可能来不及的处理大量的请求命令,所以需要定义一个缓冲区来缓冲,在写入一个命令时,redis会先将它写入到服务器的AOF的缓冲区去,在redis服务器有如下的定义:
struct redisServer {
// AOF 缓冲区
sds aof_buf;
// AOF 状态(开启/关闭/可写)
int aof_state;
// 所使用的 fsync 策略(每个写入/每秒/从不)
int aof_fsync;
// AOF 重写缓存链表,链接着多个缓存块
list *aof_rewrite_buf_blocks;
}
aof_rewrite_buf_blocks是一个指针,它指向一个链表,该链表是服务器的多个缓冲区,这个缓冲区链表的长度是由写入的命令多少而定的。aof_buf里面存贮着新建入的命令,而aof_state代表着是否开启AOF持久化,而aof_fsync代表着同步的策略,在AOF持久化中有三种方式,同步策略放在后面讲解。在redis中对AOF缓冲区的定义如下:
// 每个缓存块的大小
#define AOF_RW_BUF_BLOCK_SIZE (1024*1024*10) /* 10 MB per block */
typedef struct aofrwblock {
// 缓存块已使用字节数和可用字节数
unsigned long used, free;
// 缓存块
char buf[AOF_RW_BUF_BLOCK_SIZE];
} aofrwblock;
上面谈到AOF缓冲区链表是一个可扩充的链表,当该链表中最后一个缓冲区内存已经不够写入新的命令字符串时(前面的已经写满了),会新建一个缓冲区加入该链表,同时这也是将命令行追加到AOF缓冲区的实现,代码如下:
/*
* 将字符数组 s 追加到 AOF 缓存的末尾,
* 如果有需要的话,分配一个新的缓存块。
*/
void aofRewriteBufferAppend(unsigned char *s, unsigned long len) {
// 指向最后一个缓存块
listNode *ln = listLast(server.aof_rewrite_buf_blocks);
aofrwblock *block = ln ? ln->value : NULL;
while(len) {
//如果已经有至少一个缓存块,那么尝试将内容追加到这个缓存块里面
if (block) {
unsigned long thislen = (block->free < len) ? block->free : len;
if (thislen) {
memcpy(block->buf+block->used, s, thislen);
block->used += thislen;
block->free -= thislen;
s += thislen;
len -= thislen;
}
}
// 如果 block != NULL ,那么这里是创建另一个缓存块买容纳 block 装不下的内容
// 如果 block == NULL ,那么这里是创建缓存链表的第一个缓存块
if (len) { /* First block to allocate, or need another block. */
int numblocks;
// 分配缓存块
block = zmalloc(sizeof(*block));
block->free = AOF_RW_BUF_BLOCK_SIZE;
block->used = 0;
// 链接到链表末尾
listAddNodeTail(server.aof_rewrite_buf_blocks,block);
...
//打印。。。。。
...
}
}
}
AOF的写入与同步
为什么要进行同步呢?在现在的操作系统中,为了提高效率,将数据写入文件时,操作系统会把数据暂时的保存在内存的缓冲区中(不是AOF缓冲区,AOF缓冲区借鉴了这个思想),当缓冲区满了或者达到一定的时限后,才会将数据写入到文件中。而同步的意思就是清空缓冲区(flush),直接将数据写入到文件中,而不要等到缓冲区满了或者到达一定的时限。这样就保证了数据的正确性,因为在保存数据时可能遇见断电或者电脑宕机等情况,不及时flush掉数据的话,容易造成数据的遗漏。而redis中写入同步策略有三种,也就是aof_fsync的值:
#define AOF_FSYNC_NO 0
#define AOF_FSYNC_ALWAYS 1
#define AOF_FSYNC_EVERYSEC 2
aof_fsync | 表达的内容 |
always | 将AOF缓冲区里面的所有内容写入并同步到AOF文件 |
everysec | 将AOF缓冲区里面的所有内容写入到AOF文件,如果上次同步AOF文件超过了1s钟,那么再次进行同步 |
no | 将AOF缓冲区里面的所有内容写入AOF文件,同步的操作由操作系统决定 |
缓冲区的写入与同步是由flushAppendOnlyFile函数执行的,这个函数会在每次事件执行前执行一次,如下代码:
void beforeSleep(struct aeEventLoop *eventLoop) {
REDIS_NOTUSED(eventLoop);
......
// 将 AOF 缓冲区的内容写入到 AOF 文件
flushAppendOnlyFile(0);
......
}
redis在默认情况下是everysec同步,beforeSleep函数是事件先行函数,在每个事件执行之前,都会是调用它执行一次。该源码注释如下:
/*
* 将 AOF 缓存写入到文件中。
* 因为程序需要在回复客户端之前对 AOF 执行写操作。
* 而客户端能执行写操作的唯一机会就是在事件 loop 中,
* 因此,程序将所有 AOF 写累积到缓存中,
* 并在重新进入事件 loop 之前,将缓存写入到文件中。
* 关于 force 参数:
* 当 fsync 策略为每秒钟保存一次时,如果后台线程仍然有 fsync 在执行,
* 那么我们可能会延迟执行冲洗(flush)操作,
* 因为 Linux 上的 write(2) 会被后台的 fsync 阻塞。
* 当这种情况发生时,说明需要尽快冲洗 aof 缓存,
* 程序会尝试在 serverCron() 函数中对缓存进行冲洗。
* 不过,如果 force 为 1 的话,那么不管后台是否正在 fsync ,
* 程序都直接进行写入。
*/
#define AOF_WRITE_LOG_ERROR_RATE 30
void flushAppendOnlyFile(int force) {
ssize_t nwritten;
int sync_in_progress = 0;
// 缓冲区中没有任何内容,直接返回
if (sdslen(server.aof_buf) == 0) return;
// 策略为每秒 FSYNC
if (server.aof_fsync == AOF_FSYNC_EVERYSEC)
// 是否有 SYNC 正在后台进行?
sync_in_progress = bioPendingJobsOfType(REDIS_BIO_AOF_FSYNC) != 0;
// 每秒 fsync ,并且强制写入为假
if (server.aof_fsync == AOF_FSYNC_EVERYSEC && !force) {
// 当 fsync 策略为每秒钟一次时, fsync 在后台执行。
// 如果后台仍在执行 FSYNC ,那么我们可以延迟写操作一两秒
//(如果强制执行 write 的话,服务器主线程将阻塞在 write 上面)
if (sync_in_progress) {
// 有 fsync 正在后台进行 。。。
if (server.aof_flush_postponed_start == 0) {
/*
* 前面没有推迟过 write 操作,这里将推迟写操作的时间记录下来
* 然后就返回,不执行 write 或者 fsync
*/
server.aof_flush_postponed_start = server.unixtime;
return;
} else if (server.unixtime - server.aof_flush_postponed_start < 2) {
/*
* 如果之前已经因为 fsync 而推迟了 write 操作
* 但是推迟的时间不超过 2 秒,那么直接返回
* 不执行 write 或者 fsync
*/
return;
}
/*
* 如果后台还有 fsync 在执行,并且 write 已经推迟 >= 2 秒
* 那么执行写操作(write 将被阻塞)
*/
server.aof_delayed_fsync++;
redisLog(REDIS_NOTICE,"Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis.");
}
}
/*
* 执行到这里,程序会对 AOF 文件进行写入。
* 清零延迟 write 的时间记录
*/
server.aof_flush_postponed_start = 0;
/*
* 执行单个 write 操作,如果写入设备是物理的话,那么这个操作应该是原子的
* 当然,如果出现像电源中断这样的不可抗现象,那么 AOF 文件也是可能会出现问题的
* 这时就要用 redis-check-aof 程序来进行修复。
*/
//server.aof_fd指本机的套接字,对自身文件读写
nwritten = write(server.aof_fd,server.aof_buf,sdslen(server.aof_buf));
......
//日志打印
......
//移除错误的命令
......
//处理写入AOF文件时出现的错误
......
// 写入成功,更新最后写入状态
......
// 更新写入后的 AOF 文件大小
server.aof_current_size += nwritten;
/*
* 如果 AOF 缓存的大小足够小的话,那么重用这个缓存,
* 否则的话,释放 AOF 缓存。
*/
if ((sdslen(server.aof_buf)+sdsavail(server.aof_buf)) < 4000) {
// 清空缓存中的内容,等待重用
sdsclear(server.aof_buf);
} else {
// 释放缓存
sdsfree(server.aof_buf);
server.aof_buf = sdsempty();
}
//同步执行
/*
* 如果 no-appendfsync-on-rewrite 选项为开启状态,
* 并且有 BGSAVE 或者 BGREWRITEAOF 正在进行的话,
* 那么不执行 fsync
*/
if (server.aof_no_fsync_on_rewrite &&
(server.aof_child_pid != -1 || server.rdb_child_pid != -1))
return;
// 总是执行 fsnyc
if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
aof_fsync(server.aof_fd);
// 更新最后一次执行 fsnyc 的时间
server.aof_last_fsync = server.unixtime;
// 策略为每秒 fsnyc ,并且距离上次 fsync 已经超过 1 秒
} else if ((server.aof_fsync == AOF_FSYNC_EVERYSEC &&
server.unixtime > server.aof_last_fsync)) {
// 放到后台执行
if (!sync_in_progress) aof_background_fsync(server.aof_fd);
// 更新最后一次执行 fsync 的时间
server.aof_last_fsync = server.unixtime;
}
}
AOF重写
因为AOF持久化是采用存贮命令字符串的形式来保存数据的,所以当其大小达到一定的程度时,我们就可以对其进行AOF重写,什么是AOF重写呢?就是原本两条命令数据,其实可以用一条命令来实现,两条命令重写成一条命令的过程就叫做AOF重写,如:
set a b
set a c
上述过程可以写成下面一条命令:
set a c
AOF重写过程其实是创建一个零时文件,然后直接读取当前的数据库中的数据,将当前数据库中的数据读取出来然后以命令字符串的形式写入到零时文件中,然后用零时文件替换掉以前的AOF文件完成重写,也就是说AOF重写没有读写之前的AOF文件。在其中遇到过期的键会自动过滤掉,实现代码流程如下:
int rewriteAppendOnlyFile(char *filename) {
......
/*
* 创建临时文件
* 注意这里创建的文件名和 rewriteAppendOnlyFileBackground() 创建的文件名稍有不同
*/
snprintf(tmpfile,256,"temp-rewriteaof-%d.aof", (int) getpid());
fp = fopen(tmpfile,"w");
if (!fp) {
redisLog(REDIS_WARNING, "Opening the temp file for AOF rewrite in rewriteAppendOnlyFile(): %s", strerror(errno));
return REDIS_ERR;
}
......
// 设置每写入 REDIS_AOF_AUTOSYNC_BYTES 字节
// 就执行一次 FSYNC
// 防止缓存中积累太多命令内容,造成 I/O 阻塞时间过长
if (server.aof_rewrite_incremental_fsync)
rioSetAutoSync(&aof,REDIS_AOF_AUTOSYNC_BYTES);
// 遍历所有数据库
for (j = 0; j < server.dbnum; j++) {
......
//先写入 SELECT 命令,确保之后的数据会被插入到正确的数据库上
if (rioWrite(&aof,selectcmd,sizeof(selectcmd)-1) == 0) goto werr;
if (rioWriteBulkLongLong(&aof,j) == 0) goto werr;
//遍历数据库所有键,并通过命令将它们的当前状态(值)记录到新 AOF 文件中
while((de = dictNext(di)) != NULL) {
......
// 取出过期时间
expiretime = getExpire(db,&key);
//如果键已经过期,那么跳过它,不保存
if (expiretime != -1 && expiretime < now) continue;
// 根据值的类型,选择适当的命令来保存值
........
// 保存键的过期时间
......
......
}
// 冲洗并关闭新 AOF 文件
if (fflush(fp) == EOF) goto werr;
if (aof_fsync(fileno(fp)) == -1) goto werr;
if (fclose(fp) == EOF) goto werr;
//原子地改名,用重写后的新 AOF 文件覆盖旧 AOF 文件
if (rename(tmpfile,filename) == -1) {
redisLog(REDIS_WARNING,"Error moving temp append only file on the final destination: %s", strerror(errno));
unlink(tmpfile);
return REDIS_ERR;
}
......
}
实际上是,AOF重写都是放在子线程里面进行的,也就是BGWRITEAOF命令,后台AOF重写,而文件的新旧替换是发生在AOF文件重写后,在子线程里将至替换掉。而开启后台AOF重写后,服务端不会阻塞,新键入的命令会追加到AOF重写缓冲区去,随着重写的进行写入AOF文件中。
AOF文件的载入
AOF文件的载入值得一提,当服务器重启时,其载入的步骤有如下:
- 创建一个伪客户端,因为redis的命令写入只能在客户端的上下文中执行,所以需要一个假的客户端来执行命令的写入;
- 为客户端从AOF文件读取命令并向服务器发送命令;
- 一直执行步骤2直到文件中所有数据被读完,释放资源;
代码如下:
int loadAppendOnlyFile(char *filename) {
// 为客户端
struct redisClient *fakeClient;
// 打开 AOF 文件
FILE *fp = fopen(filename,"r");
struct redis_stat sb;
int old_aof_state = server.aof_state;
long loops = 0;
// 检查文件的正确性
if (fp && redis_fstat(fileno(fp),&sb) != -1 && sb.st_size == 0) {
server.aof_current_size = 0;
fclose(fp);
return REDIS_ERR;
}
// 检查文件是否正常打开
if (fp == NULL) {
redisLog(REDIS_WARNING,"Fatal error: can't open the append log file for reading: %s",strerror(errno));
exit(1);
}
/*
* 暂时性地关闭 AOF ,防止在执行 MULTI 时,
* EXEC 命令被传播到正在打开的 AOF 文件中。
*/
server.aof_state = REDIS_AOF_OFF;
fakeClient = createFakeClient();
// 设置服务器的状态为:正在载入
// startLoading 定义于 rdb.c
startLoading(fp);
while(1) {
int argc, j;
unsigned long len;
robj **argv;
char buf[128];
sds argsds;
struct redisCommand *cmd;
/*
* 间隔性地处理客户端发送来的请求
* 因为服务器正处于载入状态,所以能正常执行的只有 PUBSUB 等模块
*/
if (!(loops++ % 1000)) {
loadingProgress(ftello(fp));
processEventsWhileBlocked();
}
// 读入文件内容到缓存
if (fgets(buf,sizeof(buf),fp) == NULL) {
if (feof(fp))
// 文件已经读完,跳出
break;
else
goto readerr;
}
// 确认协议格式,比如 *3\r\n
if (buf[0] != '*') goto fmterr;
// 取出命令参数,比如 *3\r\n 中的 3
argc = atoi(buf+1);
// 至少要有一个参数(被调用的命令)
if (argc < 1) goto fmterr;
// 从文本中创建字符串对象:包括命令,以及命令参数
// 例如 $3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n
// 将创建三个包含以下内容的字符串对象:
// SET 、 KEY 、 VALUE
argv = zmalloc(sizeof(robj*)*argc);
for (j = 0; j < argc; j++) {
if (fgets(buf,sizeof(buf),fp) == NULL) goto readerr;
if (buf[0] != '$') goto fmterr;
// 读取参数值的长度
len = strtol(buf+1,NULL,10);
// 读取参数值
argsds = sdsnewlen(NULL,len);
if (len && fread(argsds,len,1,fp) == 0) goto fmterr;
// 为参数创建对象
argv[j] = createObject(REDIS_STRING,argsds);
if (fread(buf,2,1,fp) == 0) goto fmterr;
}
// 查找命令
cmd = lookupCommand(argv[0]->ptr);
if (!cmd) {
redisLog(REDIS_WARNING,"Unknown command '%s' reading the append only file", (char*)argv[0]->ptr);
exit(1);
}
// 调用伪客户端,执行命令
fakeClient->argc = argc;
fakeClient->argv = argv;
cmd->proc(fakeClient);
/* The fake client should not have a reply */
redisAssert(fakeClient->bufpos == 0 && listLength(fakeClient->reply) == 0);
/* The fake client should never get blocked */
redisAssert((fakeClient->flags & REDIS_BLOCKED) == 0);
// 清理命令和命令参数对象
for (j = 0; j < fakeClient->argc; j++)
decrRefCount(fakeClient->argv[j]);
zfree(fakeClient->argv);
}
/*
* 如果能执行到这里,说明 AOF 文件的全部内容都可以正确地读取,
* 但是,还要检查 AOF 是否包含未正确结束的事务
*/
if (fakeClient->flags & REDIS_MULTI) goto readerr;
// 关闭 AOF 文件
fclose(fp);
// 释放伪客户端
freeFakeClient(fakeClient);
// 复原 AOF 状态
server.aof_state = old_aof_state;
// 停止载入
stopLoading();
// 更新服务器状态中, AOF 文件的当前大小
aofUpdateCurrentSize();
// 记录前一次重写时的大小
server.aof_rewrite_base_size = server.aof_current_size;
return REDIS_OK;
// 读入错误
......
// 内容格式错误
fmterr:
......
}
总结
1. AOF持久化保存着数据库的命令字符串,而不是保存的数据;
2. _ AOF文件到达一定的大小可以启动AOF文件重写,减少AOF文件的大小与加快其载入速度;_
3. AOF文件重写是有子线程进行的,所以不会阻塞;
4. AOF载入的时候会创建一个伪客户端来进行命令的读取和发送到服务器。