Skip to content

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\n

2.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 的三条关键设计原则:

  1. 长度前缀 → 二进制安全
  2. 文本可读 → 调试友好
  3. 类型前缀 → 解析器轻量

四、实战验证

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
$-1

4.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
# 输出: +OK

4.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-200

4.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基础与架构索引**

Knowledge4J — Java 知识库