环境说明:redis源码版本 5.0.3;我在阅读源码过程做了注释,git地址:https://gitee.com/xiaoangg/redis_annotation
参考书籍:《redis的设计与实现》
除了RDB持久化功能外,redis还提供了AOF持久化功能;
RDB持计划功能是将键值编码后保存到RDB文件键中,而AOF是将执行的命令保存到AOF文件中;
aof功能可以分为 命令写入(append)、文件同步(sync)、文件重写(rewrite)、重启加载(load);
1.命令的追加
当aof处于打开状态时,服务器在执行一个写命令后,会以协议格式写入aof缓存;
aof文件追击的入口位于service.c/call() 调用的propagate();
aof文件格式协议位于aof.c/catAppendOnlyGenericCommand:
/*
创建aof文本格式
argc 命令的个数
argv robj命令数组
(
set abc 1
则 argc =3, argv =["set", "abc", "1"]
)
*/
sds catAppendOnlyGenericCommand(sds dst, int argc, robj **argv) {
char buf[32];
int len, j;
robj *o;
buf[0] = '*'; //每个备份命令都是以*开始; 当前 buf="*"
len = 1 + ll2string(buf + 1, sizeof(buf) - 1, argc); //这时候buf= "*"+命令参数的个数;(如argc=3 这时候buf=“*3”)
buf[len++] = '\r'; //buf=“*3\r“
buf[len++] = '\n'; //buf=“*3\r\n“
dst = sdscatlen(dst, buf, len); //将buf内字符串追加到 dst
//开始拼接命令信息 这里假设命令信息是 set abc 123
for (j = 0; j < argc; j++) {
o = getDecodedObject(argv[j]);
buf[0] = '$'; //buf ="$"
len = 1+ll2string(buf+1,sizeof(buf)-1,sdslen(o->ptr)); //buf="$3" ; 加上了set 字符的长度
buf[len++] = '\r';
buf[len++] = '\n'; //buf=“buf="$3\r\n"
dst = sdscatlen(dst, buf, len); //buf 拼接到dst后
dst = sdscatlen(dst,o->ptr,sdslen(o->ptr));
dst = sdscatlen(dst,"\r\n",2);
decrRefCount(o);
}
return dst;
}
2.AOF文件的写入与同步
redis的服务器进程就是一个事件循环,这个循环中文件事件负责接受客户端的命令请求,以及向客户端发送命令回复;
时间事件则服务执行需要定时执行的函数;
服务器每次结束一次事件循环之前,都会调用aof.c/flushAppendOnlyFile 函数,考虑是否要将AOF缓存区的内容保存到AOF文件;
flushAppendOnlyFile的行为由服务器配置的appendfsync来决定:
#define AOF_FSYNC_NO 0
#define AOF_FSYNC_ALWAYS 1
#define AOF_FSYNC_EVERYSEC 2
文件的写入与同步:
为了提高文件的写入效率,在现代操作系统中,当用户调用write函数写文件时,操作系统通常会将数据咱还保存在内存中一个缓冲区里面,当缓冲区被填满或者超过来指定的时限,才将缓冲区中的数据写入磁盘;这种做法提高了文件的写入效率,但也带来了安全问题,如果计算机发生宕机,那么保存缓冲区的数据将会丢失;
为此系统提供了fsync和fdatasync两个函数,他们可以强制将缓冲区中的数据写入磁盘;
aof文件中包含了所有重建数据库的写命令,所以只需要读入aof文件内容,并重新执行一边,就可以还原数据库状态;
redis读取读取aof文件并还原的具体步骤如下:
aof持久化是通过保存被执行的写命令,来记录数据库状态的,所以aof文件的体积会随着服务器运行的时长, 变得越来越大;
为了解决这一问题,redis提供来AOF重写功能;
1.AOF重写的实现
AOF的重写并不需要对现有的AOF文件读取、分析或者写入操作,这个功能是通过读取当前数据库状态来实现的;
AOF重写的入口位于aof.c/rewriteAppendOnlyFileRio
/*
* aof文件重写
*/
int rewriteAppendOnlyFileRio(rio *aof) {
dictIterator *di = NULL;
dictEntry *de;
size_t processed = 0;
int j;
//遍历所有数据库
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;
di = dictGetSafeIterator(d);
//写入select 命令
/* SELECT the new DB */
if (rioWrite(aof,selectcmd,sizeof(selectcmd)-1) == 0) goto werr;
if (rioWriteBulkLongLong(aof,j) == 0) goto werr;
//遍历所有数据中的键
/* Iterate this DB writing every entry */
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);
/* Save the key and associated value */
if (o->type == OBJ_STRING) {
/* Emit a SET command */
char cmd[]="*3\r\n$3\r\nSET\r\n";
if (rioWrite(aof,cmd,sizeof(cmd)-1) == 0) goto werr;
/* Key and value */
//写入key
if (rioWriteBulkObject(aof,&key) == 0) goto werr;
//写入value
if (rioWriteBulkObject(aof,o) == 0) goto werr;
} else if (o->type == OBJ_LIST) { //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;
} else if (o->type == OBJ_STREAM) {
if (rewriteStreamObject(aof,&key,o) == 0) goto werr;
} else if (o->type == OBJ_MODULE) {
if (rewriteModuleObject(aof,&key,o) == 0) goto werr;
} else {
serverPanic("Unknown object type");
}
/* Save the expire time */
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;
}
/* Read some diff from the parent process from time to time. */
if (aof->processed_bytes > processed+AOF_READ_DIFF_INTERVAL_BYTES) {
processed = aof->processed_bytes;
aofReadDiffFromParent();
}
}
dictReleaseIterator(di);
di = NULL;
}
return C_OK;
werr:
if (di) dictReleaseIterator(di);
return C_ERR;
}
2.AOF后台重写
问题: AOF重写会进行大量的写入操作,所以调用这个函数的进程会被长时间的堵塞;因为redis服务器是用单线程来处理命令请求,如果由服务器来调用重写,那么服务器将无法处理客户端的命令请求;
解决:为了解决这一问题,redis将aof的重写程序放到来子进程中执行;
使用子进程带来的新的问题:子进程在aof重写期间,服务器进程还在继续处理命令请求,新的命令会对现有的数据库状态进行修改,导致 数据库当前的数据状态和aof文件中的数据不一致;
解决:redis服务器设置来一个重写缓冲区,这个缓冲区在服务器创建了子进程后开始使用。
服务器会将写命令同时发送给AOF缓冲区和AOF重写缓冲区;
当子进程完成了AOF重写工作后,会向父进程发送一个信号,父进程接收到信号后,会调用一个处理函数:
整个aof后台的重写过程,只有信号处理函数 对服务器造成了堵塞,这将AOF重写对服务器的影响降到了最低;