01.3 - RESP 协议与通信模型
定位: Redis 自定义的序列化协议——简洁、高效、可读 面试高频度: ⭐⭐⭐ 考查方式: 协议格式理解、批量操作如何优化、与其它协议对比
一、这是什么?为什么需要它?
是什么
RESP(Redis Serialization Protocol)是 Redis 与服务端/客户端之间通信的序列化协议,从 1.2 版本开始成为标准。本质是一个基于文本的二进制安全协议。
为什么需要?
不使用 HTTP/gRPC 的原因:
HTTP/1.1:
GET /get/key HTTP/1.1\r\n
Host: localhost\r\n
\r\n
→ 仅 HTTP 头就 100-200 字节,内容解析复杂
gRPC/Protobuf:
→ 虽然高效,但需要 IDL 定义、代码生成、服务启动成本
RESP 的设计目标:面向 Redis 操作定制,极简解析
*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n
→ 总共 28 字节,手写解析器只需几十行 C 代码核心特性
| 特性 | 说明 | 为什么重要 |
|---|---|---|
| 文本协议 | 可读性强,telnet 就能调试 | 降低调试和故障排查成本 |
| 二进制安全 | 不依赖分隔符,长度前缀 | 可以传输任意二进制数据 |
| 简单的类型系统 | 5 种类型前缀 | 解析器实现极简单 |
| Pipeline 友好 | 请求不等待响应就发送下一条 | 批量操作性能大幅提升 |
| Pub/Sub 模式 | 支持双向推送 | 发布订阅的消息模型 |
二、原理拆解
2.1 RESP 协议的五种类型
类型前缀:
+ 简单字符串 Simple String ← 状态回复
- 错误 Error ← 错误回复
: 整数 Integer ← 整数回复
$ 批量字符串 Bulk String ← 二进制安全的字符串(可 null)
* 数组 Array ← 多个值的集合(可 null)每种类型的格式:
简单字符串: +OK\r\n
↑ ↑
标识符 结束符
错误: -ERR unknown command\r\n
整数: :1000\r\n
批量字符串: $5\r\nhello\r\n
↑ ↑
长度 数据
$-1\r\n → null 批量字符串
数组: *2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n
↑
元素个数
*-1\r\n → null 数组
*0\r\n → 空数组2.2 命令编码示例
redis
SET key value
→ *3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n
LPUSH list a b c
→ *4\r\n$5\r\nLPUSH\r\n$4\r\nlist\r\n$1\r\na\r\n$1\r\nb\r\n$1\r\nc\r\n
GET key 的响应(如果存在):
→ $5\r\nvalue\r\n
GET key 的响应(如果不存在):
→ $-1\r\n ← 这就是为什么 Redis 客户端 GET 返回 null
MGET key1 key2 key3 的响应:
→ *3\r\n$5\r\nvalue1\r\n$5\r\nvalue2\r\n$-1\r\n2.3 Pipeline:利用协议特性批量优化
实测效果:
- 单个命令:100,000 QPS(逐个发)
- Pipeline(100):5,000,000 QPS(批量100个一次发)
- 性能提升 50 倍
2.4 内联命令(用于 telnet 调试)
RESP 也支持简单的内联格式(仅用于调试):
SET key value\r\n ← 空格分隔,类似 shell 命令
GET key\r\n
内联格式不能用于二进制数据(可能包含空格),
生产客户端应该始终使用 RESP 格式。三、图解全景
RESP 的三条关键设计原则:
- 长度前缀 → 二进制安全
- 文本可读 → 调试友好
- 类型前缀 → 解析器轻量
四、实战验证
4.1 用 telnet 直接调试(感受 RESP)
bash
# telnet 到 Redis,发送原始 RESP 命令
telnet 127.0.0.1 6379
# 1. 内联命令
SET mykey hello
+OK
GET mykey
$5
hello
# 2. RESP 格式命令(手动输入)
*3
$3
SET
$4
name
$5
Alice
+OK
# 3. 查看不存在的 key
GET nonexist
$-14.2 用 nc 工具模拟 RESP
bash
# 创建 RESP 格式的命令文件
echo -ne '*3\r\n$3\r\nSET\r\n$4\r\nkey1\r\n$5\r\nvalue\r\n' > cmd.resp
# 发送命令
cat cmd.resp | nc -w1 127.0.0.1 6379
# 输出: +OK4.3 Pipeline 性能对比
bash
# 使用 redis-benchmark 验证 Pipeline 效果
# 1. 逐个发送(Pipeline = 1)
redis-benchmark -n 100000 -t SET -P 1 -q
# SET: 125000.00 requests per second
# 2. Pipeline = 100 批量发送
redis-benchmark -n 100000 -t SET -P 100 -q
# SET: 4800000.00 requests per second ← 38x 提升
# 注意:-P 太大可能导致服务端输出缓冲区爆炸
# 生产建议 Pipeline 大小:50-2004.4 用 Python 验证 RESP 协议
python
import socket
# 手动构造 RESP 协议
def redis_cmd(*args):
resp = f"*{len(args)}\r\n"
for arg in args:
resp += f"${len(arg)}\r\n{arg}\r\n"
return resp.encode()
# 发送到 Redis
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("127.0.0.1", 6379))
# 发送 SET 命令
sock.send(redis_cmd("SET", "protocol", "test"))
print(sock.recv(1024)) # b'+OK\r\n'
# 发送 GET 命令
sock.send(redis_cmd("GET", "protocol"))
print(sock.recv(1024)) # b'$4\r\ntest\r\n'
# Pipeline 模式:一次性发送多个命令
import time
start = time.time()
for _ in range(10000):
sock.send(redis_cmd("PING"))
# 一次读取所有响应
responses = sock.recv(1024 * 1024)
print(f"10000 个 PING Pipeline 耗时: {time.time() - start:.3f}s")
# 对比:逐个等待响应的话需要 ~10s+五、面试视角
| 追问 | 答案要点 |
|---|---|
| RESP 五种类型是什么? | + 简单字符串 / - 错误 / : 整数 / $ 批量字符串 / * 数组。每个前缀后跟 CRLF |
| Redis 为什么不用 HTTP? | HTTP 头开销大(100-200字节)、解析复杂。RESP 专为 Redis 操作定制,解析性能高、实现简单 |
| Pipeline 的作用和风险? | 批量发送减少 RTT;风险是批量过大导致输出缓冲区膨胀、一个慢命令阻塞所有后续请求 |
| $ -1 和 * -1 代表什么? | null 批量字符串和 null 数组:表示"不存在"而不是"空"——如 GET 不存在的 key 返回 $-1 |
| RESP 是二进制安全的吗? | 是。用长度前缀而非分隔符来界定数据,因此可以传输包含 \r\n 的二进制数据 |
| 内联命令和 RESP 的区别? | 内联用空格分割参数(类似 shell),只能用于简单调试不支持二进制数据;RESP 是正式协议 |
📚 相关链接
- **单线程与I/O模型** — 命令读取后的执行流程
- **SDS 数据结构** — RESP 协议读取的字符串存储为什么结构
- **Pipeline 深度分析** — Pipeline 原理和最佳实践
- ← 返回 **Redis基础与架构索引**