redis支持2種持久化功能,分別是RDB持久化和AOF(Append Only File)持久化。
今天總結下redis加載AOF文件過程,以及加載AOF過程中遇到的問題。
由于AOF文件里面包含了重建redis數(shù)據(jù)庫狀態(tài)所需要的所有命令,因此在redis啟動過程中需要加載一次AOF文件(前提是redis配置文件中使用的是aof文件),這樣就可以還原之前的redis所有狀態(tài)了。
redis在啟動時加載AOF過程如下
int main(int argc, char **argv) {
...
// 如果服務器不是運行在 SENTINEL 模式,那么執(zhí)行以下代碼
if (!server.sentinel_mode) { //sentinel和集群只能二選1
/* Things not needed when running in Sentinel mode. */
redisLog(REDIS_WARNING,"Server started, Redis version " REDIS_VERSION);
#ifdef __linux__
linuxOvercommitMemoryWarning();
#endif
// 從 AOF 文件或者 RDB 文件中載入數(shù)據(jù)
loadDataFromDisk();
// 判斷是否啟動集群
if (server.cluster_enabled) { //在該函數(shù)前會先載入cluster配置nodes.conf,見initServer->clusterInit;
if (verifyClusterConfigWithData() == REDIS_ERR) {
redisLog(REDIS_WARNING,
"You can't have keys in a DB different than DB 0 when in "
"Cluster mode. Exiting.");
exit(1);
}
}
// 打印 TCP 端口
if (server.ipfd_count > 0)
redisLog(REDIS_NOTICE,"The server is now ready to accept connections on port %d", server.port);
// 打印本地套接字端口
if (server.sofd > 0)
redisLog(REDIS_NOTICE,"The server is now ready to accept connections at %s", server.unixsocket);
} else { //sentinel和集群只能二選1
sentinelIsRunning();
}
/* Warning the user about suspicious maxmemory setting. */
// 檢查不正常的 maxmemory 配置
if (server.maxmemory > 0 && server.maxmemory < 1024*1024) {
redisLog(REDIS_WARNING,"WARNING: You specified a maxmemory value that is less than 1MB (current value is %llu bytes). Are you sure this is what you really want?", server.maxmemory);
}
// 運行事件處理器,一直到服務器關閉為止
aeSetBeforeSleepProc(server.el,beforeSleep);
aeMain(server.el);
// 服務器關閉,停止事件循環(huán)
aeDeleteEventLoop(server.el);
return 0;
}
從上面代碼得知,redis在loadDataFromDisk()中加載AOF文件。
void loadDataFromDisk(void) { //loadDataFromDisk和rdbSave對應加載寫入
// 記錄開始時間
long long start = ustime();
// AOF 持久化是否已打開
if (server.aof_state == REDIS_AOF_ON) {
// 嘗試載入 AOF 文件
if (loadAppendOnlyFile(server.aof_filename) == REDIS_OK)
// 打印載入信息,并計算載入耗時長度
redisLog(REDIS_NOTICE,"DB loaded from append only file: %.3f seconds",(float)(ustime()-start)/1000000);
// AOF 持久化未打開
} else {
// 嘗試載入 RDB 文件
if (rdbLoad(server.rdb_filename) == REDIS_OK) {
// 打印載入信息,并計算載入耗時長度
redisLog(REDIS_NOTICE,"DB loaded from disk: %.3f seconds",
(float)(ustime()-start)/1000000);
} else if (errno != ENOENT) {
redisLog(REDIS_WARNING,"Fatal error loading the DB: %s. Exiting.",strerror(errno));
exit(1);
}
}
}
若開啟使用的是AOF持久化,則調(diào)用loadAppendOnlyFile進行加載AOF文件。
int loadAppendOnlyFile(char *filename) {
//偽客戶端
struct redisClient *fakeClient;
// 打開 AOF 文件
FILE *fp = fopen(filename,"r");、
struct redis_stat sb;
int old_aof_state = server.aof_state;
long loops = 0;
// 檢查文件的正確性
if (fp && redis_fstat(fileno(fp),&sb) != -1 && sb.st_size == 0) {
server.aof_current_size = 0;
fclose(fp);
return REDIS_ERR;
}
// 檢查文件是否正常打開
if (fp == NULL) {
redisLog(REDIS_WARNING,"Fatal error: can't open the append log file for reading: %s",strerror(errno));
exit(1);
}
/* Temporarily disable AOF, to prevent EXEC from feeding a MULTI
* to the same file we're about to read.
* 暫時性地關閉 AOF ,防止在執(zhí)行 MULTI 時,
* EXEC 命令被傳播到正在打開的 AOF 文件中。
*/
server.aof_state = REDIS_AOF_OFF;
//創(chuàng)建一個偽客戶端
fakeClient = createFakeClient();
// 設置服務器的狀態(tài)為:正在載入
startLoading(fp); //startLoading 定義于 rdb.c
while(1) {
int argc, j;
unsigned long len;
robj **argv;
char buf[128];
sds argsds;
struct redisCommand *cmd;
/* Serve the clients from time to time
* 間隔性地處理客戶端發(fā)送來的請求
* 因為服務器正處于載入狀態(tài),所以能正常執(zhí)行的只有 PUBSUB 等模塊
*/
if (!(loops++ % 1000)) {
loadingProgress(ftello(fp));
processEventsWhileBlocked();
}
// 讀入文件內(nèi)容到緩存
if (fgets(buf,sizeof(buf),fp) == NULL) {
if (feof(fp))
break; // 文件已經(jīng)讀完,跳出
else
goto readerr;
}
// 確認協(xié)議格式,比如 *3rn
if (buf[0] != '*') goto fmterr;
// 取出命令參數(shù),比如 *3rn 中的 3
argc = atoi(buf+1);
// 至少要有一個參數(shù)(被調(diào)用的命令)
if (argc < 1) goto fmterr;
// 從文本中創(chuàng)建字符串對象:包括命令,以及命令參數(shù)
// 例如 $3rnSETrn$3rnKEYrn$5rnVALUErn
// 將創(chuàng)建三個包含以下內(nèi)容的字符串對象:
// SET 、 KEY 、 VALUE
argv = zmalloc(sizeof(robj*)*argc);
for (j = 0; j < argc; j++) {
if (fgets(buf,sizeof(buf),fp) == NULL) goto readerr;
if (buf[0] != '$') goto fmterr;
// 讀取參數(shù)值的長度
len = strtol(buf+1,NULL,10);
// 讀取參數(shù)值
argsds = sdsnewlen(NULL,len);
if (len && fread(argsds,len,1,fp) == 0)
goto fmterr;
// 為參數(shù)創(chuàng)建對象
argv[j] = createObject(REDIS_STRING,argsds);
if (fread(buf,2,1,fp) == 0) goto fmterr; /* discard CRLF */
}
/* Command lookup
*查找命令
*/
cmd = lookupCommand(argv[0]->ptr);
if (!cmd) {
redisLog(REDIS_WARNING,"Unknown command '%s' reading the append only file", (char*)argv[0]->ptr);
exit(1);
}
/* Run the command in the context of a fake client
* 調(diào)用偽客戶端,執(zhí)行命令
*/
fakeClient->argc = argc;
fakeClient->argv = argv;
cmd->proc(fakeClient);
/* The fake client should not have a reply */
redisAssert(fakeClient->bufpos == 0 && listLength(fakeClient->reply) == 0);
/* The fake client should never get blocked */
redisAssert((fakeClient->flags & REDIS_BLOCKED) == 0);
/* Clean up. Command code may have changed argv/argc so we use the
* argv/argc of the client instead of the local variables.
* 清理命令和命令參數(shù)對象
*/
for (j = 0; j < fakeClient->argc; j++)
decrRefCount(fakeClient->argv[j]);
zfree(fakeClient->argv);
}
/* This point can only be reached when EOF is reached without errors.
* If the client is in the middle of a MULTI/EXEC, log error and quit.
* 如果能執(zhí)行到這里,說明 AOF 文件的全部內(nèi)容都可以正確地讀取,
* 但是,還要檢查 AOF 是否包含未正確結束的事務
*/
if (fakeClient->flags & REDIS_MULTI) goto readerr;
fclose(fp);
// 釋放偽客戶端
freeFakeClient(fakeClient);
// 復原 AOF 狀態(tài)
server.aof_state = old_aof_state;
// 停止載入
stopLoading();
// 更新服務器狀態(tài)中, AOF 文件的當前大小
aofUpdateCurrentSize();
// 記錄前一次重寫時的大小
server.aof_rewrite_base_size = server.aof_current_size;
return REDIS_OK;
// 讀入錯誤
readerr:
// 非預期的末尾,可能是 AOF 文件在寫入的中途遭遇了停機
if (feof(fp)) {
redisLog(REDIS_WARNING,"Unexpected end of file reading the append only file");
// 文件內(nèi)容出錯
} else {
redisLog(REDIS_WARNING,"Unrecoverable error reading the append only file: %s", strerror(errno));
}
exit(1);
// 內(nèi)容格式錯誤
fmterr:
redisLog(REDIS_WARNING,"Bad file format reading the append only file: make a backup of your AOF file, then use ./redis-check-aof --fix <filename>");
exit(1);
}
由于AOF文件中保存的都是用戶可見的一條條命令,因此loadAppendOnlyFile通過創(chuàng)建一個偽客戶端讀取AOF文件獲取一條條命令來恢復執(zhí)行。
redis讀取AOF文件并還原數(shù)據(jù)庫的狀態(tài)過程如下:
以上就是服務器根據(jù)讀入AOF文件進行還原數(shù)據(jù)庫狀態(tài)的原理。
從上面的介紹可以知道加載AOF文件是redis啟動過程中的步驟,當業(yè)務線程啟動連接redis獲取或者保存數(shù)據(jù)時,若redis正在加載數(shù)據(jù),則有可能獲取不到數(shù)據(jù):
127.0.0.1:6379> get a
(error) LOADING Redis is loading the dataset in memory
原因是當加載過程中,有些客戶端發(fā)來的命令是不執(zhí)行的,直接返回上述結果。只有命令帶有"l"(小寫的L)標志的才會執(zhí)行,比如info命令等。
那么怎么判斷AOF文件是否加載完成呢?
剛才說到加載時可以執(zhí)行info命令,redis info 命令以一種易于理解和閱讀的格式,返回關于 Redis 服務器的各種信息和統(tǒng)計數(shù)值。該統(tǒng)計信息中有Persistence 參數(shù),該參數(shù)中記錄著 RDB 和 AOF 的相關信息,其中有個loading字段標志這數(shù)據(jù)是否正在加載,當正在加載時為1,加載完為0,因此可以根據(jù)該字段來判斷數(shù)據(jù)是否加載完畢。
127.0.0.1:6379> info Persistence
# Persistence
loading:1 // 1表示正在加載數(shù)據(jù)
...
127.0.0.1:6379> info Persistence
# Persistence
loading:0 // 0表示數(shù)據(jù)加載完畢
...
因此業(yè)務線程可以根據(jù)info命令獲取loading進行判斷數(shù)據(jù)是否加載完畢。
public boolean isLoading(){
try{
String info = (String)redisTemplate.getRequiredConnectionFactory().getConnection()
.info("Persistence").get("loading");
if("0".equalsIgnoreCase(info)){
return true;
}
}catch (Exception e){
error("Failed to get loading status : {}", e.getMessage());
}
return false;
}