那么AOF模式为什么可以完整的记录整个数据库呢?
原理 :在AOF模式下,Redis会把执行过的每一条更新命令记录下来,保存到AOF文件中;当Redis需要恢复数据库数据时,只需要从之前保存的AOF文件中依次读取命令,执行即可 eg.
- 我们执行了以下命令:
- redis 127.0.0.1:6379> set name diaocow
- OK
- redis 127.0.0.1:6379> lpush country china usa
- (integer) 4
- 这时候在AOF文件中的类容类似下面:
- *3\r\n$3\r\nset\r\n$4\r\nname\r\n$7\r\ndiaocow\r\n
- *4\r\n$5\r\nlpush\r\n$7\r\ncountry\r\n$5\r\nchina\r\n$3\r\nusa\r\n
看了上面的内容,我想不用我过多解释,你也能大致猜出AOF协议格式,因为它实在太简单明了了
协议格式为:
*<count>\r\n<element1>…<elementN>
每个参数element格式:$<length>\r\n<content>\r\n
其中,不包含<>字符,count表示参数个数,length表示参数的字节长度,content表示参数的内容
(1)对于一般的写入命令(SET、SADD、ZADD、RPUSH等)+PEXPIREAT命令
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;
}
(2)设置键的过期时间命令:EXPIRE 、 PEXPIRE 和 EXPIREAT。
处理这三种命令,最终会转换为PEXPIREAT来执行。
sds catAppendOnlyExpireAtCommand(sds buf, struct redisCommand *cmd, robj *key, robj *seconds) {
long long when;
robj *argv[3];
/* Make sure we can use strtol
*
* 取出过期值
*/
seconds = getDecodedObject(seconds);
when = strtoll(seconds->ptr,NULL,10);
/* Convert argument into milliseconds for EXPIRE, SETEX, EXPIREAT
*
* 如果过期值的格式为秒,那么将它转换为毫秒
*/
if (cmd->proc == expireCommand || cmd->proc == setexCommand ||
cmd->proc == expireatCommand)
{
when *= 1000;
}
/* Convert into absolute time for 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);
// 追加到 AOF 缓存中
buf = catAppendOnlyGenericCommand(buf, 3, argv);
decrRefCount(argv[0]);
decrRefCount(argv[2]);
return buf;
}
可以看出上述函数中经过处理后,调用的依然是catAppendOnlyGenericCommand函数。
(3)对于SETEX和PSETEX命令
因为常见格式为:SETEXkey ttl value
所以对于命令参数来说,arg[1]为key,arg[2]为过期时间,argv[3]为value
在后面的feedAppendOnlyFile函数中会看到:
对于这两种命令,先翻译为SETkey value命令,接着翻译为PEXPIREAT key ttl_ms命令,依次执行上述两个逻辑函数:
// EXPIRE 、 PEXPIRE 和 EXPIREAT 命令
if (cmd->proc == expireCommand || cmd->proc == pexpireCommand ||
cmd->proc == expireatCommand) {
/* Translate EXPIRE/PEXPIRE/EXPIREAT into PEXPIREAT
*
* 将 EXPIRE 、 PEXPIRE 和 EXPIREAT 都翻译成 PEXPIREAT
*/
buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);
// SETEX 和 PSETEX 命令
} else if (cmd->proc == setexCommand || cmd->proc == psetexCommand) {
/* Translate SETEX/PSETEX to SET and PEXPIREAT
*
* 将两个命令都翻译成 SET 和 PEXPIREAT
*/
// SET
tmpargv[0] = createStringObject("SET",3);
tmpargv[1] = argv[1];
tmpargv[2] = argv[3];
buf = catAppendOnlyGenericCommand(buf,3,tmpargv);
// PEXPIREAT
decrRefCount(tmpargv[0]);
buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);
// 其他命令
} else {
/* All the other commands don't need translation or need the
* same translation already operated in the command vector
* for the replication itself. */
buf = catAppendOnlyGenericCommand(buf,argc,argv);
}
Redis把更新命令记录到AOF文件,分为两个阶段:
阶段1:把更新命令写入aof缓存
在每次调用执行更新命令后,根据设置选项(开启AOF、重写子进程)来判断是否执行追加到AOF缓存区(aof_buf)和重写缓
存区(aof_rewrite_buf_blocks)。
这里提一下,AOF缓冲区aof_buf为sds格式,重写缓存区(aof_rewrite_buf_blocks)则是一个list链表集合,具体类型定义为
aofrwblock,这样做的原因是考虑到分配到一个非常大的空间并不总是可能的,也可能产生大量的复制工作,所以这里采用多个
大小为AOF_RW_BUF_BLOCK_SIZE(10M)的空间来保存命令协议格式。aofrwblock定义为:
typedef struct aofrwblock {
// 缓存块已使用字节数和可用字节数
unsigned long used, free;
// 缓存块
char buf[AOF_RW_BUF_BLOCK_SIZE];
} aofrwblock;
设计到该类型的API有:
void aofRewriteBufferReset(void):释放旧的链表,初始化新的链表,用于AOF重写缓存的初始化
unsigned long aofRewriteBufferSize(void):返回AOF重写缓存当前已使用的大小
void aofRewriteBufferAppend(unsigned char *s, unsigned long len):将字符数组s追加到AOF重写缓存的末尾,不够的话继续分配新的缓存块
ssize_t aofRewriteBufferWrite(int fd):将重写缓存中的所有内容(可能有多个块组成)写入到给定fd中,返回写入的字节数量,错误返回-1
具体的缓存区追加函数为feedAppendOnlyFile,下面给出简约的伪代码。
Python代码
- def processCommand(cmd, argc, argv):
- # 执行命令
- call(cmd, argc, argv)
- # 该命令变更了键空间并且AOF模式打开
- if redisServer.update_key_space and redisServer.aof_state & REDIS_AOF_ON:
- feedAppendOnlyFile(cmd, argc, argv)
- def feedAppendOnlyFile(cmd, argc, argv):
- # 把命令转换成AOF协议格式
- aofCmdStr = getAofProtocolStr(cmd, argc, argv)
- redisServer.aof_buf.append(aofCmdStr )
- # 存在一个子进程正在进行AOF_REWRITE(关于AOF_REWRITE,稍后详说)
- if redisServer.aof_child_pid != -1:
- redisServer.aof_rewrite_buf_blocks.append(aofCmdStr )
阶段2: 把aof缓存写入文件
- def flushAppendOnlyFile(force):
- if len(redisServer.aof_buf) == 0:
- return
- # 把缓存数据写入文件
- if writeByPolicy(force, redisServer.aof_fsync):
- write(redisServer.aof_fd, redisServer.aof_buf, len(redisServer.aof_buf))
- # 同步数据到硬盘
- if fsyncByPolicy(force, redisServer.aof_fsync):
- fsync(redisServer.aof_fd)
更多细节请看: aof.c/flushAppendOnlyFile函数 (ps: 这个函数代码看起来比较晦涩)
看到这里,你也许会有两个疑问:
1. 为什么要调用fsync函数,不是已经调用write把数据写入到文件了吗?
2. 伪代码中aof_fsync是什么,它有几种类型?
首先回答问题1,为什么写入文件后,还要调用fsync函数:
大多数unix系统为了减少磁盘IO,采用了“延迟写”技术,也就是说当我们执行完write调用后,数据并不一定立马被写入磁盘(可能还是保留在系统的buffer cache或者page cache中),这样当主机突然断电,这些我们本以为已经写入到磁盘文件的数据可能就会丢失;所以当我们需要确保数据被完整正确的写入磁盘(譬如数据库的持久化),则需要调用同步函数fsync,它会一直阻塞直到数据全部被写入到硬盘
问题2,aof_fysnc是什么:
aof_fsync用来指定flush策略,也就是调用fsync函数的策略,它一共有三种:
a. AOF_FSYNC_NO :每次都会把aof_buf中的内容写入到磁盘,但是不会调用fsync函数;
b. AOF_FSYNC_ALWAYS :每次都会把aof_buf中的内容写入到磁盘,同时调用fsync函数; (主进程负责)
c. AOF_FSYNC_EVERYSEC :每次都会把aof_buf中的内容写入到磁盘,如果距离上次同步超过一秒则调用fsync函数,由子线程负责。(默认值)
由于AOF_FSYNC_ALWAYS每次都写入文件都会调用fsync,所以这种flush策略可以保证数据的完整性,缺点就是性能太差(因为fysnc是个同步调用,会阻塞主进程对客户端请求的处理),而AOF_FSYNC_NO由于依赖于操作系统自动sync,因此不能保证数据的完整性;
那有没有一种折中的方式:既能不过分降低系统的性能,又能最大程度上的保证数据的完整性,答案就是:AOF_FSYNC_EVERYSEC,AOF_FSYNC_EVERYSEC的flush策略是:定期(至少1s)去调用fsync,并且该操作是放到一个异步队列中(线程)去执行,因此不会阻塞主进程
AOF 后台执行的方式和 RDB 有类似的地方,fork 一个子进程,主进程仍进行服务,子进程执行 AOF 持久化,数据被 dump 到磁盘上。与 RDB 不同的是,后台子进程持久化过程中,主进程会记录期间的所有数据变更(主进程还在服务),并存储在 server.aof_rewrite_buf_blocks 中;后台子进程结束后,redis 更新缓存追加到 AOF 文件中,是 RDB 持久化所不具备的.
a. AOF协议本身是文本协议,比较占空间;
b. Redis需要记录从开始到现在的所有更新命令;
这两个原因导致了AOF文件容易变得很大,那有什么方式可以优化吗?譬如用户执行了三个命令:lpush name diaocow; lpush name jack; lpush name jobs
AOF文件会记录以下数据:
- *3\r\n$5\r\nlpush\r\n$4\r\nname\r\n$7\r\ndiaocow
- *3\r\n$5\r\nlpush\r\n$4\r\nname\r\n$4\r\njack
- *3\r\n$5\r\nlpush\r\n$4\r\nname\r\n$4\r\njobs
但其实只需要记录一条:lpush name diaocow jack jbos 命令即可:
- *5\r\n$5\r\nlpush\r\n$4\r\nname\r\n$7\r\ndiaocow\r\n$4\r\njack\r\n$4\r\njobs
所以当AOF文件达到 REDIS_AOF_REWRITE_MIN_SIZE(1M)时,Redis就会执行AOF_REWRITE来优化AOF文件;
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
AOF_REWRITE触发条件:
1. 被动: 当AOF文件尺寸超过REDIS_AOF_REWRITE_MIN_SIZE & 达到一定增长比;
2. 主动: 调用BGREWRITEAOF命令;
主动和被动方式的AOF_REWRITE过程基本相同,唯一的区别就是,通过BGREWRITEAOF命令执行的AOF_REWRITE(主动)是在一个子进程中进行,因此它不会阻塞主进程对客户端请求的处理,而被动方式由于是在主进程中进行,所以在AOF_REWRITE过程中redis是无法响应客户端请求的;
下面我就以BGREWRITEAOF命令为例,具体看下AOF_REWRITE过程:
上述执行命令过程的代码为:
void bgrewriteaofCommand(redisClient *c) {
// 不能重复运行 BGREWRITEAOF
if (server.aof_child_pid != -1) {
addReplyError(c,"Background append only file rewriting already in progress");
// 如果正在执行 BGSAVE ,那么预定 BGREWRITEAOF
// 等 BGSAVE 完成之后, BGREWRITEAOF 就会开始执行
} else if (server.rdb_child_pid != -1) {
server.aof_rewrite_scheduled = 1;
addReplyStatus(c,"Background append only file rewriting scheduled");
// 执行 BGREWRITEAOF
} else if (rewriteAppendOnlyFileBackground() == REDIS_OK) {
addReplyStatus(c,"Background append only file rewriting started");
} else {
addReply(c,shared.err);
}
}
执行rewriteAppendOnlyFileBackground的代码为:
/* This is how rewriting of the append only file in background works:
*
* 以下是后台重写 AOF 文件(BGREWRITEAOF)的工作步骤:
*
* 1) The user calls BGREWRITEAOF
* 用户调用 BGREWRITEAOF
*
* 2) Redis calls this function, that forks():
* Redis 调用这个函数,它执行 fork() :
*
* 2a) the child rewrite the append only file in a temp file.
* 子进程在临时文件中对 AOF 文件进行重写
*
* 2b) the parent accumulates differences in server.aof_rewrite_buf.
* 父进程将新输入的写命令追加到 server.aof_rewrite_buf 中
*
* 3) When the child finished '2a' exists.
* 当步骤 2a 执行完之后,子进程结束
*
* 4) The parent will trap the exit code, if it's OK, will append the
* data accumulated into server.aof_rewrite_buf into the temp file, and
* finally will rename(2) the temp file in the actual file name.
* The the new file is reopened as the new append only file. Profit!
*
* 父进程会捕捉子进程的退出信号,
* 如果子进程的退出状态是 OK 的话,
* 那么父进程将新输入命令的缓存追加到临时文件,
* 然后使用 rename(2) 对临时文件改名,用它代替旧的 AOF 文件,
* 至此,后台 AOF 重写完成。
*/
int rewriteAppendOnlyFileBackground(void) {
pid_t childpid;
long long start;
// 已经有进程在进行 AOF 重写了
if (server.aof_child_pid != -1) return REDIS_ERR;
// 记录 fork 开始前的时间,计算 fork 耗时用
start = ustime();
//fork函数执行时,创建一个子进程,复制一份副本给子进程,若fork成功执行,子进程返回0,父进程返回子进程的ID,失败则返回-1
//fork只调用一次,有两个返回值,父子进程将同时拥有以下代码,执行。
//子进程
if ((childpid = fork()) == 0) {
char tmpfile[256];
/* Child */
// 关闭网络连接 fd
closeListeningSockets(0);
// 为进程设置名字,方便记认
redisSetProcTitle("redis-aof-rewrite");
// 创建临时文件,临时文件中的名字是由获得进程的ID来设置,防止临时文件重名,并进行 AOF 重写
snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof", (int) getpid());
if (rewriteAppendOnlyFile(tmpfile) == REDIS_OK) {
size_t private_dirty = zmalloc_get_private_dirty();
if (private_dirty) {
redisLog(REDIS_NOTICE,
"AOF rewrite: %zu MB of memory used by copy-on-write",
private_dirty/(1024*1024));
}
// 发送重写成功信号
exitFromChild(0);
} else {
// 发送重写失败信号
exitFromChild(1);
}
} else {
/* Parent父进程 */
// 记录执行 fork 所消耗的时间
server.stat_fork_time = ustime()-start;
if (childpid == -1) {
redisLog(REDIS_WARNING,
"Can't rewrite append only file in background: fork: %s",
strerror(errno));
return REDIS_ERR;
}
redisLog(REDIS_NOTICE,
"Background append only file rewriting started by pid %d",childpid);
// 记录 AOF 重写的信息
server.aof_rewrite_scheduled = 0;
server.aof_rewrite_time_start = time(NULL);
server.aof_child_pid = childpid;
// 关闭字典自动 rehash
updateDictResizePolicy();
/* We set appendseldb to -1 in order to force the next call to the
* feedAppendOnlyFile() to issue a SELECT command, so the differences
* accumulated by the parent into server.aof_rewrite_buf will start
* with a SELECT statement and it will be safe to merge.
*
* 将 aof_selected_db 设为 -1 ,
* 强制让 feedAppendOnlyFile() 下次执行时引发一个 SELECT 命令,
* 从而确保之后新添加的命令会设置到正确的数据库中
*/
server.aof_selected_db = -1;
replicationScriptCacheFlush();
return REDIS_OK;
}
return REDIS_OK; /* unreached */
}
整个AOF_WRITE过程,最重要的一个函数是: rewriteAppendOnlyFile,它主要做了下面事情:
a. 创建一个临时文件temp-rewriteaof-pid.aof;
b. 循环所有数据库,把每一个数据库中的键值对(过期键不写入),按照aof协议写入到临时文件;
c. 重命名临时文件;
注意的是,为了避免在执行命令时造成客户端输入缓冲区溢出,重写程序在处理列表、哈希表、集合、有序集合这四种可能会带有多个元素的键时,会先检查键所包含的元素数量,当超过了redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值,那么重写程序将使用多条命令来记录键的值,不单单使用一个命令,每条命令所恢复的元素个数最多不能超过上述常量值。
int rewriteAppendOnlyFile(char *filename) {
dictIterator *di = NULL;
dictEntry *de;
rio aof;
FILE *fp;
char tmpfile[256];
int j;
long long now = mstime();
/* Note that we have to use a different temp name here compared to the
* one used by rewriteAppendOnlyFileBackground() function.
*
* 创建临时文件
*
* 注意这里创建的文件名和 rewriteAppendOnlyFileBackground() 创建的文件名稍有不同
* 后者为snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof", (int) getpid());
*/
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;
}
// 初始化文件 io
rioInitWithFile(&aof,fp);
// 设置每写入 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++) {
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);
if (!di) {
fclose(fp);
return REDIS_ERR;
}
/* SELECT the new DB
*
* 首先写入 SELECT 命令,确保之后的数据会被插入到正确的数据库上
*/
if (rioWrite(&aof,selectcmd,sizeof(selectcmd)-1) == 0) goto werr;
if (rioWriteBulkLongLong(&aof,j) == 0) goto werr;
/* Iterate this DB writing every entry
*
* 遍历数据库所有键,并通过命令将它们的当前状态(值)记录到新 AOF 文件中
*/
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);
/* If this key is already expired skip it
*
* 如果键已经过期,那么跳过它,不保存
*/
if (expiretime != -1 && expiretime < now) continue;
/* Save the key and associated value
*
* 根据值的类型,选择适当的命令来保存值
*/
if (o->type == REDIS_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 */
if (rioWriteBulkObject(&aof,&key) == 0) goto werr;
if (rioWriteBulkObject(&aof,o) == 0) goto werr;
} else if (o->type == REDIS_LIST) {
if (rewriteListObject(&aof,&key,o) == 0) goto werr;
} else if (o->type == REDIS_SET) {
if (rewriteSetObject(&aof,&key,o) == 0) goto werr;
} else if (o->type == REDIS_ZSET) {
if (rewriteSortedSetObject(&aof,&key,o) == 0) goto werr;
} else if (o->type == REDIS_HASH) {
if (rewriteHashObject(&aof,&key,o) == 0) goto werr;
} else {
redisPanic("Unknown object type");
}
/* Save the expire time
*
* 保存键的过期时间
*/
if (expiretime != -1) {
char cmd[]="*3\r\n$9\r\nPEXPIREAT\r\n";
// 写入 PEXPIREAT expiretime 命令
if (rioWrite(&aof,cmd,sizeof(cmd)-1) == 0) goto werr;
if (rioWriteBulkObject(&aof,&key) == 0) goto werr;
if (rioWriteBulkLongLong(&aof,expiretime) == 0) goto werr;
}
}
// 释放迭代器
dictReleaseIterator(di);
}
/* Make sure data will not remain on the OS's output buffers */
// 冲洗并关闭新 AOF 文件
if (fflush(fp) == EOF) goto werr;
if (aof_fsync(fileno(fp)) == -1) goto werr;
if (fclose(fp) == EOF) goto werr;
/* Use RENAME to make sure the DB file is changed atomically only
* if the generate DB file is ok.
*
* 原子地改名,用重写后的新 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;
}
redisLog(REDIS_NOTICE,"SYNC append only file rewrite performed");
return REDIS_OK;
werr:
fclose(fp);
unlink(tmpfile);
redisLog(REDIS_WARNING,"Write error writing append only file on disk: %s", strerror(errno));
if (di) dictReleaseIterator(di);
return REDIS_ERR;
}
将重建各种对象所需的命令协议格式写入到rio中:(在下列具体函数中会严格判断每条命令包含的元素数量)
int rioWriteBulkObject(rio *r, robj *obj):obj指向的整数对象或者字符串对象
int rewriteListObject(rio*r, robj *key, robj *o):列表对象
int rewriteSetObject(rio *r, robj *key, robj *o):集合对象
int rewriteSortedSetObject(rio *r, robj *key, robj *o):有序集合对象
int rewriteHashObject(rio *r, robj *key, robj *o):哈希对象
当AOF_REWRITE过程执行完毕,Redis会用新生成的文件去替换原来的AOF文件,至此我们可以说,现在AOF文件中的内容已经是最精简的了
。
现在还存在一个问题:如果我们是通过 主动方式 去执行AOF_REWRITE,那么在保存AOF文件期间,“键空间”是可能发生变化的(因为主进程没有被阻塞),若直接用新生成的文件去替换原来的AOF文件,就会造成数据的不一致性(丢失在AOF_REWRITE过程中更新的数据)
那redis如何解决这个问题呢? 在文章开头讲AOF模式的时候,我列举了下面一段伪代码:
- def processCommand(cmd, argc, argv):
- # 执行命令
- call(cmd, argc, argv)
- # 该命令变更了键空间并且AOF模式打开
- if redisServer.update_key_space and redisServer.aof_state & REDIS_AOF_ON:
- feedAppendOnlyFile(cmd, argc, argv)
- def feedAppendOnlyFile(cmd, argc, argv):
- # 把命令转换成AOF协议格式
- aofCmdStr = getAofProtocolStr(cmd, argc, argv)
- redisServer.aof_buf.append(aofCmdStr )
- # 存在一个子进程正在进行AOF_REWRITE
- if redisServer.aof_child_pid != -1:
- # 把变更命写写到aof重写缓存
- redisServer.aof_rewrite_buf_blocks.append(aofCmdStr )
你会发现,如果redis检测到有一个子进程正在进行AOF_REWRITE,那么它会把这期间所有变更命令写到AOF重写缓存(aof_rewrite_buf_blocks),然后当子进程完成AOF_REWRITE后,它会向父进程发送信号,父进程接受到子进程发来的信号,会将AOF重写缓存中的内容追加到新生成文件(该过程执行函数为backgroundRewriteDoneHandler,该函数在serverCron中会被判断捕捉。AOF重写缓存的追加过程会阻塞父进程,直至完成),这样我们就可以保证数据的一致性,避免刚才说的问题发生。
AOF文件的载入和还原
Redis读取AOF文件并还原数据库状态的步骤为:
1、创建一个不带网络连接的伪客户端(fakeClient):因为命令来源于AOF文件,非网络连接
2、循环读取每条指令,伪客户端执行,直至所有写命令处理完毕。
可参考aof.c/int loadAppendOnlyFile(char *filename)函数,该函数执行逻辑为:
打开AOF文件并检查,暂时性关闭AOF,建立伪客户端:
开始从文件中循环:
每一次读取内容到缓存,解析得到参数个数、每个参数,从命令表中寻找该命令,调用伪客户端,执行命令,最后清理命令和命令参数对象。
循环结束,关闭AOF文件,释放伪客户端,恢复AOF状态。
RDB和AOF持久化区别:
参考博客:https://blog.csdn.net/jackpk/article/details/30073097
总结:
1. 了解AOF协议
2. 了解AOF模式作用及原理
3. 了解AOF重写作用及原理
参考博客:https://blog.csdn.net/erica_1230/article/details/51305552