那麼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