Redis主體流程分析

網上分析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 命令流程的,值得一看。

Redis啓動流程

4 參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章