Redis深度歷險-AOF持久化
Redis提供兩種持久化方式AOF和RDB,RDB是快照形式持久化全量數據、AOF是增量持久化記錄執行命令
AOF原理
struct redisServer {
........
sds aof_buf; //AOF緩衝區
AOF持久化的是Redis執行的寫入命令,Redis會將執行的寫入命令放入AOF緩衝區中;而在Redis的定時任務server.c/serverCron
中將數據寫入到磁盤中
在恢復數據時按照順序依次執行即可恢復數據。
AOF配置
-
appendonly yes
:是否開啓AOF持久化 -
appendfilename appendonly.aof
:文件名 -
appendfsync
:磁盤寫入策略,主要是因爲文件IO有緩衝區可能數據沒有真正寫入到文件-
always
:每次寫入磁盤,性能較差 -
everysec
:每秒寫入一次 -
no
:系統處理緩存回寫
-
在這裏面影響最大的是磁盤寫入策略,因爲調用write
寫入的是系統緩衝區而沒有真正落盤,如果系統發生宕機則會造成數據丟失,不過Linux提供fsync
和fdatasync
等接口將數據刷入硬盤中
磁盤寫入策略就是控制落盤的時機,需要根據系統需求在數據和性能中作出取捨
當然,調用write
接口也是註冊事件到Redis的事件循環中一併處理的
AOF實現
void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int argc) {
........
//將命令寫入到aof緩衝區中
if (server.aof_state == AOF_ON)
server.aof_buf = sdscatlen(server.aof_buf,buf,sdslen(buf));
//如果正在進程AOF重寫時,則同樣將輸入發送給AOF重寫子進程
if (server.child_type == CHILD_TYPE_AOF)
aofRewriteBufferAppend((unsigned char*)buf,sdslen(buf));
AOF重寫
AOF的問題在於存儲的是明文命令且存在冗餘,文件內容會非常大,在恢復數據時耗時非常長
AOF重寫原理
SET "A" "B"
DEL "A"
SET "A" "C"
SET "A" "D"
類似於這樣執行後,Redis中實際上只有A
這樣一個鍵,AOF中存儲的四條命令實際上存在冗餘;AOF重寫的原理就是根據內存中的數據將AOF文件重寫爲:
SET "A" "D"
在恢復數據時最終得出的結果時一致的
AOF重寫實現
執行
bgrewriteaof
命令即可進行AOF重寫
int rewriteAppendOnlyFileBackground(void) {
pid_t childpid;
if (hasActiveChildProcess()) return C_ERR;
if (aofCreatePipes() != C_OK) return C_ERR;
//創建一個子進程來執行重寫指令
if ((childpid = redisFork(CHILD_TYPE_AOF)) == 0) {
char tmpfile[256];
//並不是在AOF文件中直接重寫,而是重寫到一個新的文件最終原子行替換
redisSetProcTitle("redis-aof-rewrite");
redisSetCpuAffinity(server.aof_rewrite_cpulist);
snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof", (int) getpid());
if (rewriteAppendOnlyFile(tmpfile) == C_OK) {
sendChildCowInfo(CHILD_INFO_TYPE_AOF_COW_SIZE, "AOF rewrite");
exitFromChild(0);
} else {
exitFromChild(1);
}
} else {
/* Parent */
if (childpid == -1) {
serverLog(LL_WARNING,
"Can't rewrite append only file in background: fork: %s",
strerror(errno));
aofClosePipes();
return C_ERR;
}
serverLog(LL_NOTICE,
"Background append only file rewriting started by pid %ld",(long) childpid);
server.aof_rewrite_scheduled = 0;
server.aof_rewrite_time_start = time(NULL);
server.aof_selected_db = -1;
replicationScriptCacheFlush();
return C_OK;
}
return C_OK; /* unreached */
}
- AOF重寫是不會阻塞Redis執行的,因爲創建了子進程來執行
- AOF重寫也是不會影響AOF文件的,是在一個臨時文件中寫入數據的,最終用
rename
來替換
int rewriteAppendOnlyFileRio(rio *aof) {
dictIterator *di = NULL;
dictEntry *de;
size_t processed = 0;
int j;
long key_count = 0;
long long updated_time = 0;
//逐個遍歷所有db
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);
//在db切換時記錄select命令
if (rioWrite(aof,selectcmd,sizeof(selectcmd)-1) == 0) goto werr;
if (rioWriteBulkLongLong(aof,j) == 0) goto werr;
//直接遍歷內存中的字典
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);
//存儲命令、key、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 */
if (rioWriteBulkObject(aof,&key) == 0) goto werr;
if (rioWriteBulkObject(aof,o) == 0) goto werr;
........
AOF重寫直接就是遍歷所有數據庫的所有鍵值,存儲到AOF文件中
AOF重寫緩衝區
AOF重寫過程中主進程接收到新的指令就會放入到AOF重寫緩衝區中
子進程
int rewriteAppendOnlyFile(char *filename) {
........
while(mstime()-start < 1000 && nodata < 20) {
if (aeWait(server.aof_pipe_read_data_from_parent, AE_READABLE, 1) <= 0)
{
nodata++;
continue;
}
nodata = 0; /* Start counting from zero, we stop on N *contiguous*
timeouts. */
aofReadDiffFromParent();
}
同時AOF重寫子進程會接收到重寫過程中Redis進程執行的指令並同樣記錄到文件中
父進程
void aofRewriteBufferAppend(unsigned char *s, unsigned long len) {
listNode *ln = listLast(server.aof_rewrite_buf_blocks);
........
重寫緩衝區是以鏈表實現的,一段一段的通過事件循環發送給子進程;最終如果子進程結束後還有剩餘,由主進程將數據追加寫入到重寫後的AOF文件中