網上分析Redis源碼的文章挺多,如黃健宏的《Redis設計與實現》就很詳盡的分析了redis源碼,很贊。前不久看到Paul Smith的較早年份的大作《Redis:under the hood》,受益匪淺,如此從整體上對redis原理有個大的把控,不過多糾結於細節,甚好。這裏我用的版本是redis2.4.18版本,跟Paul Smith的版本有所不同,不過主體流程沒有太多變化,這篇文章基本是對 《Redis:under the hood》的翻譯和註解。
1 啓動
讓我們從 src/redis.c 中的main() 函數開始。
全局服務器狀態初始化
首先, initServerConfig()
函數被調用。這部分主要是初始化 server
變量,它是一個 struct redisServer
類型,用於保存redis服務器全局狀態。
// redis.h:388
struct redisServer {
pthread_t mainthread;
int arch_bits;
int port;
char *bindaddr;
char *unixsocket;
mode_t unixsocketperm;
int ipfd;
int sofd;
redisDb *db;
list *clients; // 客戶端列表
dict *commands; // 命令字典,key爲命令名如get,值爲redisCommand類型。
unsigned lruclock:22; /* clock incrementing every minute, for LRU */
unsigned lruclock_padding:10;
...
}
// redis.c:71
struct redisServer server;
redisServer結構體有很多成員,主要可以分爲下面幾種類型:
- 全局的服務器狀態
- 統計信息
- 配置信息
- 複製信息
- 排序參數
- 虛擬內存配置,狀態,I/O線程以及統計
- zip結構體(默認ziplist,zipmap大小等)
- event loop helpers
- pub/sub
initServerConfig()
的作用是設置redis server的默認配置。
- 如設置redis的run_id,設置端口爲6379。
- 設置db名爲
dump.rdb
,pid文件目錄爲/var/run/redis.pid
,關閉aof。 - 關閉daemon模式,過期數據淘汰策略爲LRU。
- 設置保存策略
saveparams
爲 1小時有1個值變化就保存,5分鐘有100個值變化保存以及1分鐘有1萬個值變化保存。 - 設置
double R_PosInf = 1.0/0.0
,好吧,用python的時候除0.0會拋異常,C裏面不會,這裏會變成無窮大。同理,還可以設置無窮小等。 - 創建
commandTableDictType
類型的字典commands,然後調用populateCommandTable()
函數來初始化redis命令集。redis的字典dict
的實現採用的也是經典的哈希表實現,衝突的鍵通過哈希表串聯。 - 設置慢日誌記錄條件,默認是
REDIS_SLOWLOG_LOG_SLOWER_THAN
即命令執行時間超過10000毫秒(10秒)才記錄慢日誌。慢日誌記錄條數默認爲128。
設置redis命令表
上一節提到redis的命令存儲在server.commands這個字典中,其中key爲命令名字,value爲 redisCommand
結構體,其定義如下:
// redis.c:73
struct redisCommand readonlyCommandTable[] = {
{"get",getCommand,2,0,NULL,1,1,1},
{"set",setCommand,3,REDIS_CMD_DENYOOM,NULL,0,0,0},
{"setnx",setnxCommand,3,REDIS_CMD_DENYOOM,NULL,0,0,0},
{"setex",setexCommand,4,REDIS_CMD_DENYOOM,NULL,0,0,0},
{"append",appendCommand,3,REDIS_CMD_DENYOOM,NULL,1,1,1},
{"strlen",strlenCommand,2,0,NULL,1,1,1},
{"del",delCommand,-2,0,NULL,0,0,0},
...
}
// redis.c:561
typedef void redisCommandProc(redisClient *c);
// redis.c:563
struct redisCommand {
char *name; // 命令名,如get
redisCommandProc *proc; // 命令對應函數,如getCommand
int arity; // 參數個數,如2
int flags; // 標記,如set命令爲REDIS_CMD_DENYOOM,表示在內存不夠時不再處理set命令。
redisVmPreloadProc *vm_preload_proc;
int vm_firstkey; /* The first argument that's a key (0 = no keys) */
int vm_lastkey; /* THe last argument that's a key */
int vm_keystep; /* The step between first and last key */
};
其中 readonlyCommandTable數組就是命令集合,redisCommand各字段分別是命名名,命令函數,參數個數,oom標記以及vm相關參數。
解析配置文件並更新配置
接下來會判斷啓動參數個數,如果參數個數爲2:
- 第二個參數是
-v/--version
則顯示版本信息,若是--help
顯示幫助信息。如果是其他,則標識是配置文件,則解析配置文件並更新server的配置。 - 如果超過2個參數,則會判斷是否是測試內存的命令,如果是,則測試內存,否則顯示幫助信息。
解析配置文件是loadServerConfig()
函數完成的,通過fgets一行行讀取redis配置文件
並更新服務器的配置。在這個函數裏面可以看到若干的if else
語句,一些所謂的編程書籍不提倡這樣,包括goto使用等,然而大師級的程序員並不在意這些細節,所謂編程無定法,境界高就是可以爲所欲爲的。
從代碼中可以發現,redis配置也可以不指定配置文件,而是在標準輸入指定,運行src/redis-server -
,然後在命令行輸入配置即可。
開啓daemon
如果配置了以守護進程運行,則會調用daemonize()
函數通過fork()
創建子進程然後子進程調用setsid()
創建一個新的會話,並將標準輸入輸出,錯誤輸出衝形象到/dev/null
。
有些書上會寫運行守護進程要fork()兩次,其實通過setsid()創建了新的會話的話,沒有必要fork()兩次,redis就是這麼做的。
如果以守護進程運行,後面還需調用createPidFile()
創建pid文件,默認路徑是/var/run/redis.pid
。
初始化服務器
在上面步驟完成後,就調用initServer()
函數來初始化服務器了。
信號設置
先是信號設置。忽略SIGHUP, SIGPIPE信號,然後通過setupSignalHandlers()
設置TERM信號等處理函數爲 sigtermHandler()等。
成員初始化
接着是初始化server成員變量,如客戶端鏈表clients,slave鏈表slaves,這些列表都是雙向鏈表struct list
。
創建事件循環對象
接着是調用aeCreateEventLoop()
創建Event Loop
並賦值給 server.el
變量。它的類型是 aeEventLoop
,定義如下:
// ae.h:89
typedef struct aeEventLoop {
int maxfd;
long long timeEventNextId;
aeFileEvent events[AE_SETSIZE]; /* Registered events */
aeFiredEvent fired[AE_SETSIZE]; /* Fired events */
aeTimeEvent *timeEventHead;
int stop;
void *apidata; /* This is used for polling API specific data */
aeBeforeSleepProc *beforesleep;
} aeEventLoop;
/* File event structure */
typedef struct aeFileEvent {
int mask; /* one of AE_(READABLE|WRITABLE) */
aeFileProc *rfileProc;
aeFileProc *wfileProc;
void *clientData;
} aeFileEvent;
/* Time event structure */
typedef struct aeTimeEvent {
long long id; /* time event identifier. */
long when_sec; /* seconds */
long when_ms; /* milliseconds */
aeTimeProc *timeProc;
aeEventFinalizerProc *finalizerProc;
void *clientData;
struct aeTimeEvent *next;
} aeTimeEvent;
/* A fired event */
typedef struct aeFiredEvent {
int fd;
int mask;
} aeFiredEvent;
aeEventLoop
包括時間事件鏈表頭 timeEventHead
,文件事件數組 aeFileEvent
數組,以及待處理的文件事件數組 aeFiredEvent
(AE_SETSIZE爲10240)。
可以看到文件事件結構體aeFileEvent中字段mask是標記,表示事件類型讀/寫。而rfileProc則是讀取事件處理函數,而wfileProc是寫事件處理函數。而待處理的文件事件 aeFiredEvent
則只包含了需要處理的文件描述符fd和它的讀寫標記mask。
而時間事件則是 aeTimeEvent
類型,存儲的包括時間事件ID,時間事件執行時間(秒 when_sec 和 毫秒 when_ms),此外還有時間事件的處理函數 timeProc 等。這是一個單向鏈表結構,next指向下一個時間事件,時間事件和文件事件最後都是在redis服務器的大循環中處理的。
服務器監聽
如果指定了端口,則會啓動anetTcpServer並開始監聽。監聽端口默認爲6379,配置文件可以指定綁定的ip和端口。對應文件描述符爲ipfd。如果是設置的unixsocket,則啓動anetUnixServer,對應文件描述符爲sofd。
這裏跟我們平時寫WEB服務器程序基本一致,只是稍作了封裝,流程也是通用的socket(),bind(),listen()。
int anetTcpServer(char *err, int port, char *bindaddr)
{
int s;
struct sockaddr_in sa;
if ((s = anetCreateSocket(err,AF_INET)) == ANET_ERR)
return ANET_ERR;
memset(&sa,0,sizeof(sa));
sa.sin_family = AF_INET;
sa.sin_port = htons(port);
sa.sin_addr.s_addr = htonl(INADDR_ANY);
if (bindaddr && inet_aton(bindaddr, &sa.sin_addr) == 0) {
anetSetError(err, "invalid bind address");
close(s);
return ANET_ERR;
}
if (anetListen(err,s,(struct sockaddr*)&sa,sizeof(sa)) == ANET_ERR)
return ANET_ERR;
return s;
}
數據庫
initServer()
中還完成了數據庫初始化。默認是16個db,對應類型爲dbDictType,id爲0-15。此外還要初始化過期鍵字典expires,阻塞鍵字典 blocking_keys,觀察鍵字典 watched_keys等。
typedef struct redisDb {
dict *dict; /* The keyspace for this DB */
dict *expires; /* Timeout of keys with a timeout set */
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP) */
dict *io_keys; /* Keys with clients waiting for VM I/O */
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
int id;
} redisDb;
redis的持久化分爲兩種方式,rdb和aof。其中rdb是使用二進制格式存儲數據,包括鍵值類型和數據,過期時間等,saveparams裏面指定存儲條件,詳細格式說明見 Redis-RDB-Format。aof則是按redis命令存儲,重啓後可以按照aof中的命令重放恢復數據。
註冊時間事件
接着在server.el中註冊時間事件。這個就是1毫秒後要開始執行的事件 serverCron()
,主要是爲了在服務器啓動後馬上運行它,後面該函數100毫秒執行一次。這個函數要做很多事情,主要包括:
- 緩存當前時間,因爲在LRU和VM訪問對象時都要記錄訪問時間,每次調用
time(NULL)
開銷太大,而緩存這個時間並不會有很大影響。 - 更新LRUClock值,這個用於LRU策略。
redisServer
有一個全局的lruclock
,該時鐘每100ms更新一次。雖然lru用了22位,但是因爲它最大爲REDIS_LRU_CLOCK_MAX((1<<21)-1)
,其實是只用到21位,精度爲10秒,所以它每242天會重新開始計時(跟redis源碼註釋中說的略有不同,註釋說22位的話wrap時間爲1.5年左右,但其實最大是用了21位)。而每個redisObject
也有一個自己的 lruclock,這樣在使用內存超過maxmemory之後就可以根據全局時鐘和每個redisObject的時鐘進行比較,確定是否淘汰。這裏有個問題是,因爲LRUClock每隔242天會重置,所以可能會導致一些很久沒有訪問的鍵它的lru更大,不過這個沒有太大問題,一個鍵這麼久沒有訪問,說明不太活躍。 - 如果達到了條件,執行BGSAVE(根據save配置來決定)和AOF文件重寫。BGSAVE和AOF重寫都是在子進程中執行的,這樣不會影響redis主進程繼續處理請求,見
rdbSaveBackground()
。注意,aof文件定期刷磁盤主要在beforeSleep中通過後臺IO線程執行,serverCron只是在對aof刷磁盤操作推遲時做些處理。 - 打印統計信息。如key的數目,設置了過期時間的key的數目,連接的client數目,slave數目以及內存使用情況等,統計信息每50個循環(50*100ms=5秒)打印一次。
- 還有resize 哈希表,關閉超時客戶端連接,後臺的AOF重寫,BGSAVE(如多少秒內有多少個鍵發生了變化執行的保存操作)。
- 計算LRU信息並刪除一部分過期的鍵,如果開啓了vm的話還要swap一些鍵值到磁盤上。
- 如果是slave,還需要從master同步數據。
註冊文件事件
那之前我們創建了一個TcpServer而且已經開始監聽,需要對客戶端連接事件進行註冊和處理。這在Linux上面是通過 epoll 來實現的。
aeCreateFileEvent()
函數主要是設置aeFileEvent結構體的值,包括指定該文件事件是讀還是寫,根據讀寫事件指定對應的處理函數 rfileProc和 wfileProc。這裏對tcp服務器指定的函數是 acceptTcpHandler()
。最終都是通過 aeApiAddEvent()
函數使用 epoll_ctl()
將 tcp socket的fd註冊到epoll中,客戶端連接的命令處理都是在 acceptTcpHandler()
中完成,這個函數後面分析。
// redis.c:980
aeCreateFileEvent(server.el,server.ipfd,AE_READABLE, acceptTcpHandler,NULL);
// ae.c:88
typedef struct aeFileEvent {
int mask; /* one of AE_(READABLE|WRITABLE) */
aeFileProc *rfileProc;
aeFileProc *wfileProc;
void *clientData;
} aeFileEvent;
// ae_epoll.c:29
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
aeApiState *state = eventLoop->apidata;
struct epoll_event ee;
/* If the fd was already monitored for some event, we need a MOD
* operation. Otherwise we need an ADD operation. */
int op = eventLoop->events[fd].mask == AE_NONE ?
EPOLL_CTL_ADD : EPOLL_CTL_MOD;
ee.events = 0;
mask |= eventLoop->events[fd].mask; /* Merge old events */
if (mask & AE_READABLE) ee.events |= EPOLLIN;
if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
ee.data.u64 = 0; /* avoid valgrind warning */
ee.data.fd = fd;
if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;
return 0;
}
其他
- 如果配置了AOF的話,接着會打開AOF文件。
- 如果是32位機器,且沒有配置maxmemory的話,會將maxmemory設置爲3.5G,數據淘汰策略爲
REDIS_MAXMEMORY_NO_EVICTION
,這個選項的意思是禁止刪除數據,永遠不過期,只會對寫操作返回一個錯誤
,這也是默認的數據淘汰數據。 - 如果開啓了vm,則會調用
vmInit()
初始化vm模塊。 - 調用
slowlogInit()
初始化slowlog。 - 調用
bioInit()
初始化後臺IO線程,一共兩個後臺線程,其中一個用於關閉文件,一個用於定期將AOF文件刷到磁盤。redis的AOF刷新策略有ALWAYS,EVERYSEC,NO,默認是EVERYSEC。ALWAYS是同步寫入,會在進入每個事件循環後通過aof_fsync()
(在linux下面就是fdatasync)同步到磁盤。而EVERYSEC則是通過aof_background_fsync()
將刷磁盤操作加入到一個作業列表中(每秒執行一次),由bioInit創建的後臺IO線程執行刷磁盤操作。設置爲NO則不刷新磁盤,同步到磁盤的操作由操作系統決定。 - 調用
srand(time(NULL)^getpid())
初始化隨機數發生器。
恢復數據
如果開啓了AOF,則從AOF文件重放命令來恢復redis數據。否則,則是從RDB文件恢復數據。每次進行RDB持久化時,redis都是將內存中的數據庫的數據全部寫到文件中,不是增量的持久化。
if (server.appendonly) {
if (loadAppendOnlyFile(server.appendfilename) == REDIS_OK)
redisLog(REDIS_NOTICE,"DB loaded from append only file: %ld seconds",time(NULL)-start);
} else {
if (rdbLoad(server.dbfilename) == REDIS_OK) {
redisLog(REDIS_NOTICE,"DB loaded from disk: %ld seconds",
time(NULL)-start);
} else if (errno != ENOENT) {
redisLog(REDIS_WARNING,"Fatal error loading the DB. Exiting.");
exit(1);
}
}
建立事件循環
接着,redis註冊beforeSleep()
函數到事件循環中,這個函數在每次進入事件循環時首先調用它,它主要做兩件事:
- 對開啓vm的情況下,將那些請求已經交換到磁盤的key的客戶端解除阻塞並處理這些客戶端請求。
- 調用
flushAppendOnlyFile()
將AOF文件刷到磁盤,最終調用的是aof_fsync()
或者aof_background_fsync()
。
進入事件循環
redis接着正式調用 aeMain()
函數進入事件循環。當有時間事件或者文件事件需要處理時,會調用他們對應的處理函數進行處理。aeProcessEvents()
封裝了處理函數,時間事件通過自定義的函數處理,而文件事件則通過epoll或者kqueue或者select
系統調用來處理,在Linux裏面通常使用的是epoll。
aeProcessEvents()
會優先處理文件事件,其次纔是處理時間事件。文件事件就是通過 aeApiPoll()
函數來獲取事件,並將觸發的事件加入到 server.el.fired
數組中,最終就是調用epoll_wait()
獲取事件,其中超時時間設置的是距離最近一次時間事件的時間,這樣如果沒有文件事件也不會太耽誤時間事件執行。獲取到文件事件後,會根據事件類型是讀還是寫調用相應的方法處理。如讀事件就是調用的 acceptTcpHandler()
處理的。
接着處理時間事件,從 server.el.timeEventHead
可以拿到時間事件鏈表的頭,遍歷該鏈表,如果有時間事件的執行時間到了,則執行對應的函數即可。這裏有個地方注意下,如果時間事件處理函數返回值不是-1,則表示該時間事件需要定期執行,需要設置該事件下一次執行時間而不是從時間事件鏈表移除它,如serverCron這個時間事件,就是這樣的定期執行事件,100ms執行一次。
2 請求處理和響應
現在服務器已經啓動完畢了,接下來看看redis是如何在事件循環中接收客戶端請求並處理請求的,這裏以 TCP 方式爲例分析,unix socket的類似。
接收連接
redis處理客戶端請求是在函數 acceptTcpHandler()
完成的。這個函數通過 anetTcpAccept()
接收客戶端請求,然後調用的 acceptCommandHandler()
來處理客戶端請求。
在acceptCommandHandler最終調用的是 createClient(fd)
函數創建了redisClient對象,初始化該對象的變量,並將該連接fd註冊到事件循環中,事件類型爲 AE_READABLE(EPOOLIN)
。該事件處理函數爲 readQueryFromClient()
,用於處理客戶端連接的命令。這樣,我們之前註冊了listenfd
,用於在新連接到來時接收新連接,現在將客戶端連接fd註冊到事件循環,完成客戶端命令處理。注意這裏必須通過anetNonBlock(NULL, fd)
將客戶端連接fd設置爲非阻塞的。
void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
int cport, cfd;
char cip[128];
cfd = anetTcpAccept(server.neterr, fd, cip, &cport);
if (cfd == AE_ERR) {
redisLog(REDIS_WARNING,"Accepting client connection: %s", server.neterr);
return;
}
redisLog(REDIS_VERBOSE,"Accepted %s:%d", cip, cport);
acceptCommonHandler(cfd);
}
redisClient *createClient(int fd) {
redisClient *c = zmalloc(sizeof(redisClient));
c->bufpos = 0;
anetNonBlock(NULL,fd);
anetTcpNoDelay(NULL,fd);
if (aeCreateFileEvent(server.el,fd,AE_READABLE,
readQueryFromClient, c) == AE_ERR)
{
close(fd);
zfree(c);
return NULL;
}
...
}
從客戶端讀取命令
當客戶端發送命令時,會通過readQueryFromClient()
處理。它每次讀取最多REDIS_IOBUF_LEN(16*1024)
16K字節到緩存數組buf中,最後將緩存的數據拷貝到 redisClient->querybuf 中,然後調用 processInputBuffer()
函數處理客戶端命令。
processInputBuffer()
解析客戶端的原始命令字符串並將命令參數設置到 redisClient->argv 數組中,命令參數是 redisObject
類型的結構體。注意命令類型有兩種,我們通過 redis-cli
發送的命令類型爲 REDIS_REQ_MULTIBULK
,這種命令以*
開頭,符合 redis protocol,調用processMultibulkBuffer()
函數處理。另外一種命令是 REDIS_REQ_INLINE
,這種命令是你通過其他工具連接的時候發的,比如通過 telnet localhost 6379
,這種命令是直接的原生字符串,沒有使用 redis protocol
封裝客戶端命令。
void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
redisClient *c = (redisClient*) privdata;
char buf[REDIS_IOBUF_LEN];
int nread;
server.current_client = c;
nread = read(fd, buf, REDIS_IOBUF_LEN);
...
processInputBuffer(c);
server.current_client = NULL;
}
void processInputBuffer(redisClient *c) {
while(sdslen(c->querybuf)) {
/* Immediately abort if the client is in the middle of something. */
if (c->flags & REDIS_BLOCKED || c->flags & REDIS_IO_WAIT) return;
/* Determine request type when unknown. */
if (!c->reqtype) {
if (c->querybuf[0] == '*') {
c->reqtype = REDIS_REQ_MULTIBULK;
} else {
c->reqtype = REDIS_REQ_INLINE;
}
}
if (c->reqtype == REDIS_REQ_INLINE) {
if (processInlineBuffer(c) != REDIS_OK) break;
} else if (c->reqtype == REDIS_REQ_MULTIBULK) {
if (processMultibulkBuffer(c) != REDIS_OK) break;
} else {
redisPanic("Unknown request type");
}
/* Multibulk processing could see a <= 0 length. */
if (c->argc == 0) {
resetClient(c);
} else {
/* Only reset the client when the command was executed. */
if (processCommand(c) == REDIS_OK)
resetClient(c);
}
}
}
在processInputBuffer()
解析到完整命令後,便會調用 processCommand()
函數開始處理客戶端命令。先通過lookupCommand
根據命令字符串找到命令的處理程序。在調用命令處理程序執行實際的命令前,會先執行一系列的檢查:
- 如果命令不存在或者參數個數不對,返回錯誤。
- 如果redis配置了密碼而客戶端請求沒有通過認證就發正式命令,返回錯誤。
- 比如如果當前是在一個
PUB/SUB
上下文中,則只允許subscribe/unsubscribe
等命令。 - 如果設置了最大內存maxmemory,則執行
SET
等設置了REDIS_CMD_DENYOOM
標識的命令時會返回錯誤。 - 如果redis服務器正在加載DB文件,則只允許
info
命令,其他命令報錯。 - 如果是事務命令
MULTI
,只要輸入的不是exec,discard,multi,watch
等命令,則將命令加入隊列中以便後面批量執行。
執行命令
在上一節的檢查OK後執行 call()
函數真正開始執行命令,它調用的是 redisCommand
的proc指向的函數,爲 redisCommandProc
類型對象。命令執行完後,會通過 addReply()
函數將執行結果緩存到 redisClient 的 buf 數組中。
那這個響應數據什麼時候會發送給客戶端呢?這是因爲在 addReply()
中會調用 _installWriteEvent()
,該函數就是將客戶端連接的fd加入到事件循環中,事件類型爲AE_WRITABLE(EPOLLOUT)
,然後將響應數據通過_addReplyToBuffer()
和_addReplyObjectToList()
寫入到響應緩存redisClient->buf和redisClient->reply
中。這裏的buf和reply兩個地方都是用於寫響應緩存的,如果響應的總的數據長度(響應數據長度+數據本身)小於 REDIS_REPLY_CHUNK_BYTES(7500)
字節,則用buf數組緩存數據,否則用reply鏈表來存儲數據。
當下一個事件循環到來時,會讀取到該客戶端連接fd,然後通過函數 sendReplyToClient()
從響應緩存讀取數據併發送響應數據給客戶端,然後移除寫事件。命令執行完成後,redis會重置redisClient對象並接收後續命令。
void call(redisClient *c) {
long long dirty, start = ustime(), duration;
dirty = server.dirty;
c->cmd->proc(c);
dirty = server.dirty-dirty;
duration = ustime()-start;
slowlogPushEntryIfNeeded(c->argv,c->argc,duration);
if (server.appendonly && dirty > 0)
feedAppendOnlyFile(c->cmd,c->db->id,c->argv,c->argc);
if ((dirty > 0 || c->cmd->flags & REDIS_CMD_FORCE_REPLICATION) &&
listLength(server.slaves))
replicationFeedSlaves(server.slaves,c->db->id,c->argv,c->argc);
if (listLength(server.monitors))
replicationFeedMonitors(server.monitors,c->db->id,c->argv,c->argc);
server.stat_numcommands++;
}
寫操作如SET/ZADD
等會讓redis服務器變的dirty,後臺IO線程執行的BGSAVE很重要,它會根據時間和修改過的key的數目來刷數據到rdb中。而如果開啓了AOF還需要刷新AOF文件到磁盤。feedAppendOnlyFile
是將客戶端命令寫入到AOF文件中,所以可以通過AOF文件重放來恢復數據。當然這裏會對expire,setex
等命令做些轉換,將過期時間設置爲絕對時間,添加必要的SELECT db
的命令等。另外則是直接通過 catAppendOnlyGenericCommand()
將命令寫入到AOF文件。
如果有slave連接到該服務器,則通過 replicationFeedSlaves()
將命令發給slave服務器(slave同步時master會先通過BGSAVE保存一份rdb併發送給slave,後續的命令同步則由replicationFeedSlaves()來完成)。如果有客戶端通過 monitor
命令連接到該服務器,則還要通過 replicationFeedMonitors()
發送命令字符串過去,並帶上命令時間戳。
3 總結
這篇文章主要對redis的初始化流程做了分析,包括啓動時配置初始化,使用epoll支持高併發,讀取解析和響應命令等,流程圖如下(來自 Redis:under the hood 博客)。這裏還有篇 Paul Smith的大作 More Redis internals: Tracing a GET & SET 用於跟蹤 redis 的 GET/SET
命令流程的,值得一看。
4 參考資料
- https://pauladamsmith.com/articles/redis-under-the-hood.html (基本是對這篇文章的翻譯加註解)
- https://redis.io/topics/protocol (redis協議,看了可以比較深入理解redis客戶端和服務端的交互)