之前敘述了Redis中的數據庫鍵值對存儲方式以及鍵過期策略。本篇文章將着重介紹一下Redis中的數據持久化策略,這也是Redis中的重中之重。衆所周知,Redis是一種基於內存KV數據庫,也就是說一般情況下,數據都是存儲在內存中,好處在於存取效率相比於磁盤數據庫要高很多,缺點在於內存的易失性,當斷電或者系統崩潰時內存中的數據是無法被記錄下的,也就是可靠性較差。
目前Redis提供兩種持久化方案,一種是RDB,另一種是AOF,兩種策略都可靠但是在原理以及用法上大相徑庭。RDB持久化是通過記錄數據庫的瞬時狀態來實現,默認情況下使用RDB進行持久化的,RDB文件恢復數據庫狀態比AOF文件要效率高,AOF持久化是通過記錄增量的方式實現,由於AOF持久化方案的更新頻率通常比RDB方案高,更加可靠,所以當在redis.conf中開啓appendonly的時候,系統就會優先使用AOF文件來恢復數據庫狀態。
appendonly yes
RDB持久化
RDB持久化本質上類似snapshot的方法,就是將某個時間節點的數據庫狀態用二進制文件保存到磁盤中,當加載RDB二進制文件後就又可以恢復原先的狀態。RDB持久化有兩種觸發機制,一種是手動用SAVE命令和BGSAVE命令實現,另一種是通過配置文件中save選項自動化實現。接下來對兩種分別進行介紹。
SAVE和BGSAVE
SAVE命令和BGSAVE命令都是手動執行RDB持久化的方法,區別在於SAVE命令是在服務器進程中完成的,所以執行命令的過程中服務器無法響應客戶端的其他命令,只能阻塞,而BGSAVE是在子進程中實現RDB持久化,Redis服務器依舊能繼續處理客戶端的請求。接下來看一下具體實現。
void saveCommand(redisClient *c) {
// BGSAVE 已經在執行中,不能再執行 SAVE
// 否則將產生競爭條件
if (server.rdb_child_pid != -1) {
addReplyError(c,"Background save already in progress");
return;
}
// 執行
if (rdbSave(server.rdb_filename) == REDIS_OK) {
addReply(c,shared.ok);
} else {
addReply(c,shared.err);
}
}
void bgsaveCommand(redisClient *c) {
// 不能重複執行 BGSAVE
if (server.rdb_child_pid != -1) {
addReplyError(c,"Background save already in progress");
// 不能在 BGREWRITEAOF 正在運行時執行
} else if (server.aof_child_pid != -1) {
addReplyError(c,"Can't BGSAVE while AOF log rewriting is in progress");
// 執行 BGSAVE
addReplyStatus(c,"Background saving started");
} else {
addReply(c,shared.err);
}
}
/*
* 將數據庫保存到磁盤上。
* 保存成功返回 REDIS_OK ,出錯/失敗返回 REDIS_ERR 。
*/
int rdbSave(char *filename) {
dictIterator *di = NULL;
dictEntry *de;
char tmpfile[256];
char magic[10];
int j;
long long now = mstime();
FILE *fp;
rio rdb;
uint64_t cksum;
// 創建臨時文件
snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
fp = fopen(tmpfile,"w");
if (!fp) {
redisLog(REDIS_WARNING, "Failed opening .rdb for saving: %s",
strerror(errno));
return REDIS_ERR;
}
// 初始化 I/O
rioInitWithFile(&rdb,fp);
// 設置校驗和函數
if (server.rdb_checksum)
rdb.update_cksum = rioGenericUpdateChecksum;
// 寫入 RDB 版本號
snprintf(magic,sizeof(magic),"REDIS%04d",REDIS_RDB_VERSION);
if (rdbWriteRaw(&rdb,magic,9) == -1) goto werr;
// 遍歷所有數據庫
for (j = 0; j < server.dbnum; j++) {
// 指向數據庫
redisDb *db = server.db+j;
// 指向數據庫鍵空間
dict *d = db->dict;
// 跳過空數據庫
if (dictSize(d) == 0) continue;
// 創建鍵空間迭代器
di = dictGetSafeIterator(d);
if (!di) {
fclose(fp);
return REDIS_ERR;
}
/*
* 寫入 DB 選擇器
*/
if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_SELECTDB) == -1) goto werr;
if (rdbSaveLen(&rdb,j) == -1) goto werr;
/*
* 遍歷數據庫,並寫入每個鍵值對的數據
*/
while((de = dictNext(di)) != NULL) {
sds keystr = dictGetKey(de);
robj key, *o = dictGetVal(de);
long long expire;
// 根據 keystr ,在棧中創建一個 key 對象
initStaticStringObject(key,keystr);
// 獲取鍵的過期時間
expire = getExpire(db,&key);
// 保存鍵值對數據
if (rdbSaveKeyValuePair(&rdb,&key,o,expire,now) == -1) goto werr;
}
dictReleaseIterator(di);
}
di = NULL; /* So that we don't release it again on error. */
/*
* 寫入 EOF 代碼
*/
if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_EOF) == -1) goto werr;
/*
* CRC64 校驗和。
* 如果校驗和功能已關閉,那麼 rdb.cksum 將爲 0 ,
* 在這種情況下, RDB 載入時會跳過校驗和檢查。
*/
cksum = rdb.cksum;
memrev64ifbe(&cksum);
rioWrite(&rdb,&cksum,8);
// 沖洗緩存,確保數據已寫入磁盤
if (fflush(fp) == EOF) goto werr;
if (fsync(fileno(fp)) == -1) goto werr;
if (fclose(fp) == EOF) goto werr;
/*
* 使用 RENAME ,原子性地對臨時文件進行改名,覆蓋原來的 RDB 文件。
*/
if (rename(tmpfile,filename) == -1) {
redisLog(REDIS_WARNING,"Error moving temp DB file on the final destination: %s", strerror(errno));
unlink(tmpfile);
return REDIS_ERR;
}
// 寫入完成,打印日誌
redisLog(REDIS_NOTICE,"DB saved on disk");
// 清零數據庫髒狀態
server.dirty = 0;
// 記錄最後一次完成 SAVE 的時間
server.lastsave = time(NULL);
// 記錄最後一次執行 SAVE 的狀態
server.lastbgsave_status = REDIS_OK;
return REDIS_OK;
werr:
// 關閉文件
fclose(fp);
// 刪除文件
unlink(tmpfile);
redisLog(REDIS_WARNING,"Write error saving DB on disk: %s", strerror(errno));
if (di) dictReleaseIterator(di);
return REDIS_ERR;
}
int rdbSaveBackground(char *filename) {
pid_t childpid;
long long start;
// 如果 BGSAVE 已經在執行,那麼出錯
if (server.rdb_child_pid != -1) return REDIS_ERR;
// 記錄 BGSAVE 執行前的數據庫被修改次數
server.dirty_before_bgsave = server.dirty;
// 最近一次嘗試執行 BGSAVE 的時間
server.lastbgsave_try = time(NULL);
// fork() 開始前的時間,記錄 fork() 返回耗時用
start = ustime();
if ((childpid = fork()) == 0) {
int retval;
/* Child */
// 關閉網絡連接 fd
closeListeningSockets(0);
// 設置進程的標題,方便識別
redisSetProcTitle("redis-rdb-bgsave");
// 執行保存操作
retval = rdbSave(filename);
// 打印 copy-on-write 時使用的內存數
if (retval == REDIS_OK) {
size_t private_dirty = zmalloc_get_private_dirty();
if (private_dirty) {
redisLog(REDIS_NOTICE,
"RDB: %zu MB of memory used by copy-on-write",
private_dirty/(1024*1024));
}
}
// 向父進程發送信號
exitFromChild((retval == REDIS_OK) ? 0 : 1);
} else {
/* Parent */
// 計算 fork() 執行的時間
server.stat_fork_time = ustime()-start;
// 如果 fork() 出錯,那麼報告錯誤
if (childpid == -1) {
server.lastbgsave_status = REDIS_ERR;
redisLog(REDIS_WARNING,"Can't save in background: fork: %s",
strerror(errno));
return REDIS_ERR;
}
// 打印 BGSAVE 開始的日誌
redisLog(REDIS_NOTICE,"Background saving started by pid %d",childpid);
// 記錄數據庫開始 BGSAVE 的時間
server.rdb_save_time_start = time(NULL);
// 記錄負責執行 BGSAVE 的子進程 ID
server.rdb_child_pid = childpid;
// 關閉自動 rehash
updateDictResizePolicy();
return REDIS_OK;
}
return REDIS_OK; /* unreached */
}
由上面的實現過程可以看到,在redisServer結構體中維持了dirty變量和lastsave變量,dirty變量表示距離上次執行SAVE命令或BGSAVE命令後數據庫修改的次數,lastsave是上次執行SAVE命令或BGSAVE命令的時間戳。這兩個變量的功能是實現下面的自動RDB持久化。
自動RDB持久化
如果在redis.conf配置文件中出現如下的save配置項,就是會默認在滿足下面三個條件之一的時候執行BGSAVE命令:
save 900 1
save 300 10
save 60 10000
//每個條件用saveparam結構體表示
struct saveparam{
time_t seconds; //秒數
int changes; //修改數
};
- 服務器在900秒之內對數據庫進行至少1次修改
- 服務器在300秒之內對數據庫進行至少10次修改
- 服務器在60秒之內對數據庫進行至少10000次修改
serverCron函數會默認每隔100ms循環檢查以上三個條件是否滿足,如果nowtime - lastsave >= save.time && save.changes >= dirty 則執行BGSAVE命令。
RDB文件
從上面的rdbSave函數中可以看到寫入RDB文件的順序。具體RDB文件格式如下圖所示。如果鍵值對有expiredtime(並且沒有過期)則將過期時間也放入RDB文件。
/*
* 將鍵值對的鍵、值、過期時間和類型寫入到 RDB 中。
* 出錯返回 -1 。
* 成功保存返回 1 ,當鍵已經過期時,返回 0 。
*/
int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val,
long long expiretime, long long now)
{
/*
* 保存鍵的過期時間
*/
if (expiretime != -1) {
/*
* 不寫入已經過期的鍵
*/
if (expiretime < now) return 0; //已經過期的鍵值對就不會放到RDB文件中
if (rdbSaveType(rdb,REDIS_RDB_OPCODE_EXPIRETIME_MS) == -1) return -1;
if (rdbSaveMillisecondTime(rdb,expiretime) == -1) return -1;
}
/*
* 保存類型,鍵,值
*/
if (rdbSaveObjectType(rdb,val) == -1) return -1;
if (rdbSaveStringObject(rdb,key) == -1) return -1;
if (rdbSaveObject(rdb,val) == -1) return -1;
return 1;
}
/*
* 將對象的不同類型以及編碼方式寫入RDB文件中
*/
int rdbSaveObjectType(rio *rdb, robj *o) {
switch (o->type) {
case REDIS_STRING:
return rdbSaveType(rdb,REDIS_RDB_TYPE_STRING);
case REDIS_LIST:
if (o->encoding == REDIS_ENCODING_ZIPLIST)
return rdbSaveType(rdb,REDIS_RDB_TYPE_LIST_ZIPLIST);
else if (o->encoding == REDIS_ENCODING_LINKEDLIST)
return rdbSaveType(rdb,REDIS_RDB_TYPE_LIST);
else
redisPanic("Unknown list encoding");
case REDIS_SET:
if (o->encoding == REDIS_ENCODING_INTSET)
return rdbSaveType(rdb,REDIS_RDB_TYPE_SET_INTSET);
else if (o->encoding == REDIS_ENCODING_HT)
return rdbSaveType(rdb,REDIS_RDB_TYPE_SET);
else
redisPanic("Unknown set encoding");
case REDIS_ZSET:
if (o->encoding == REDIS_ENCODING_ZIPLIST)
return rdbSaveType(rdb,REDIS_RDB_TYPE_ZSET_ZIPLIST);
else if (o->encoding == REDIS_ENCODING_SKIPLIST)
return rdbSaveType(rdb,REDIS_RDB_TYPE_ZSET);
else
redisPanic("Unknown sorted set encoding");
case REDIS_HASH:
if (o->encoding == REDIS_ENCODING_ZIPLIST)
return rdbSaveType(rdb,REDIS_RDB_TYPE_HASH_ZIPLIST);
else if (o->encoding == REDIS_ENCODING_HT)
return rdbSaveType(rdb,REDIS_RDB_TYPE_HASH);
else
redisPanic("Unknown hash encoding");
default:
redisPanic("Unknown object type");
}
return -1;
}
/*
* 將給定對象 o 保存到 rdb 中。
* 保存成功返回 rdb 保存該對象所需的字節數 ,失敗返回 0 。
*/
int rdbSaveObject(rio *rdb, robj *o) {
int n, nwritten = 0;
// 保存字符串對象
if (o->type == REDIS_STRING) {
/* Save a string value */
if ((n = rdbSaveStringObject(rdb,o)) == -1) return -1;
nwritten += n;
// 保存列表對象
} else if (o->type == REDIS_LIST) {
/* Save a list value */
if (o->encoding == REDIS_ENCODING_ZIPLIST) {
size_t l = ziplistBlobLen((unsigned char*)o->ptr)
// 以字符串對象的形式保存整個 ZIPLIST 列表
if ((n = rdbSaveRawString(rdb,o->ptr,l)) == -1) return -1;
nwritten += n;
} else if (o->encoding == REDIS_ENCODING_LINKEDLIST) {
list *list = o->ptr;
listIter li;
listNode *ln;
if ((n = rdbSaveLen(rdb,listLength(list))) == -1) return -1;
nwritten += n;
// 遍歷所有列表項
listRewind(list,&li);
while((ln = listNext(&li))) {
robj *eleobj = listNodeValue(ln);
// 以字符串對象的形式保存列表項
if ((n = rdbSaveStringObject(rdb,eleobj)) == -1) return -1;
nwritten += n;
}
} else {
redisPanic("Unknown list encoding");
}
// 保存集合對象
} else if (o->type == REDIS_SET) {
/* Save a set value */
if (o->encoding == REDIS_ENCODING_HT) {
dict *set = o->ptr;
dictIterator *di = dictGetIterator(set);
dictEntry *de;
if ((n = rdbSaveLen(rdb,dictSize(set))) == -1) return -1;
nwritten += n;
// 遍歷集合成員
while((de = dictNext(di)) != NULL) {
robj *eleobj = dictGetKey(de);
// 以字符串對象的方式保存成員
if ((n = rdbSaveStringObject(rdb,eleobj)) == -1) return -1;
nwritten += n;
}
dictReleaseIterator(di);
} else if (o->encoding == REDIS_ENCODING_INTSET) {
size_t l = intsetBlobLen((intset*)o->ptr);
// 以字符串對象的方式保存整個 INTSET 集合
if ((n = rdbSaveRawString(rdb,o->ptr,l)) == -1) return -1;
nwritten += n;
} else {
redisPanic("Unknown set encoding");
}
// 保存有序集對象
} else if (o->type == REDIS_ZSET) {
if (o->encoding == REDIS_ENCODING_ZIPLIST) {
size_t l = ziplistBlobLen((unsigned char*)o->ptr);
// 以字符串對象的形式保存整個 ZIPLIST 有序集
if ((n = rdbSaveRawString(rdb,o->ptr,l)) == -1) return -1;
nwritten += n;
} else if (o->encoding == REDIS_ENCODING_SKIPLIST) {
zset *zs = o->ptr;
dictIterator *di = dictGetIterator(zs->dict);
dictEntry *de;
if ((n = rdbSaveLen(rdb,dictSize(zs->dict))) == -1) return -1;
nwritten += n;
// 遍歷有序集
while((de = dictNext(di)) != NULL) {
robj *eleobj = dictGetKey(de);
double *score = dictGetVal(de);
// 以字符串對象的形式保存集合成員
if ((n = rdbSaveStringObject(rdb,eleobj)) == -1) return -1;
nwritten += n;
// 成員分值(一個雙精度浮點數)會被轉換成字符串
// 然後保存到 rdb 中
if ((n = rdbSaveDoubleValue(rdb,*score)) == -1) return -1;
nwritten += n;
}
dictReleaseIterator(di);
} else {
redisPanic("Unknown sorted set encoding");
}
// 保存哈希表
} else if (o->type == REDIS_HASH) {
if (o->encoding == REDIS_ENCODING_ZIPLIST) {
size_t l = ziplistBlobLen((unsigned char*)o->ptr);
// 以字符串對象的形式保存整個 ZIPLIST 哈希表
if ((n = rdbSaveRawString(rdb,o->ptr,l)) == -1) return -1;
nwritten += n;
} else if (o->encoding == REDIS_ENCODING_HT) {
dictIterator *di = dictGetIterator(o->ptr);
dictEntry *de;
if ((n = rdbSaveLen(rdb,dictSize((dict*)o->ptr))) == -1) return -1;
nwritten += n;
// 迭代字典
while((de = dictNext(di)) != NULL) {
robj *key = dictGetKey(de);
robj *val = dictGetVal(de);
// 鍵和值都以字符串對象的形式來保存
if ((n = rdbSaveStringObject(rdb,key)) == -1) return -1;
nwritten += n;
if ((n = rdbSaveStringObject(rdb,val)) == -1) return -1;
nwritten += n;
}
dictReleaseIterator(di);
} else {
redisPanic("Unknown hash encoding");
}
} else {
redisPanic("Unknown object type");
}
return nwritten;
}
AOF持久化
AOF持久化是通過記錄Redis服務器所執行的寫命令來記錄服務器的狀態。除了增量式記錄,AOF持久化與RDB持久化最大區別在於AOF持久化的實時性較好。由上面對RDB持久化方法的描述可知,如果不考慮RDB手動持久化,默認情況下RDB自動持久化的最快時間也要到60秒,而AOF基本上可以做到每秒持久化。
AOF持久化實現
AOF持久化實現可以分成三個步驟:命令追加,文件寫入,文件同步。三者關係如下圖所示。接下來會一一進行詳細介紹。
- 命令追加。每當服務器執行完一個寫命令都會將其按照協議格式追加到AOF緩衝區末尾。
- 文件寫入。這裏指的是調用write函數將AOF緩衝區數據寫入內核緩衝區,然後就立刻返回(非阻塞)。
- 文件同步。這裏指的是將內核緩衝區的數據調用fsync系統調用寫入系統磁盤。Redis有三種持久化策略:always、everysec、no。always表示將AOF緩衝區的數據寫入並立刻fsync同步到磁盤文件,everysec表示將AOF緩衝區的數據寫入內核緩衝區,但是每隔1s調用fsync系統調用寫入同步,no表示將AOF緩衝區數據寫入內核緩衝區,何時同步由操作系統決定。三種同步方式比較:用fsync週期越頻繁,讀寫效率就越差,但是相應的安全性越高,發生宕機時丟失的數據越少。
AOF文件載入並數據還原的過程比較簡單,主要過程就是用僞客戶端從AOF文件中一條一條讀取寫命令,然後將命令傳輸到服務器端,服務器端執行即可。
AOF重寫
當系統運行時間過長就會導致AOF文件過大,有時候會影響系統讀寫AOF文件性能,這時候就需要AOF重寫,簡單來說就是將AOF文件壓縮,把當前數據庫的狀態重寫到AOF文件中並更改名字。
當有大量寫操作的時候就會導致調用AOF重寫的線程被長時間阻塞,無法響應客戶端的命令,因此可以將AOF重寫程序放到子進程中進行(這也是BGREWRITEAOF所做的),同時設置一個AOF重寫緩衝區,作用是:在子進程執行AOF重寫過程中,對於客戶端發來的命令,要同時追加到AOF重寫緩衝區和AOF緩衝區,當子進程完成AOF重寫工作後,會將AOF重寫緩衝區中的所有內容寫到新的AOF文件中,此時新AOF文件的狀態就和數據庫中的狀態一致。大概瞭解重寫過程後看看源碼實現。
/*
* 將一集足以還原當前數據集的命令寫入到 filename 指定的文件中。
* 這個函數被 REWRITEAOF 和 BGREWRITEAOF 兩個命令調用。
* (REWRITEAOF 似乎已經是一個廢棄的命令)
* 爲了最小化重建數據集所需執行的命令數量,
* Redis 會盡可能地使用接受可變參數數量的命令,比如 RPUSH 、SADD 和 ZADD 等。
* 不過單個命令每次處理的元素數量不能超過 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();
/*
* 創建臨時文件
* 注意這裏創建的文件名和 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;
}
// 初始化文件 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 命令,確保之後的數據會被插入到正確的數據庫上
*/
if (rioWrite(&aof,selectcmd,sizeof(selectcmd)-1) == 0) goto werr;
if (rioWriteBulkLongLong(&aof,j) == 0) goto werr;
/*
* 遍歷數據庫所有鍵,並通過命令將它們的當前狀態(值)記錄到新 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 (expiretime != -1 && expiretime < now) continue;
/*
* 根據值的類型,選擇適當的命令來保存值
*/
if (o->type == REDIS_STRING) {
char cmd[]="*3\r\n$3\r\nSET\r\n";
if (rioWrite(&aof,cmd,sizeof(cmd)-1) == 0) goto werr;
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");
}
/*
* 保存鍵的過期時間
*/
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);
}
// 沖洗並關閉新 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;
}
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;
}
參考: