Redis實現之用戶端

來源:互聯網
上載者:User

標籤:組成   相同   cas   red   包括   假設   部分   command   mem   

用戶端

Redis伺服器是典型的一對多伺服器程式:一個伺服器可以與多個用戶端建立網路連接,每個用戶端可以向伺服器發送命令請求,而伺服器則接收並處理用戶端發送的命令請求,並向用戶端返回命令回複。通過使用I/O多工技術實現的檔案事件處理器,Redis伺服器使用單線程單進程的方式來處理命令請求,並與多個用戶端進行網路通訊

對於每個與伺服器進行串連的用戶端,伺服器都為這些用戶端建立了相應的redis.h/redisClient結構(用戶端狀態),這個結構儲存了用戶端當前的狀態資訊,以及執行相關功能時需要用到的資料結構,其中包括:

  • 用戶端的通訊端描述符
  • 用戶端的名字
  • 用戶端的標誌值(flag)
  • 指向用戶端正在使用的資料庫指標,以及該資料庫的號碼
  • 用戶端當前要執行的命令、命令的參數、命令參數的個數,以及指向命令實現函數的指標
  • 用戶端的輸入緩衝區和輸出緩衝區
  • 用戶端的複製狀態資訊,以及進行複製所需的資料結構
  • 用戶端執行BRPOP、BLPOP等列表阻塞命令時使用的資料結構
  • 用戶端的事物狀態,以及執行WATCH命令時用到的資料結構
  • 用戶端執行發布與訂閱功能時用到的資料結構
  • 用戶端的身分識別驗證標識
  • 用戶端的建立時間,用戶端和伺服器最後一次通訊時間,以及用戶端的輸出華沖區大小超出軟性限制的時間

Redis伺服器狀態結構的clients屬性是一個鏈表,這個鏈表儲存了所有與伺服器串連的用戶端的狀態結構,對用戶端執行大量操作,或者尋找某個指定的用戶端,都可以通過遍曆clients鏈表來完成:

redis.h

struct redisServer {    ……//一個鏈表,儲存了所有用戶端狀態    list *clients;                 ……};

  

圖1-1   用戶端與伺服器

作為例子,圖1-1展示了一個與三個用戶端進行已連線的服務器,而圖1-2則展示了這個伺服器clients鏈表的例子

圖1-2   clients鏈表

用戶端屬性

用戶端狀態包含的屬性可以分為兩類:

  • 一類是比較通用的屬性,這類屬性很少與特定功能相關,無論用戶端執行的是什麼工作,它們都要用到這些屬性
  • 另一類是和特定功能相關的屬性,比如操作資料庫時需要用到的db屬性和dictid屬性,執行事務時需要用到的master屬性,以及執行WATCH命令時需要用到的watched_keys屬性等

通訊端描述符

用戶端狀態的fd屬性記錄了用戶端正在使用的通訊端描述符

redis.h

typedef struct redisClient {    int fd;    ……} redisClient;

  

根據用戶端類型不同,fd屬性的值可以是-1或者是大於-1的整數:

  • 偽用戶端(fake client)的fd屬性的值為-1:偽用戶端處理的命令請求來源於AOF檔案或者Lua指令碼,而不是網路,所以這種用戶端不需要通訊端串連,自然也不需要記錄通訊端描述符。目前Redis伺服器會在兩個地方用到偽用戶端,一個是用於載入AOF檔案並還原資料庫狀態,另一個則是用於執行Lua指令碼中包含的Redis命令
  • 普通用戶端的fd屬性的值為大於-1的整數:普通用戶端使用通訊端來與伺服器進行通訊,所以伺服器會用fd屬性來記錄用戶端通訊端的描述符。因為合法的通訊端描述符不能為-1,所以普通用戶端的通訊端描述符的值必然是大於-1的整數

執行CLIENT list命令可以列出目前所有串連到伺服器的普通用戶端,命令輸出中fd域顯示了伺服器串連用戶端所使用的通訊端描述符:

127.0.0.1:6379> CLIENT listid=3 addr=127.0.0.1:57522 fd=9 name= age=76021 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client

  

名字

在預設情況下,一個串連到伺服器的用戶端是沒有名字的。比如上面的CLIENT list命令中,name域是空白的。不過我們可以使用CLIENT SETNAME命令為用戶端設定一個名字,讓用戶端的身份變得清晰。以下展示用戶端執行CLIENT SETNAME命令之後的用戶端列表

127.0.0.1:6379> CLIENT SETNAME message_ququeOK127.0.0.1:6379> CLIENT listid=3 addr=127.0.0.1:57522 fd=9 name=message_quque age=76399 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client

  

用戶端的名字記錄在用戶端狀態的name屬性中:

redis.h

typedef struct redisClient {    ……    robj *name;                 ……} redisClient;

  

如果用戶端沒有為自己設定名字,那麼相應用戶端狀態的name屬性指向NULL指標;相反地,如果用戶端為自己設定了名字,那麼name屬性將指向一個字串對象,而對象則存放著用戶端的名字。圖1-3展示了用戶端狀態樣本,根據name屬性顯示,用戶端名字為"message_queue"

圖1-3   name屬性樣本

標誌

用戶端的標誌屬性flags記錄了用戶端的角色,以及用戶端目前所處的狀態

redis.h

typedef struct redisClient {    ……    int flags;               ……} redisClient;

  

flags屬性的值可以是單個標誌:flags = <flag>,也可以是多個標誌的二進位:flags = <flags1> | <flags2> | ……

每個標誌使用一個常量表示,一部分標誌記錄了用戶端的角色:

  • 在主從伺服器進行複製操作時,主從伺服器會成為從伺服器的用戶端,而從伺服器也會成為主伺服器的用戶端。REDIS_MASTER標誌表示用戶端代表的一個主伺服器,REDIS_SLAVE標誌表示用戶端代表的是一個從伺服器
  • REDIS_PRE_PSYNC標誌表示用戶端代表的是一個版本低於Redis2.8的從伺服器,主伺服器不能使用PSYNC命令與這個從伺服器同步。這個標誌只能在REDIS_SLAVE標誌處於開啟狀態時使用
  • REDIS_LUA_CLIENT標識表示用戶端是專門用於處理Lua指令碼裡麵包含的Redis命令的偽用戶端

而另外一部分標誌則記錄了用戶端目前所處的狀態:

  • REDIS_MONITOR標誌表示用戶端正在執行MONITOR命令
  • REDIS_UNIX_SOCKET標誌表示伺服器使用UNIX通訊端來串連用戶端
  • REDIS_BLOCKED標誌表示用戶端正在被BRPOP、BLPOP等命令阻塞
  • REDIS_UNBLOCKED標誌表示用戶端已經從REDIS_BLOCKED標誌所表示的阻塞狀態中脫離出來,不再阻塞。REDIS_UNBLOCKED標誌只能在REDIS_BLOCKED標誌已經開啟的情況下使用
  • REDIS_MULTI標誌表示用戶端正在執行事務
  • REDIS_DIRTY_CAS標誌表示事務使用WATCH命令監視的資料庫鍵已經被修改,REDIS_DIRTY_EXEC標誌表示事務在命令入隊時出現了錯誤,以上兩個標誌都表示事務的安全性已經被破壞,只要這兩個標記中的任意一個被開啟,EXEC命令必然會執行失敗。這兩個標誌只能在用戶端開啟了REDIS_MULTI標誌的情況下使用
  • REDIS_CLOSE_ASAP標誌表示用戶端的輸出緩衝區大小超出了伺服器允許的範圍,伺服器會在下一次執行serverCron函數時關閉這個用戶端,以免伺服器的穩定性受到這個用戶端影響。積存在輸出緩衝區中的所有內容會直接被釋放,不會返回給用戶端
  • REDIS_CLOSE_AFTER_REPLY標誌表示有使用者對這個用戶端執行了CLIENT_KILL命令,或者用戶端發送給伺服器的命令請求中包含了錯誤的協議內容。伺服器會將用戶端積存在輸出緩衝區中的所有內容發送給用戶端,然後關閉用戶端
  • REDIS_ASKING標誌表示用戶端向叢集節點(運行在叢集模式下的伺服器)發送了ASKING命令
  • REDIS_FORCE_AOF標誌強制服務器將當前執行的命令寫入到AOF檔案裡面,REDIS_FORCE_REPL標誌強制主伺服器將當前執行的命令複製給所有從伺服器。執行PUBSUB命令會使用戶端開啟REDIS_FORCE_AOF標誌,執行SCRIPT_LOAD命令會使用戶端開啟REDIS_FORCE_AOF標誌和REDIS_FORCE_REPL標誌
  • 在主從伺服器進行命令傳播期間,從伺服器需要向主伺服器發送REPLICATIONACK命令,在發送這個命令之前,從伺服器必須開啟主伺服器對應的用戶端的REDIS_MASTER_FORCE_REPLY標誌,否則發送操作會被拒絕執行

以上提到的所有標誌都定義在redis.h檔案中

PUBSUB命令和SCRIPT LOAD命令的特殊性

通常情況下,Redis只會將那些對資料庫進行了修改的命令寫入到AOF檔案,並複製到各個從伺服器。如果一個命令沒有對資料庫進行任何修改,那麼它就會被認為是唯讀命令,這個命令不會被寫入到AOF檔案,也不會被複製到從伺服器

以上規則適用於絕大部分Redis命令,但PUBSUB命令和SCRIPT LOAD命令是其中的例外。PUBSUB命令雖然沒有修改資料庫,但PUBSUB命令向頻道的所有訂閱者發送訊息這一行為帶有副作用,接收到訊息的所有用戶端的狀態都會因為這個命令而改變。因此,伺服器需要使用REDIS_FORCE_AOF標誌,強制將這個命令寫入AOF檔案,這樣在將來載入AOF檔案時,伺服器就可以再次執行相同的PUBSUB命令,併產生相同的副作用。SCRIPT LOAD命令的情況與PUBSUB命令類似:雖然SCRIPT LOAD命令沒有修改資料庫,但它修改了伺服器狀態,所以它是一個帶有副作用的命令,伺服器需要使用REDIS_FORCE_AOF標誌,強制將這個命令寫入AOF檔案,使得將來在載入AOF檔案時,伺服器可以產生相同的副作用

另外,為了讓主伺服器和從伺服器都可以正確地載入SCRIPT LOAD命令指定的指令碼,伺服器需要使用REDIS_FORCE_REPL標誌,強制將SCRIPT LOAD命令複製給所有從伺服器

以下是一些flags屬性的例子

# 用戶端是一個主伺服器REDIS_MASTER# 用戶端正在被列表命令阻塞REDIS_BLOCKED# 用戶端正在執行事務,但事務的安全性已被破壞REDIS_MULTI | REDIS_DIRTY_CAS# 用戶端是一個從伺服器,並且版本低於Redis 2.8 REDIS_SLAVE | REDIS_PRE_PSYNC# 這是專門用於執行Lua指令碼包含的Redis命令的偽用戶端# 它強制服務器將當前執行的命令寫入AOF檔案,並複製給從伺服器REDIS_LUA_CLIENT | REDIS_FORCE_AOF| REDIS_FORCE_REPL

  

輸入緩衝區

用戶端狀態的輸入緩衝區用於儲存用戶端發送的命令請求

redis.h

typedef struct redisClient {    ……    sds querybuf;    ……} redisClient;

  

舉個栗子,如果用戶端向伺服器發送了以下命令請求:

SET key value

  

那麼用戶端狀態的querybuf屬性將是一個包含以下內容的SDS值:

*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n

  

圖1-4展示了這個SDS值以及querybuf屬性的樣子

圖1-4   querybuf屬性樣本

命令與命令參數

在伺服器將用戶端發送的命令請求儲存到用戶端狀態的querybuf屬性之後,伺服器將對命令請求的內容進行分析,並將得出的命令參數以及命令參數的個數分別儲存到用戶端狀態的argv屬性和argc屬性:

redis.h

typedef struct redisClient {    ……    int argc;    robj **argv;    ……} redisClient;

  

argv屬性是一個數組,數組中的每個項都是一個字串對象,其中argv[0]是要執行的命令,而之後的其他項則是傳給命令的參數。argc屬性負責記錄argv數組的長度

舉個栗子,對於圖1-4所示的querybuf屬性來說,伺服器將分析並建立圖1-5所示的argv屬性和argc屬性

圖1-5   argv屬性和argc屬性樣本

注意,圖1-5展示的用戶端狀態中,argc屬性的值為3,而不是2,因為命令的名字"SET"本身也是一個參數

命令的實現函數

當伺服器從協議內容中分析並得出argv屬性和argc屬性的值之後,伺服器將根據項argv[0]的值,在命令表中尋找命令所對應的命令實現函數。圖1-6展示了一個命令表示例,該表是一個字典,字典的鍵是一個SDS結構體,儲存了命令的名字,字典的值是命令所對應的redisCommand結構體,這個結構體儲存了命令的實現函數、命令的標誌、命令應該給定的參數個數、命令的總執行次數和總消耗時間長度等統計資訊

圖1-6   命令表

當程式在命令表中成功找到argv[0]所對應的redisCommand結構體時,它會將用戶端狀態的cmd指標指向這個結構體:

redis.h

typedef struct redisClient {    ……    struct redisCommand *cmd;    ……} redisClient;

  

之後,伺服器就可以使用cmd屬性所指向的redisCommand結構體,以及argv、argc屬性中儲存的命令參數資訊,調用命令實現函數,執行用戶端指定的命令

圖1-7示範了伺服器在argv[0]為"SET"時,尋找命令表並將用戶端狀態的cmd指標指向目標redisCommand結構體的整個過程

圖1-7   尋找命令並設定cmd屬性

針對命令表的尋找操作不區分輸入字母的大小寫,所以無論argv[0]是"SET"、"set"或者是"SeT"等等,尋找結果都是相同的

輸出緩衝區

執行命令所得的命令回複會被儲存在用戶端狀態的輸出緩衝區裡面,每個用戶端都有兩個輸出緩衝區可用,一個緩衝區的大小是固定的,另一個緩衝區的大小是可變的

  • 固定大小的緩衝區用於儲存那些長度比較小的回複,比如OK、簡短的字串值、整數值、錯誤回複等等
  • 可變大小的緩衝區用於儲存那些長度比較大的回複,比如一個非常長的字串值、一個由很多項組成的列表、一個包含了很多元素的集合等等

用戶端的固定大小緩衝區由buf和bufpos兩個屬性群組合:

redis.h

typedef struct redisClient {    ……    int bufpos;    char buf[REDIS_REPLY_CHUNK_BYTES];} redisClient;

  

buf是一個大小為REDIS_REPLY_CHUNK_BYTES位元組的位元組數組,而bufpos屬性則記錄了buf數組目前已使用的位元組數量。REDIS_REPLY_CHUNK_BYTES常量目前的預設值為16*1024,也即是說,buf數組的預設大小為16KB

圖1-8展示了一個使用固定大小緩衝區來儲存傳回值+OK\r\n的例子

圖1-8   固定大小緩衝區樣本

當buf數組的空間已經用完,或者回複因為太大而無法裝進buf數組裡面,伺服器就會開始使用可變大小緩衝區。可變大小緩衝區由reply鏈表和一個或多個字串對象組成:

redis.h

typedef struct redisClient {    ……    list *reply;    ……} redisClient;

  

通過使用鏈表來串連多個字串對象,伺服器可以為用戶端儲存一個非常長的命令回複,而不必受到固定大小緩衝區16KB大小的限制。圖1-9展示了一個包含三個字串對象的reply鏈表

圖1-9   可變大小緩衝區樣本

身分識別驗證

用戶端狀態的authenticated屬性用於記錄用戶端是否通過了身分識別驗證:

redis.h

typedef struct redisClient {    ……    int authenticated;      /* when requirepass is non-NULL */    ……} redisClient;

  

如果authenticated的值為0,表示用戶端尚未通過身份認證;如果authenticated的值為1,表示用戶端已通過認認證。舉個栗子,對於一個尚未進程身份認證的用戶端來說,用戶端狀態的authenticated屬性1-10所示

圖1-10   未驗證身份時的用戶端狀態

當用戶端authenticated屬性的值為0時,除了AUTH命令之外,用戶端發送的所有其他命令都會被伺服器拒絕執行:

127.0.0.1:6379> PING(error) NOAUTH Authentication required.127.0.0.1:6379> SET msg "hello world"(error) NOAUTH Authentication required.

  

當用戶端通過AUTH命令成功進行身分識別驗證之後,用戶端狀態authenticated屬性的值從0變為1,1-11所示,這時用戶端就可以像往常一樣向伺服器發送命令請求了: 

127.0.0.1:6379> AUTH 123456OK127.0.0.1:6379> PINGPONG127.0.0.1:6379> SET msg "hello world"OK

  

圖1-11   已經通過身分識別驗證的用戶端狀態

 

authenticated屬性僅在伺服器啟用了身分識別驗證功能時使用,如果伺服器沒有啟用身分識別驗證功能的話,那麼即使authenticated屬性的值為0(預設值),伺服器也不會拒絕未驗證身份的用戶端發送的命令請求。關於伺服器身分識別驗證的更多資訊可以參考設定檔對requirepass選項的相關說明

時間

最後,用戶端還有幾個和時間相關的屬性:

redis.h

typedef struct redisClient {    ……    time_t ctime;              time_t lastinteraction;     time_t obuf_soft_limit_reached_time;    ……} redisClient;

  

ctime屬性記錄了建立用戶端的時間,這個時間可以用來計算用戶端與伺服器已經串連了多少秒,CLIENT list命令的age域記錄了這個秒數

127.0.0.1:6379> CLIENT list…… age=87315 ………… age=535 ……

    

lastinteraction屬性記錄了用戶端與伺服器最後一次進行互動(interaction)的時間,這裡的互動可以是用戶端向伺服器發送命令請求,也可以是伺服器向用戶端發送命令回複。lastinteraction屬性可以用來計算用戶端的空轉時間,也即是,距離用戶端與伺服器最後一次進行互動以來,已經過去多少秒,CLIENT list命令的idle域記錄了這個秒數

127.0.0.1:6379> CLIENT list…… idle=10916 ………… idle=0 ……

  

obuf_soft_limit_reached_time屬性記錄了輸出緩衝區第一次達到軟性限制(soft time)的時間

用戶端的建立與關閉

伺服器使用不同的方式來建立和關閉不同類型的用戶端,本節將介紹伺服器建立和關閉用戶端的方法

建立普通用戶端

如果用戶端是通過網路連接和伺服器進行串連的普通用戶端,那麼在用戶端使用connect函數串連到服務端時,伺服器就會調用串連事件處理器,為用戶端建立相應的用戶端狀態,並將這個用戶端狀態添加到伺服器狀態結構體中clients鏈表的末尾

舉個栗子,假設當前有c1和c2兩個普通用戶端正在串連伺服器,那麼當一個新的普通用戶端c3串連到伺服器後,伺服器會將c3所對應的用戶端狀態添加到clients鏈表的末尾,1-12所示,其中用虛線包圍的就是伺服器為c3新建立的用戶端狀態

圖1-12   伺服器狀態結構體的clients鏈表

關閉普通用戶端

一個普通用戶端可以因為多種原因而被關閉:

  •  如果用戶端進程退出或者被殺死,那麼用戶端與伺服器之間的網路連接將被關閉,從而造成用戶端被關閉
  • 如果用戶端向伺服器發送了帶有不符合協議格式的命令請求,那麼這個用戶端也會被伺服器關閉
  • 如果用戶端成為了CLIENTKILL命令的目標,那麼它也會被關閉
  • 如果使用者為伺服器設定了timeout配置選項,那麼當用戶端的空轉時間超過timeout選項設定的值時,用戶端將被關閉。不過timeout選項有一些例外情況:如果用戶端是主伺服器(開啟了REDIS_MASTER標誌),從伺服器(開啟了REDIS_SLAVE標誌),正在被BLPOP等命令阻塞(開啟了REDIS_BLOCKED標誌),或者正在執行SUBSCRIBE, PSUBSCRIBE等訂閱命令,那麼即使用戶端的空轉時間超過了timeout選項的值,用戶端也不會被伺服器關閉
  • 如果用戶端發送的命令請求的大小超過了輸入緩衝區的限制大小(預設為1GB),那麼這個用戶端會被伺服器關閉
  • 如果要發送給用戶端的命令回複的大小超過了輸出緩衝區的限制大小,那麼這個用戶端會被伺服器關閉

前面介紹輸出緩衝區的時候提到過,可變大小緩衝區由一個鏈表和任意多個字串對象組成,理論上來說,這個緩衝區可以儲存任意長度的命令回複。但為了避免用戶端回複過大,佔用過多伺服器資源,伺服器會時刻檢查用戶端的輸出緩衝區的大小,並在緩衝區的大小超出範圍時,執行相應的限制操作。伺服器使用兩種模式來限制用戶端輸出緩衝的大小:

  • 硬性限制(hard limit):如果輸出緩衝區的大小超過了硬性限制所設定的大小,那麼伺服器立即關閉用戶端
  • 軟性限制(soft limit):如果輸出緩衝區的大小超過了軟性限制所設定的大小,但還沒有超過硬性限制,那麼伺服器將使用伺服器狀態結構的 obuf_soft_limit_reached_time 屬性記錄下用戶端到達軟性限制的起始時間,之後伺服器會繼續監視用戶端,如果輸出緩衝區的大小一直超出軟性限制,並且期間超過伺服器設定的時間長度,那麼伺服器就會關閉用戶端,相反地,如果用戶端在指 定時間內不再超出軟性限制,那麼用戶端就不會被關閉,並且obuf_soft_limit_reached_time也會被清零

使用client-output-buffer-limit 可以為普通用戶端、從伺服器用戶端、執行發布與訂閱功能的用戶端分別設定不同的軟性限制和硬性限制,格式為:

client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>

  

以下是三個設定樣本:

client-output-buffer-limit normal 0 0 0  #不限制輸出緩衝區大小client-output-buffer-limit slave 256mb 64mb 60client-output-buffer-limit pubsub 32mb 8mb 60 #執行發布和訂閱功能的用戶端硬性限制為32mb,軟性限制為8mb,軟性限制時間長度為60秒

  

第一行設定將普通用戶端的硬性限制和軟性限制都設定為0,表示不限制用戶端的輸出緩衝區大小

第二行設定將從伺服器用戶端的硬性限制設定為256MB,而軟性限制設定為64MB,軟性限制的時間長度為60秒

第三行設定將執行發布與訂閱功能的用戶端的硬性限制設定為32MB,軟性限制設定為8MB,軟性限制的時間長度為60秒

關於client-output-buffer-limit選項的更多用法,可以參考樣本設定檔redis.conf

Lua指令碼的偽用戶端

伺服器會在初始化時建立負責執行Lua指令碼中包含的Redis命令的偽用戶端,並將這個偽用戶端關聯在伺服器結構的lua_client屬性中:

redis.h

struct redisServer {    ……    redisClient *lua_client;      ……};

  

lua_client偽用戶端在伺服器啟動並執行整個生命週期中會一直存在,只有伺服器被關閉時,這個用戶端才會被關閉

AOF檔案的偽用戶端

伺服器在載入AOF檔案時,會建立用於執行AOF檔案包含的Redis命令的偽用戶端,並在載入完成之後,關閉這個偽用戶端

 

Redis實現之用戶端

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.