Redis底層協議RESP詳解

RESP

文章開始前,先放出兩道面試題
1.Redis底層,使用的什麼協議?
2.RESP是什麼,在Redis怎麼體現的?

帶着這兩個問題,來一探究竟。

什麼是 RESP?

是基於TCP的應用層協議 RESP(REdis Serialization Protocol);
RESP底層採用的是TCP的連接方式,通過tcp進行數據傳輸,然後根據解析規則解析相應信息,

Redis 的客戶端和服務端之間採取了一種獨立名爲 RESP(REdis Serialization Protocol) 的協議,作者主要考慮了以下幾個點:

  • 容易實現
  • 解析快
  • 人類可讀
    RESP可以序列化不同的數據類型,如整數,字符串,數組。還有一種特定的錯誤類型。請求從客戶端發送到Redis服務器,作爲表示要執行的命令的參數的字符串數組。Redis使用特定於命令的數據類型進行回覆。
    RESP是二進制安全的,不需要處理從一個進程傳輸到另一個進程的批量數據,因爲它使用前綴長度來傳輸批量數據。
    注意:RESP 雖然是爲 Redis 設計的,但是同樣也可以用於其他 C/S 的軟件。Redis Cluster使用不同的二進制協議(gossip),以便在節點之間交換消息。

關於協議的具體描述,官方文檔 https://redis.io/topics/protocol

RESP協議說明

RESP協議是在Redis 1.2中引入的,但它成爲了與Redis 2.0中的Redis服務器通信的標準方式。這是所有Redis客戶端都要遵循的協議,我們甚至可以基於此協議,開發實現自己的Redis客戶端。
RESP實際上是一個支持以下數據類型的序列化協議:簡單字符串,錯誤類型,整數,批量字符串和數組。

RESP在Redis中用作請求-響應協議的方式如下:

  • 客戶端將命令作爲Bulk Strings的RESP數組發送到Redis服務器。
  • 服務器根據命令實現回覆一種RESP類型。
    在RESP中,某些數據的類型取決於第一個字節:
  • “+”代表簡單字符串Simple Strings
  • “+”代表錯誤類型
  • “:”代表整數
  • “$”代表Bulk Strings
  • “*”代表數組
    此外,RESP能夠使用稍後指定的Bulk Strings或Array的特殊變體來表示Null值。
    在RESP中,協議的不同部分始終以“\r\n”(CRLF)結束。

看了RESP的協議說明,我們該如何驗證呢?

RESP驗證

既然我們知道,Redis客戶端與server端通信,本身就是基於tcp的一個Request/Response模式。並且jedis與redis底層通信基於socket,是遵循resp通信協議。
我們不妨用網絡抓包工具,攔截客戶端與server端傳輸的數據、一探究竟。
實驗:使用jedis客戶端向server端發送命令,攔截TCP數據傳輸(Redis 6379端口),深度探究RESP協議。
這裏使用的工具自行發揮,tcpdump、wireshark均可。目標就是抓到6379端口的傳輸數據。

我這裏選擇了一種取巧的方式:
因爲我們經常使用redis-cli客戶端操作Redis,既然redis-cli作爲官方提供的Redis客戶端,必然遵循了RESP協議;
我們使用redis-cli操作redis-server,大膽推斷肯定是redis-cli基於RESP和redis-server的6379端口進行了通信。
那麼,我們使用一個demo程序(SocketListener),監聽本地的6379端口,接收到數據就打印;
另外,使用redis-cli客戶端執行set key value,看看向6379端口發送了什麼。
ps:SocketListener先運行,再在redis-cli窗口執行命令。

當用redis-cli客戶端發送命令時會打印

public class SocketListener {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(6379);
        Socket socket = serverSocket.accept();
        byte[] bytes = new byte[1024];
        socket.getInputStream().read(bytes);
        System.out.println(new String(bytes));
    }
}

如果沒有TCP監控工具的同學,可以通過這種方式驗證。

最後給出攔截到的TCP數據:
SET key value #對應的resp通信協議串

*3
$3
SET
$3
key
$5
value

第一次看到這個通信協議串,看不懂不必擔心,我們按照RESP的協議說明慢慢看,並且下文會有詳細的講解。
這裏大概翻譯一下這段傳輸的數據含義:

  • 第一行*3表示這條發給Redis server的命令是數組,數組有3個元素(其實就是SET、key、value這仨字符串);
    後面的6行數據,分別是對數組三個元素的表示,每個元素用兩行;
  • 數組第一個元素:$3 SET $3代表Bulk Strings字符串長度爲3,內容是SET。
  • 數組第二個元素:$3 key $3代表Bulk Strings字符串長度爲3,key。
  • 數組第三個元素:$5 value $5代表Bulk Strings字符串長度爲5,內容是value。

是不是很簡單呢。RESP協議傳輸的數據,不僅人類可讀、容易實現,還解析快。
我們繼續驗證,Redis最常用的客戶端工具jedis是否也是同樣的格式呢?

    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        System.out.println(jedis.set("abc", "110"));
        System.out.println(jedis.get("mt"));
    }

結果是肯定的,使用TCP抓包工具、獲取到Jedis客戶端發給Redis server的數據也是上面的格式。

Jedis客戶端小結

Jedis跟redis通過socket建立通信。
Jedis與redis服務進行交互通信,本質是通過socke(長連接),發送由resp協議規定的指令集。
AOF持久化方式:就是存儲了resp指令。可以查看.aof持久化文件。


下面我們一一來看RESP支持的序列化數據類型:簡單字符串,錯誤類型,整數,批量字符串和數組。
說明:下面的類型說明中,\r\n都是顯示的添加上去的,是爲了讓大家理解RESP協議實際數據傳輸的格式,在redis-cli客戶端中,命令執行返回的響應,是

簡單字符串Simple Strings

簡單字符串按以下方式編碼:+字符,後跟不能包含CR或LF字符的字符串(不允許換行),由CRLF終止(即“\r\n”)。
Simple Strings用於以最小的開銷、傳輸非二進制安全字符串。例如,許多Redis命令在成功時僅回覆“OK”,因爲RESP Simple String使用以下5個字節進行編碼:
+OK\r\n
當Redis使用Simple String回覆時,該字符串由’+'之後的第一個字符組成,直到字符串結尾,不包括最終的CRLF字節。

RESP錯誤

RESP具有特定的錯誤數據類型。實際上錯誤與RESP Simple Strings完全相同,但第一個字符是減-字符而不是加號。RESP中簡單字符串和錯誤之間的真正區別在於客戶端將錯誤視爲異常,組成錯誤類型的字符串是錯誤消息本身。
基本格式是:-ERR errorMsg\r\n
錯誤回覆僅在發生錯誤時發送,例如,如果你嘗試對錯誤的數據類型執行操作,或者命令不存在等等。收到錯誤答覆時,庫客戶端應引發異常。
以下是錯誤回覆的示例:

-ERR unknown command 'foobar'
-WRONGTYPE Operation against a key holding the wrong kind of value

“-”之後的第一個單詞,直到第一個空格或換行符,表示返回的錯誤類型。這只是Redis使用的約定,不是RESP錯誤格式的一部分。
例如,ERR是一般錯誤,而WRONGTYPE更具體的錯誤意味着客戶端嘗試對錯誤的數據類型執行操作。這稱爲錯誤前綴,是一種允許客戶端理解服務器返回的錯誤類型的方法,而不依賴於給定的確切消息,這可能隨時間而變化。
下面是幾個使用redis-cli的實際錯誤的例子:

127.0.0.1:6379> TaoBeier
-ERR unknown command 'TaoBeier'\r\n  # 服務端實際返回, 下同
---
(error) ERR unknown command 'TaoBeier'  # redis-cli 客戶端顯示, 下同
127.0.0.1:6379> set name TaoBeier love
-ERR syntax error\r\n
---
(error) ERR syntax error

客戶端實現可以針對不同的錯誤返回不同類型的異常,或者可以通過直接將錯誤名稱作爲字符串提供給調用者來提供捕獲錯誤的通用方法。
但是,錯誤類型很少有用,並且有限的客戶端實現可能只是返回一般的錯誤條件,例如false。

整數類型

此類型只是一個CRLF終止的字符串,表示一個以字節爲前綴的整數。例如:0 \r\n:1000 \r\n是整數回覆。

很多Redis命令返回RESP整數類型,比如INCR,LLEN和LASTSAVE。
返回的整數沒有特殊含義,它只是INCR的增量數,LASTSAVE的UNIX時間等等。但是,返回的整數保證在有符號的64位整數範圍內。

整數回覆也被廣泛使用以返回真或假。例如,EXISTS或SISMEMBER之類的命令將返回1表示true,0表示false表示。
如果操作實際執行,其他命令如SADD,SREM和SETNX將返回1,否則返回0。
下面的命令都是整數類型回覆:SETNX,DEL, EXISTS,INCR,INCRBY,DECR,DECRBY,DBSIZE,LASTSAVE, RENAMENX,MOVE,LLEN,SADD,SREM,SISMEMBER,SCARD。

Bulk Strings類型

翻譯過來,是指批量、多行字符串。
Bulk Strings用於表示長度最大爲512MB的單個二進制安全字符串。
批量字符串按以下方式編碼:

  • 一個“$”字節後跟組成字符串的字節數(一個前綴長度),由CRLF終止。
  • 實際的字符串數據。
  • 最終的CRLF。
    所以字符串“foobar”的編碼如下:
$6\r\n
foobar\r\n"

一個完整的Bulk Strings,主要包括兩行:
第一行,$後面跟上字符串長度;
第二行,就是實際的字符串。
如下面執行set、get的例子:

127.0.0.1:6379> set site ljheee
+OK\r\n  # 服務端實際返回, 下同
---
OK   # redis-cli 客戶端顯示, 下同
127.0.0.1:6379> get site
$6\r\
ljheee\r\n
---
"ljheee"

在執行set site value時,客戶端給Redis server發送RESP命令後,Redis server返回的是simple strings類型+OK\r\n,redis-cli命令行客戶端給我們只顯示了有效字符、省略了最後的CRLF。
在執行get site時,Redis server返回的是Bulk Strings類型,第一行$6代表site對應的value值length爲6,第二行是實際value值。

當只是一個空字符串時,表示爲:$0\r\n
Bulk Strings也可用於使用用於表示Null值的特殊格式來表示值的不存在。在這種特殊格式中,長度爲-1,並且沒有數據,因此Null表示爲:$-1\r\n,這稱爲Null Bulk String。

數組類型

客戶端使用數組、將命令發送到Redis服務器。類似地,某些Redis命令將元素集合返回給客戶端使用數組類型回覆。如LRANGE命令,它返回元素列表其實就是數組類型。

RESP數組使用以下格式發送:

  • 它以 “*” 開頭,後面跟着返回元素的個數,後跟CRLF。
  • 然後就是數組中各元素自己的類型了,數組每個元素可以是任意的RESP類型。

最典型的是 LRRANGE 命令,返回的就是數組類型

LRANGE info 0 -1
*2\r\n
$3\r\
abc\r\n
$6\r\n
ljheee\r\n
--- # 實際redis-cli顯示
1) "abc"
2) "ljheee"

返回的結果,*2代表數組長度爲2,數組的第一個元素$3是長度爲三的字符串abc;數組的第二個元素$6是長度爲6的字符串ljheee。

好了,到這裏我們瞭解了Redis底層通信協議的各個類型,基於此,我們可以實現自己的Redis客戶端。
(手寫Redis客戶端,請看下文)

本文首發於公衆號 架構道與術(ToBeArchitecturer),歡迎關注、學習更多幹貨~

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