深刻淺出 Redis client/server交互流程

最近筆者閱讀並研究redis源碼,在redis客戶端與服務器端交互這個內容點上,須要參考網上一些文章,可是遺憾的是發現大部分文章都斷斷續續的非系統性的,不能給讀者此交互流程的總體把握。因此這裏我嘗試,站在源碼的角度,將redis client/server 交互流程儘量簡單地展示給你們,同時也站在DBA的角度給出一些平常工做中注意事項。node

Redis client/server 交互步驟分爲如下6個步驟:linux

1、Client 發起socket 鏈接redis

2、Server 接受socket鏈接數據庫

3、客戶端 開始寫入編程

4、server 端接收寫入緩存

5、server 返回寫入結果服務器

6、Client收到返回結果網絡

注:爲使文章儘量簡潔,這裏只討論客戶端命令寫入的過程,不討論客戶端命令讀取的流程。數據結構

在進一步閱讀和了解互動流程以前,請你們確保已經熟練掌握了Linux Socket 創建流程和epoll I/O 多路複用技術兩個技術點,這對文章內容的理解相當重要。併發

交互的總體流程

在介紹6個步驟以前,首先看一下redis client/server 交互流程總體的程序執行流程圖:

(點擊放大圖像)

上圖中6個步驟分別用不一樣的顏色箭頭表示,而且最終結果也用相對應的顏色標識。

更多資料請加入Redis緩存技術交流組:288724942

首先看看綠色框裏面的循環執行的方法,最末是epoll_wait方法,即等待事件產生的方法。而後再看第二、四、5步驟的末尾都有epoll_ctl方法,即epoll事件註冊函數。關於epoll的相關技術解析請參看文末一段。

在這裏的循環還有個beforeSleep方法,其實它跟咱們此次討論的話題沒有太大的關係。可是仍是想給你們介紹一下。

beforeSleep方法主要作如下幾件事:

  1. 執行一次快速的主動過時檢查,檢查是否有過時的key

  2. 當有客戶端阻塞時,向全部從庫發送ACK請求

  3. unblock 在同步複製時候被阻塞的客戶端

  4. 嘗試執行以前被阻塞客戶端的命令

  5. 將AOF緩衝區的內容寫入到AOF文件中

  6. 若是是集羣,將會根據須要執行故障遷移、更新節點狀態、保存node.conf 配置文件。

如此,redis整個事件管理器機制就比較清楚了。接下來進一步探討並理解事件是如何觸發並建立。

交互的六大步驟

下面正式開始介紹redis client/server 交互的6大步驟

1、Client 發起socket 鏈接

(點擊放大圖像)

這裏以redis-cli 客戶端爲例,當執行如下語句時:

[root@zbdba redis-3.0]# ./src/redis-cli -p 6379 -h 127.0.0.1
127.0.0.1:6379>

客戶端會作以下操做:

一、獲取客戶端參數,如端口、ip地址、dbnum、socket等

也就是咱們執行./src/redis-cli --help 中列出的參數

二、根據用戶指定參數肯定客戶端處於哪一種模式

目前共有:

Latency mode/Slave mode/Get RDB mode/Pipe mode/Find 
big keys/Stat mode/Scan mode/Intrinsic latency mode

以上8種模式

例如:stat 模式

[root@zbdba redis-3.0]# ./src/redis-cli -p 6379 -h 127.0.0.1 --stat
------- data ------ --------------------- load -------------------- - child -
keys       mem      clients blocked requests            connections         
1          817.18K  2       0       1 (+0)              2           
1          817.18K  2       0       2 (+1)              2           
1          817.18K  2       0       3 (+1)              2           
1          817.18K  2       0       4 (+1)              2           
1          817.18K  2       0       5 (+1)              2           
1          817.18K  2       0       6 (+1)              2

咱們這裏沒有指定,就是默認的模式。

三、進入上圖中step1的cliConnect 方法,cliConnect主要包含redisConnect、redisConnectUnix方法。這兩個方法分別用於TCP Socket鏈接以及Unix Socket鏈接,Unix Socket用於同一主機進程間的通訊。咱們上面是採用的TCP Socket鏈接方式也就是咱們日常生產環境經常使用的方式,這裏不討論Unix Socket鏈接方式,若是要使用Unix Socket鏈接方式,須要配置unixsocket 參數,而且按照下面方式進行鏈接:

[root@zbdba redis-3.0]# ./src/redis-cli -s /tmp/redis.sock
redis /tmp/redis.sock>

四、進入redisContextInit方法,redisContextInit方法用於建立一個Context結構體保存在內存中,以下:

/* Context for a connection to Redis */
typedef struct redisContext {
    int err; /* Error flags, 0 when there is no error */
    char errstr[128]; /* String representation of error when applicable */
    int fd;
    int flags;
    char *obuf; /* Write buffer */
    redisReader *reader; /* Protocol reader */
} redisContext;

主要用於保存客戶端的一些東西,最重要的就是 write buffer和redisReader,write buffer 用於保存客戶端的寫入,redisReader用於保存協議解析器的一些狀態。

五、進入redisContextConnectTcp 方法,開始獲取IP地址和端口用於創建鏈接,主要方法以下:

s = socket(p->ai_family,p->ai_socktype,p->ai_protocol
connect(s,p->ai_addr,p->ai_addrlen)

到此客戶端向服務端發起創建socket鏈接,而且等待服務器端響應。

固然cliConnect方法中還會調用cliAuth方法用於權限驗證、cliSelect用於db選擇,這裏不着重討論。

2、Server 接受socket鏈接

(點擊放大圖像)

服務器接收客戶端的請求首先是從epoll_wait取出相關的事件,而後進入上圖中step2中的方法,執行acceptTcpHandler或者acceptUnixHandler方法,那麼這兩個方法對應的事件是在何時註冊的呢?他們是在服務器端初始化的時候建立。下面看看服務器端在初始化的時候與socket相關的地方

一、打開TCP監聽端口

    if (server.port != 0 &&
        listenToPort(server.port,server.ipfd,&server.ipfd_count) == REDIS_ERR)
        exit(1);

二、打開unix 本地端口

  if (server.unixsocket != NULL) {
        unlink(server.unixsocket); /* don't care if this fails */
        server.sofd = anetUnixServer(server.neterr,server.unixsocket,
            server.unixsocketperm, server.tcp_backlog);
        if (server.sofd == ANET_ERR) {
            redisLog(REDIS_WARNING, "Opening socket: %s", server.neterr);
            exit(1);
        }
        anetNonBlock(NULL,server.sofd);
    }

三、爲TCP鏈接關聯鏈接應答處理器(accept)

   for (j = 0; j < server.ipfd_count; j++) {
        if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
            acceptTcpHandler,NULL) == AE_ERR)
            {
                redisPanic(
                    "Unrecoverable error creating server.ipfd file event.");
            }
    }

四、爲Unix Socket關聯應答處理器

if (server.sofd > 0 && aeCreateFileEvent
     (server.el,server.sofd,AE_READABLE,
        acceptUnixHandler,NULL) == AE_ERR) 
redisPanic("Unrecoverable error creating server.sofd file event.");

在1/2步驟涉及到的方法中是Linux Socket的常規操做,獲取IP地址,端口。最終經過socket、bind、listen方法創建起Socket監聽。也就是上圖中acceptTcpHandler和acceptUnixHandler下面對應的方法。

在3/4步驟涉及到的方法中採用aeCreateFileEvent 方法建立相關的鏈接應答處理器,在客戶端請求鏈接的時候觸發。

因此如今整個socket鏈接創建流程就比較清楚了,以下:

至此客戶端和服務器端的socket鏈接已經創建,可是此時服務器端還繼續作了2件事:

能夠從圖中得知,aeCreateFileEvent 調用aeApiAddEvent方法最終經過epoll_ctl 方法進行註冊事件。

3、客戶端開始寫入

(點擊放大圖像)

客戶端在與服務器端創建好socket鏈接以後,開始執行上圖中step3的repl方法。從圖中可知repl方法接受輸入輸出主要是採用linenoise插件。固然這是針對redis-cli客戶端哦。linenoise 是一款優秀的命令行編輯庫,被普遍的運用在各類DB上,如Redis、MongoDB,這裏不詳細討論。客戶端寫入流程分爲如下幾步:

一、linenoise等待接受用戶輸入

二、linenoise 將用戶輸入內容傳入cliSendCommand方法,cliSendCommand方法會判斷命令是否爲特殊命令,如:

客戶端會根據以上命令設置對應的輸出格式以及客戶端的模式,由於這裏咱們是普通寫入,因此不會涉及到以上的狀況。

三、cliSendCommand方法會調用redisAppendCommandArgv方法,redisAppendCommandArgv方法會調用redisFormatCommandArgv和__redisAppendCommand方法

redisFormatCommandArgv方法用於將客戶端輸入的內容格式化成redis協議:

例如:

set zbdba jingbo
*3\r\n$3\r\n set\r\n $5\r\n zbdba\r\n $6\r\n jingbo

__redisAppendCommand方法用於將命令寫入到outbuf中

接着客戶端進入下一個流程,將outbuf內容寫入到套接字描述符上並傳輸到服務器端。

四、進入redisGetReply方法,該方法下主要有redisGetReplyFromReader和redisBufferWrite 方法,redisGetReplyFromReader主要用於讀取掛起的回覆,redisBufferWrite 方法用於將當前outbuf中的內容寫入到套接字描述符中,並傳輸內容。

主要方法以下:

nwritten = write(c->fd,c->obuf,sdslen(c->obuf));

此時客戶端等待服務器端接收寫入。

4、server 端接收寫入

(點擊放大圖像)

服務器端依然在進行事件循環,在客戶端發來內容的時候觸發,對應的文件讀取事件。這就是以前建立socket鏈接的時候創建的事件,該事件綁定的方法是readQueryFromClient 。此時進入step4的readQueryFromClient 方法。

readQueryFromClient 方法用於讀取客戶端的發送的內容。它的執行步驟以下:

1、在readQueryFromClient方法中從服務器端套接字描述符中讀取客戶端的內容到服務器端初始化client的查詢緩衝中,主要方法以下:

nread = read(fd, c->querybuf+qblen, readlen);

2、交給processInputBuffer處理,processInputBuffer 主要包含兩個方法,processInlineBuffer和processCommand。processInlineBuffer方法用於採用redis協議解析客戶端內容並生成對應的命令並傳給processCommand 方法,processCommand方法則用於執行該命令

三、processCommand方法會如下操做:

四、最後進入call方法。

call方法會調用setCommand,由於這裏咱們執行的set zbdba jingbo,set 命令對應setCommand 方法,redis服務器端在開始初始化的時候就會初始化命令表,命令表以下:

struct redisCommand redisCommandTable[] = {
    {"get",getCommand,2,"r",0,NULL,1,1,1,0,0},
    {"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},
    {"setnx",setnxCommand,3,"wm",0,NULL,1,1,1,0,0},
    {"setex",setexCommand,4,"wm",0,NULL,1,1,1,0,0},
    {"psetex",psetexCommand,4,"wm",0,NULL,1,1,1,0,0},
    {"append",appendCommand,3,"wm",0,NULL,1,1,1,0,0},
    {"strlen",strlenCommand,2,"r",0,NULL,1,1,1,0,0},
    {"del",delCommand,-2,"w",0,NULL,1,-1,1,0,0},
    {"exists",existsCommand,2,"r",0,NULL,1,1,1,0,0},
    {"setbit",setbitCommand,4,"wm",0,NULL,1,1,1,0,0},
    ....
}

因此若是是其餘的命令會調用其餘相對應的方法。call方法還會作一些事件,好比發送命令到從庫、發送命令到aof、計算命令執行的時間。

五、setCommand方法,setCommand方法會調用setGenericCommand方法,該方法首先會判斷該key是否已通過期,最後調用setKey方法。

這裏須要說明一點的是,經過以上的分析。redis的key過時包括主動檢測以及被動監測

主動監測

被動監測

以上主要是讓運維的同窗更加清楚redis的key過時刪除機制。

六、進入setKey方法,setKey方法最終會調用dbAdd方法,其實最終就是將該鍵值對存入服務器端維護的一個字典中,該字典是在服務器初始化的時候建立,用於存儲服務器的相關信息,其中包括各類數據類型的鍵值存儲。完成了寫入方法時候,此時服務器端會給客戶端返回結果。

七、進入prepareClientToWrite方法而後經過調用_addReplyToBuffer方法將返回結果寫入到outbuf中(客戶端鏈接時建立的client)

八、經過aeCreateFileEvent方法註冊文件寫事件並綁定sendReplyToClient方法

5、server 返回寫入結果

(點擊放大圖像)

此時按照慣例,aeMain主函數循環,監測到新註冊的事件,調用sendReplyToClient方法。sendReplyToClient方法主要包含兩個操做:

一、將outbuf內容寫入到套接字描述符並傳輸到客戶端,主要方法以下:

nwritten = write(fd,c->buf+c->sentlen,c->bufpos-c->sentlen);

二、aeDeleteFileEvent 用於刪除 文件寫事件

6、Client收到返回結果

(點擊放大圖像)

客戶端接收到服務器端的返回調用redisBufferRead方法,該方法主要用於從socket中讀取數據。主要方法以下:

nread = read(c->fd,buf,sizeof(buf));

而且將讀取的數據交由redisReaderFeed方法,該方法主要用於將數據交給回覆解析器處理,也就是cliFormatReplyRaw,該方法將回復內容格式化。最終經過

fwrite(out,sdslen(out),1,stdout);

方法返回給客戶端並打印展現給用戶。

至此整個寫入流程完成。以上還有不少細節沒有說到,感興趣的朋友能夠自行閱讀源碼。

結語

在深刻了解一個DB的時候,個人第一步就是去理解它執行一條命令執行的整個流程,這樣就能對它整個運行流程較爲熟悉,接着咱們能夠去深刻各個細節的部分,好比Redis的相關數據結構、持久化以及高可用相關的東西。寫這篇文章的初衷就是但願咱們更加輕鬆的走好這第一步。這裏還須要提醒的是,在咱們進行Redis源碼閱讀的時候最關鍵的是須要靈活的使用GDB調試工具,它能幫咱們更好的去理順相關執行步驟,從而讓咱們更加容易理解其實現原理。

附錄:兩個相關重要知識點

一、Linux Socket 創建流程

(點擊放大圖像)

linux socket創建過程如上圖所示。在Linux編程時,不管是操做文件仍是網絡操做時都是經過文件描述符來進行讀寫的,可是他們有一點區別,這裏咱們不具體討論,咱們將網絡操做時就稱爲套接字描述符。你們能夠自行用c寫一個簡單的demo,這裏就不詳細說明了。

這裏列出幾個重要的方法:

int socket(int family,int type,int protocol);
int connect(int sockfd,const struct sockaddr * servaddr,socklen_taddrlen);
int bind(int sockfd,const struct sockaddr * myaddr,socklen_taddrlen);
int listen(int sockfd,int backlog);
int accept(int sockfd,struct sockaddr *cliaddr,socklen_t * addrlen);

Redis client/server 也是基於linux socket鏈接進行交互,而且最終調用以上方法綁定IP,監聽端口最終與客戶端創建鏈接。

二、epoll I/O 多路複用技術

這裏重點介紹一下epoll,由於Redis事件管理器核心實現基本依賴於它。首先來看epoll是什麼,它能作什麼?

epoll是在Linux 2.6內核中引進的,是一種強大的I/O多路複用技術,上面咱們已經說到在進行網絡操做的時候是經過文件描述符來進行讀寫的,那麼日常咱們就是一個進程操做一個文件描述符。然而epoll能夠經過一個文件描述符管理多個文件描述符,而且不阻塞I/O。這使得咱們單進程能夠操做多個文件描述符,這就是redis在高併發性能還如此強大的緣由之一。

下面簡單介紹epoll 主要的三個方法:

Redis 的事件管理器主要是基於epoll機制,先採用 epoll_ctl方法 註冊事件,而後再使用epoll_wait方法取出已經註冊的事件。

咱們知道redis支持多種平臺,那麼redis在這方面是如何兼容其餘平臺的呢?Redis會根據操做系統的類型選擇對應的IO多路複用實現。

#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
    #ifdef HAVE_EPOLL
    #include "ae_epoll.c"
    #else
        #ifdef HAVE_KQUEUE
        #include "ae_kqueue.c"
        #else
        #include "ae_select.c"
        #endif
    #endif
#endif
ae_evport.c sun solaris
ae_poll.c linux
ae_select.c unix/linux epoll是select的增強版
ae_kqueue BSD/Apple

以上只是簡單的介紹,你們須要詳細瞭解了epoll機制才能更好的理解後面的東西。

    1. 服務器初始化創建socket監聽

    2. 服務器初始化建立相關鏈接應答處理器,經過epoll_ctl註冊事件

    3. 客戶端初始化建立socket connect 請求

    4. 服務器接受到請求,用epoll_wait方法取出事件

    5. 服務器執行事件中的方法(acceptTcpHandler/acceptUnixHandler)並接受socket鏈接

    1. 採用createClient方法在服務器端爲客戶端建立一個client,由於I/O複用因此須要爲每一個客戶端維持一個狀態。這裏的client也在內存中分配了一塊區域,用於保存它的一些信息,如套接字描述符、默認數據庫、查詢緩衝區、命令參數、認證狀態、回覆緩衝區等。這裏提醒一下DBA同窗關於client-output-buffer-limit設置,設置不恰當將會引發客戶端中斷。

    2. 採用aeCreateFileEvent方法在服務器端建立一個文件讀事件而且綁定readQueryFromClient方法。

    • help

    • info

    • cluster nodes

    • cluster info

    • client list

    • shutdown

    • monitor

    • subscribe

    • psubscribe

    • sync

    • psync

    • 處理是否爲quit命令。

    • 對命令語法及參數會進行檢查。

    • 這裏若是採起認證也會檢查認證信息。

    • 若是Redis爲集羣模式,這裏將進行hash計算key所屬slot並進行轉向操做。

    • 若是設置最大內存,那麼檢查內存是否超過限制,若是超過限制會根據相應的內存策略刪除符合條件的鍵來釋放內存

    • 若是這是一個主服務器,而且這個服務器以前執行bgsave發生了錯誤,那麼不執行命令

    • 若是min-slaves-to-write開啓,若是沒有足夠多的從服務器將不會執行命令

    • 注:因此DBA在此的設置很是重要,建議不是特殊場景不要設置。

    • 若是這個服務器是一個只讀從庫的話,拒絕寫入命令。

    • 在訂閱於發佈模式的上下文中,只能執行訂閱和退訂相關的命令

    • 當這個服務器是從庫,master_link down 而且slave-serve-stale-data 爲 no 只容許info 和slaveof命令

    • 若是服務器正在載入數據到數據庫,那麼只執行帶有REDIS_CMD_LOADING標識的命令

    • lua腳本超時,只容許執行限定的操做,好比shutdown、script kill 等

    • 在beforeSleep方法中執行key快速過時檢查,檢查模式爲ACTIVE_EXPIRE_CYCLE_FAST。週期爲每一個事件執行完成時間到下一次事件循環開始

    • 在serverCron方法中執行key過時檢查,這是key過時檢查主要的地方,檢查模式爲ACTIVE_EXPIRE_CYCLE_SLOW,serverCron方法執行週期爲1秒鐘執行server.hz 次,hz默認爲10,因此約100ms執行一次。hz設置越大過時鍵刪除就越精準,可是cpu使用率會越高,這裏咱們線上redis採用的默認值。redis主要是在這個方法裏刪除大部分的過時鍵。

    • 使用內存超過最大內存被迫根據相應的內存策略刪除符合條件的key。

    • 在key寫入以前進行被動檢查,檢查key是否過時,過時就進行刪除。

    • 還有一種不友好的方式,就是randomkey命令,該命令隨機從redis獲取鍵,每次獲取到鍵的時候會檢查該鍵是否過時。

    1. int epoll_create(int size) //建立一個epoll句柄用於監聽文件描述符FD,size用於告訴內核這個監聽的數目一共有多大。該epoll句柄建立後在操做系統層面只會佔用一個fd值,可是它能夠監聽size+1 個文件描述符。

    2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)//epoll事件註冊函數

    3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)//等待事件的產生

    4. 更多資料請加入Redis緩存技術交流組:288724942

相關文章
相關標籤/搜索