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 |
|---|---|---|---|---|---|---|
| MySQL | MySQL Protocol | 二進位 | Layer 7 | Layer 4 | TCP | 3306 |
| PostgreSQL | PostgreSQL Protocol | 二進位 | Layer 7 | Layer 4 | TCP | 5432 |
| Redis | RESP | 文字 | Layer 7 | Layer 4 | TCP | 6379 |
| MongoDB | Wire Protocol | 二進位(BSON) | Layer 7 | Layer 4 | TCP | 27017 |
重點:
- RESP 是應用層協定(兩種模型都是)
- 使用 TCP 作為傳輸層(Port 6379)
- 文字協定(相對於 MySQL/MongoDB 的二進位協定)
- 可以直接用 Telnet 測試(因為是文字協定)
🏗️ RESP 協定特性
為什麼 Redis 要設計專屬協定?
對比 HTTP:
| 特性 | HTTP | RESP |
|---|---|---|
| 複雜度 | 複雜(Header + Body) | 簡單(純文字) |
| 解析速度 | 慢 | 快 |
| 傳輸開銷 | 大 | 小 |
| 人類可讀 | ✅ 是 | ✅ 是 |
| 效能 | 中 | 高 |
RESP 設計原則:
- ✅ 簡單:人類可讀的純文字協定
- ✅ 快速:容易解析,效能高
- ✅ 錯誤處理:內建錯誤訊息
- ✅ 二進位安全:支援任意 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\n2️⃣ 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\n3️⃣ 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 AliceRESP 格式(發送):
*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\ntelnet 實際操作:
$ 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 nameRESP 格式(發送):
*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 counterRESP 格式:
*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 -1RESP 格式:
*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 --pipePython 範例:
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
| 特性 | Pipeline | Transaction (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 MONITORQ6: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 種資料類型:
- + Simple Strings - 簡單字串(+OK\r\n)
- - Errors - 錯誤(-ERR…\r\n)
- : Integers - 整數(:1000\r\n)
- $ Bulk Strings - 批量字串($5\r\nHello\r\n)⭐
- * 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