05-3. Redis Protocol (RESP) 完整指南

深入理解 Redis 序列化協定,掌握 Redis 通訊原理

🔴 Redis Protocol (RESP) 完整指南

⏱️ 閱讀時間: 15 分鐘 🎯 難度: ⭐⭐ (中等)


🎯 本篇重點

理解 Redis 序列化協定 (RESP) 的原理、5 種資料類型、實際操作範例,以及如何用 telnet 直接測試 Redis,掌握 Pipeline 批次操作優化效能。


🤔 什麼是 RESP?

RESP (REdis Serialization Protocol) = Redis 序列化協定

一句話解釋: RESP 是 Redis 客戶端與伺服器之間的「通訊語言」,就像摩斯密碼一樣,用簡單的符號表示不同類型的資料。

比喻:快遞單
當你寄包裹時,快遞單上會用不同的符號標記:
📦 一般包裹
⚠️  易碎品
❄️  冷凍
💰 貴重物品

RESP 也是一樣,用不同的「符號」標記不同類型的資料

🏗️ RESP 在網路模型中的位置

OSI 7 層模型

┌──────────────────────────────┬─────────────────┐
│ 7. Application Layer (應用層) │  RESP (Redis)   │ ← RESP 在這裡
├──────────────────────────────┼─────────────────┤
│ 6. Presentation Layer (表示層)│  加密、壓縮      │
├──────────────────────────────┼─────────────────┤
│ 5. Session Layer (會話層)     │  建立、維護會話  │
├──────────────────────────────┼─────────────────┤
│ 4. Transport Layer (傳輸層)   │  TCP            │
├──────────────────────────────┼─────────────────┤
│ 3. Network Layer (網路層)     │  IP             │
├──────────────────────────────┼─────────────────┤
│ 2. Data Link Layer (資料鏈結層)│  Ethernet       │
├──────────────────────────────┼─────────────────┤
│ 1. Physical Layer (實體層)    │  網路線、光纖    │
└──────────────────────────────┴─────────────────┘

RESP 位於第 7 層(應用層)

  • RESP (REdis Serialization Protocol) 是應用層協定
  • 提供 Redis 資料序列化與通訊
  • 文字協定,人類可讀(易於除錯)

TCP/IP 4 層模型

┌─────────────────────────────┬─────────────────┐
│ 4. Application Layer (應用層) │  RESP (Redis)   │ ← RESP 在這裡
├─────────────────────────────┼─────────────────┤
│ 3. Transport Layer (傳輸層)  │  TCP            │
├─────────────────────────────┼─────────────────┤
│ 2. Internet Layer (網際網路層)│  IP             │
├─────────────────────────────┼─────────────────┤
│ 1. Network Access (網路存取層)│  Ethernet       │
└─────────────────────────────┴─────────────────┘

RESP 位於第 4 層(應用層)

  • 在 TCP/IP 模型中,RESP 是應用層協定
  • 使用 TCP 作為傳輸層協定(Port 6379)
  • TCP 提供可靠的連線導向傳輸

對比表:

資料庫協定協定類型OSI 層級TCP/IP 層級底層協定Port
MySQLMySQL Protocol二進位Layer 7Layer 4TCP3306
PostgreSQLPostgreSQL Protocol二進位Layer 7Layer 4TCP5432
RedisRESP文字Layer 7Layer 4TCP6379
MongoDBWire Protocol二進位(BSON)Layer 7Layer 4TCP27017

重點:

  • RESP 是應用層協定(兩種模型都是)
  • 使用 TCP 作為傳輸層(Port 6379)
  • 文字協定(相對於 MySQL/MongoDB 的二進位協定)
  • 可以直接用 Telnet 測試(因為是文字協定)

🏗️ RESP 協定特性

為什麼 Redis 要設計專屬協定?

對比 HTTP:

特性HTTPRESP
複雜度複雜(Header + Body)簡單(純文字)
解析速度
傳輸開銷
人類可讀✅ 是✅ 是
效能

RESP 設計原則:

  1. 簡單:人類可讀的純文字協定
  2. 快速:容易解析,效能高
  3. 錯誤處理:內建錯誤訊息
  4. 二進位安全:支援任意 binary data

📋 RESP 的 5 種資料類型

💡 記憶口訣:「加減冒錢星」

+ Simple Strings  (簡單字串)
- Errors          (錯誤)
: Integers        (整數)
$ Bulk Strings    (批量字串)
* Arrays          (陣列)

1️⃣ Simple Strings(簡單字串)

格式: +內容\r\n

範例:
+OK\r\n
+PONG\r\n
+hello world\r\n

用途:
- 簡單的成功回應
- 不包含換行的短字串

實際例子:

客戶端:SET name Alice
伺服器:+OK\r\n

2️⃣ Errors(錯誤)

格式: -錯誤類型 錯誤訊息\r\n

範例:
-ERR unknown command 'foobar'\r\n
-WRONGTYPE Operation against a key holding the wrong kind of value\r\n
-ERR syntax error\r\n

用途:
- 回傳錯誤訊息
- 錯誤類型方便客戶端處理

實際例子:

客戶端:INVALID_COMMAND
伺服器:-ERR unknown command 'INVALID_COMMAND'\r\n

3️⃣ Integers(整數)

格式: :數字\r\n

範例:
:0\r\n
:1000\r\n
:-1\r\n

用途:
- 數字回應(計數、長度等)
- 布林值(1 = true, 0 = false)

實際例子:

客戶端:INCR counter
伺服器::1\r\n

客戶端:EXISTS name
伺服器::1\r\n  (存在)

客戶端:EXISTS nonexistent
伺服器::0\r\n  (不存在)

客戶端:LLEN mylist
伺服器::5\r\n  (list 長度為 5)

4️⃣ Bulk Strings(批量字串)⭐

格式: $長度\r\n內容\r\n

範例:
$5\r\nHello\r\n       → "Hello" (5 個字元)
$0\r\n\r\n             → "" (空字串)
$-1\r\n               → NULL (不存在)
$11\r\nhello world\r\n → "hello world" (11 個字元,包含空格)

用途:
- 包含換行的字串
- 二進位資料
- NULL 值

為什麼需要標示長度?

問題:如果字串包含 \r\n 怎麼辦?

錯誤做法:
+hello\r\nworld\r\n  ← 會被誤認為兩行

正確做法:
$12\r\nhello\r\nworld\r\n  ← 先告知長度 12,就不會搞混

實際例子:

客戶端:GET name
伺服器:$5\r\nAlice\r\n

客戶端:GET nonexistent
伺服器:$-1\r\n  (key 不存在,回傳 NULL)

5️⃣ Arrays(陣列)⭐⭐

格式: *元素數量\r\n元素1元素2...

範例:
*0\r\n  → 空陣列
*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n  → ["foo", "bar"]
*3\r\n:1\r\n:2\r\n:3\r\n  → [1, 2, 3]
*-1\r\n  → NULL

用途:
- 多個值的回應
- 命令參數
- 巢狀結構

巢狀陣列:

*2\r\n
*3\r\n:1\r\n:2\r\n:3\r\n
*2\r\n+Hello\r\n-Err\r\n

代表:
[
  [1, 2, 3],
  ["Hello", Error("Err")]
]

實際例子:

客戶端:LRANGE mylist 0 2
伺服器:
*3\r\n
$5\r\nfirst\r\n
$6\r\nsecond\r\n
$5\r\nthird\r\n

代表:["first", "second", "third"]

🔧 實戰:用 Telnet 測試 Redis

環境準備

# 1. 啟動 Redis
redis-server

# 2. 用 telnet 連線
telnet localhost 6379

實驗 1:SET 命令

Redis 命令:

SET name Alice

RESP 格式(發送):

*3\r\n
$3\r\nSET\r\n
$4\r\nname\r\n
$5\r\nAlice\r\n

解析:

*3                    → 陣列,3 個元素
$3\r\nSET\r\n        → "SET" (3 個字元)
$4\r\nname\r\n       → "name" (4 個字元)
$5\r\nAlice\r\n      → "Alice" (5 個字元)

伺服器回應:

+OK\r\n

telnet 實際操作:

$ telnet localhost 6379
Trying 127.0.0.1...
Connected to localhost.

# 輸入(手動輸入需要按 Ctrl+] 然後 send,較不方便)
*3
$3
SET
$4
name
$5
Alice

# 回應
+OK

實驗 2:GET 命令

Redis 命令:

GET name

RESP 格式(發送):

*2\r\n
$3\r\nGET\r\n
$4\r\nname\r\n

伺服器回應:

$5\r\nAlice\r\n  → "Alice"

如果 key 不存在:

GET nonexistent

回應:

$-1\r\n  → NULL

實驗 3:INCR 命令

Redis 命令:

INCR counter

RESP 格式:

*2\r\n
$4\r\nINCR\r\n
$7\r\ncounter\r\n

伺服器回應:

:1\r\n  → 1(第一次)

再執行一次:
:2\r\n  → 2

實驗 4:LPUSH 命令

Redis 命令:

LPUSH mylist "first" "second"

RESP 格式:

*4\r\n
$5\r\nLPUSH\r\n
$6\r\nmylist\r\n
$5\r\nfirst\r\n
$6\r\nsecond\r\n

伺服器回應:

:2\r\n  → list 目前有 2 個元素

實驗 5:LRANGE 命令

Redis 命令:

LRANGE mylist 0 -1

RESP 格式:

*4\r\n
$6\r\nLRANGE\r\n
$6\r\nmylist\r\n
$1\r\n0\r\n
$2\r\n-1\r\n

伺服器回應:

*2\r\n
$6\r\nsecond\r\n
$5\r\nfirst\r\n

代表:["second", "first"]
(LPUSH 是從左邊插入,所以順序相反)

🚀 Pipeline(批次操作)

什麼是 Pipeline?

💡 比喻:郵局寄信

一般模式(Request-Response):
你:寄一封信
郵局:好,寄出了
你:再寄一封信
郵局:好,寄出了
你:再寄一封信
郵局:好,寄出了
→ 需要往返 3 次

Pipeline 模式:
你:這裡有 3 封信,一次寄出
郵局:好,3 封都寄出了
→ 只需要往返 1 次

Pipeline 優勢

沒有 Pipeline(RTT = 10ms):

SET key1 val1  → 等待 10ms
SET key2 val2  → 等待 10ms
SET key3 val3  → 等待 10ms
總時間:30ms

使用 Pipeline:

SET key1 val1 \
SET key2 val2  → 批次發送
SET key3 val3 /
總時間:10ms(省 66% 時間!)

Pipeline 實戰範例

使用 redis-cli:

# 建立 pipeline 命令檔
cat > commands.txt << 'EOF'
SET key1 value1
SET key2 value2
SET key3 value3
INCR counter
INCR counter
GET key1
EOF

# 執行 pipeline
cat commands.txt | redis-cli --pipe

Python 範例:

import redis

r = redis.Redis(host='localhost', port=6379)

# 建立 pipeline
pipe = r.pipeline()

# 批次加入命令(不會立即執行)
pipe.set('key1', 'value1')
pipe.set('key2', 'value2')
pipe.set('key3', 'value3')
pipe.incr('counter')
pipe.get('key1')

# 一次執行所有命令
results = pipe.execute()
print(results)  # [True, True, True, 1, b'value1']

Node.js 範例:

const redis = require('redis');
const client = redis.createClient();

const pipeline = client.pipeline();

pipeline.set('key1', 'value1');
pipeline.set('key2', 'value2');
pipeline.incr('counter');
pipeline.get('key1');

pipeline.exec((err, results) => {
  console.log(results);
  // ['OK', 'OK', 1, 'value1']
});

Pipeline vs Transaction

特性PipelineTransaction (MULTI/EXEC)
批次執行✅ 是✅ 是
減少 RTT✅ 是✅ 是
原子性❌ 否✅ 是
回滾❌ 否❌ 否(Redis 不支援回滾)
中途失敗繼續執行繼續執行(已執行的不回滾)
適用場景批次操作需要原子性的操作

Pipeline 範例:

SET key1 val1
INVALID_COMMAND  ← 這個會失敗
SET key2 val2    ← 但這個仍會執行

Transaction 範例:

MULTI
SET key1 val1
INVALID_COMMAND  ← 語法錯誤,整個 transaction 放棄
SET key2 val2
EXEC
→ 都不會執行

🎓 面試常見問題

Q1:什麼是 RESP?為什麼 Redis 要設計專屬協定?

A:RESP (REdis Serialization Protocol) 是 Redis 的序列化協定

為什麼要專屬協定:
1. 簡單高效
   - 純文字協定,容易解析
   - 比 HTTP 更輕量,overhead 更小

2. 效能優異
   - 解析速度快(只需檢查第一個字元)
   - 傳輸開銷小(沒有 HTTP header)

3. 人類可讀
   - 方便除錯(可以用 telnet 直接測試)
   - 容易理解和學習

4. 二進位安全
   - 支援任意 binary data
   - 用長度標記,不會有解析問題

對比 HTTP:
- HTTP:複雜(Header + Body),解析慢
- RESP:簡單(符號 + 內容),解析快
- Redis 場景下,RESP 效能更好

Q2:RESP 有哪 5 種資料類型?各自用途是什麼?

A:「加減冒錢星」

1. + Simple Strings(簡單字串)
   格式:+OK\r\n
   用途:簡單成功回應、短字串

2. - Errors(錯誤)
   格式:-ERR unknown command\r\n
   用途:錯誤訊息

3. : Integers(整數)
   格式::1000\r\n
   用途:數字回應、計數、布林值

4. $ Bulk Strings(批量字串)⭐
   格式:$5\r\nHello\r\n
   用途:包含換行的字串、二進位資料、NULL

5. * Arrays(陣列)⭐
   格式:*3\r\n:1\r\n:2\r\n:3\r\n
   用途:多個值、命令參數、巢狀結構

面試重點:
- Bulk Strings 為什麼需要長度?
  → 支援包含 \r\n 的字串和二進位資料

- $-1 代表什麼?
  → NULL(key 不存在)

- Arrays 可以巢狀嗎?
  → 可以!*2\r\n*2\r\n:1\r\n:2\r\n*2\r\n:3\r\n:4\r\n
     代表 [[1, 2], [3, 4]]

Q3:Redis Pipeline 是什麼?有什麼優勢?

A:Pipeline 是批次操作技術

原理:
一般模式:
客戶端 → 命令1 → 伺服器 → 回應1 → 客戶端
客戶端 → 命令2 → 伺服器 → 回應2 → 客戶端
→ 每個命令都要等 RTT

Pipeline 模式:
客戶端 → 命令1、命令2、命令3 → 伺服器 → 回應1、2、3 → 客戶端
→ 只需要一次 RTT

優勢:
1. 減少 RTT(往返時間)
   - 沒有 Pipeline:n 個命令 = n 次 RTT
   - 使用 Pipeline:n 個命令 = 1 次 RTT

2. 提升吞吐量
   - 範例:RTT 10ms,Pipeline 可提升 10 倍效能

3. 降低系統調用
   - 減少 socket read/write 次數

注意事項:
❌ Pipeline 不保證原子性
❌ 不支援回滾
❌ 命令之間不能有依賴(結果不能作為下一個命令的輸入)

適用場景:
✅ 批次寫入大量資料
✅ 初始化資料
✅ 批次讀取多個 key

不適用場景:
❌ 命令之間有依賴(如:GET 結果再 SET)
❌ 需要原子性(應該用 MULTI/EXEC)

Q4:Pipeline vs Transaction(MULTI/EXEC)差異?

A:兩者都是批次執行,但保證不同

Pipeline:
- 目的:減少 RTT,提升效能
- 原子性:❌ 沒有
- 中途失敗:繼續執行後續命令
- 回滾:❌ 不支援
- 使用場景:批次操作(無依賴)

Transaction(MULTI/EXEC):
- 目的:保證原子性
- 原子性:✅ 有(全部執行或全部不執行)
- 中途失敗:
  - 語法錯誤:全部放棄
  - 執行錯誤:繼續執行(但不回滾)
- 回滾:❌ Redis 不支援回滾
- 使用場景:需要原子性的操作

範例:

Pipeline:
SET key1 val1
INVALID     ← 失敗
SET key2 val2  ← 仍執行
→ key1, key2 都會被設定

Transaction:
MULTI
SET key1 val1
INVALID     ← 語法錯誤
SET key2 val2
EXEC
→ 全部放棄,key1, key2 都不會被設定

結論:
- 需要效能 → Pipeline
- 需要原子性 → MULTI/EXEC
- 兩者可以結合使用!

Q5:如何用 telnet 測試 Redis?

A:步驟如下

1. 連線 Redis
   telnet localhost 6379

2. 發送 RESP 格式命令
   範例:SET name Alice

   RESP 格式:
   *3
   $3
   SET
   $4
   name
   $5
   Alice

3. 查看回應
   +OK

4. 發送 GET 命令
   *2
   $3
   GET
   $4
   name

5. 查看回應
   $5
   Alice

技巧:
- 用 \r\n 結尾(telnet 會自動處理)
- 可以用 nc(netcat)代替 telnet
- redis-cli --pipe 更方便批次操作

實用命令:
# 用 nc 發送 Pipeline
echo -e "*1\r\n\$4\r\nPING\r\n" | nc localhost 6379

# 用 redis-cli 監控協定
redis-cli --raw MONITOR

Q6:RESP 如何處理 NULL 值?

A:RESP 用特殊標記表示 NULL

Bulk Strings 的 NULL:
$-1\r\n

Arrays 的 NULL:
*-1\r\n

範例:

GET 不存在的 key:
客戶端:GET nonexistent
伺服器:$-1\r\n

對比:
GET 空字串:
客戶端:SET empty ""
伺服器:+OK\r\n
客戶端:GET empty
伺服器:$0\r\n\r\n  ← 空字串(長度 0)

HGETALL 不存在的 hash:
客戶端:HGETALL nonexistent
伺服器:*0\r\n  ← 空陣列

結論:
- Bulk String NULL = $-1\r\n
- 空字串 = $0\r\n\r\n
- 空陣列 = *0\r\n
- Array NULL = *-1\r\n

💡 實戰建議

1. 用 redis-cli 監控協定

# 啟動 monitor 模式
redis-cli MONITOR

# 在另一個終端執行命令
redis-cli SET name Alice
redis-cli GET name

# monitor 會顯示實際的 RESP 協定
# 方便學習和除錯

2. 用 Wireshark 抓包

1. 啟動 Wireshark
2. 篩選:tcp.port == 6379
3. 執行 Redis 命令
4. 查看 RESP 協定細節

優點:
- 看到完整的 \r\n
- 看到 TCP 層細節
- 理解網路傳輸過程

3. Pipeline 最佳實踐

import redis

r = redis.Redis()

# ❌ 錯誤:逐筆操作
for i in range(10000):
    r.set(f'key{i}', f'value{i}')
# 耗時:~3 秒

# ✅ 正確:使用 Pipeline
pipe = r.pipeline()
for i in range(10000):
    pipe.set(f'key{i}', f'value{i}')
pipe.execute()
# 耗時:~0.3 秒(快 10 倍!)

# ✅ 更好:分批執行(避免記憶體問題)
def batch_pipeline(items, batch_size=1000):
    pipe = r.pipeline()
    for i, item in enumerate(items):
        pipe.set(f'key{i}', item)

        if (i + 1) % batch_size == 0:
            pipe.execute()
            pipe = r.pipeline()

    if len(pipe) > 0:
        pipe.execute()

batch_pipeline(range(10000))

✅ 重點回顧

RESP 定義:

  • RESP = REdis Serialization Protocol
  • 純文字協定,簡單高效
  • 人類可讀,容易除錯

5 種資料類型:

  1. + Simple Strings - 簡單字串(+OK\r\n)
  2. - Errors - 錯誤(-ERR…\r\n)
  3. : Integers - 整數(:1000\r\n)
  4. $ Bulk Strings - 批量字串($5\r\nHello\r\n)⭐
  5. * Arrays - 陣列(*3\r\n:1\r\n:2\r\n:3\r\n)⭐

Pipeline:

  • 批次操作,減少 RTT
  • 效能提升 10 倍以上
  • ❌ 不保證原子性
  • ✅ 適合批次寫入

面試重點:

  • ✅ RESP 5 種類型與用途
  • ✅ Bulk String 為什麼需要長度
  • ✅ Pipeline vs Transaction 差異
  • ✅ 如何用 telnet 測試 Redis
  • ✅ NULL 的表示方式($-1, *-1)

記憶口訣:

  • 「加減冒錢星」= + - : $ *

上一篇: 05-2. PostgreSQL Protocol 下一篇: 05-4. MongoDB Wire Protocol

相關文章:


最後更新:2025-01-15

0%