01 - RESP 协议概述
RESP 协议概述
1.1 什么是 RESP
RESP(REdis Serialization Protocol)是 Redis 客户端与服务器之间的通信协议。它由 Salvatore Sanfilippo(antirez)在 2009 年设计,目标是:
- 实现简单:解析器可以用几十行代码完成
- 人类可读:可以用 telnet 直接调试
- 高效传输:二进制安全,支持大体积数据
协议定位
┌──────────────┐ RESP ┌──────────────┐
│ Client │ ─────────────────────→ │ Redis │
│ (Application)│ ←───────────────────── │ Server │
└──────────────┘ TCP Stream └──────────────┘
RESP 运行在 TCP 之上,默认端口 6379。每个 Redis 命令对应一次请求-响应交互:
- 客户端将命令编码为 RESP 数组发送
- 服务器解析命令并执行
- 服务器将结果编码为 RESP 类型返回
核心特征
| 特征 | 说明 |
|---|---|
| 传输层 | TCP(也支持 Unix Domain Socket) |
| 编码方式 | 文本前缀 + CRLF 分隔,二进制安全 |
| 类型系统 | RESP2 有 5 种,RESP3 扩展到约 14 种 |
| 方向性 | 请求和响应使用相同的类型系统 |
| 状态 | 无状态协议(单个连接内保持顺序) |
1.2 版本演进
RESP2(Redis 1.2+,正式成为默认协议)
RESP2 是 Redis 长期使用的协议版本,定义了 5 种基本类型:
| 类型 | 前缀 | 用途 |
|---|---|---|
| Simple String | + | OK 等简短文本响应 |
| Error | - | 错误信息 |
| Integer | : | 整数计数 |
| Bulk String | $ | 二进制安全的字符串(含 NULL) |
| Array | * | 命令和复合响应的载体 |
RESP2 的设计极度精简——整个协议规范不到一页纸。这种简洁性是 Redis 生态繁荣的重要原因:几乎所有语言都有轻量级的 Redis 客户端实现。
RESP3(Redis 6.0+,2020 年 4 月)
RESP3 由 antirez 重新设计,目标是解决 RESP2 的几个痛点:
RESP2 的问题:
- 类型信息不足:
HGETALL返回扁平数组,客户端必须"知道"这是 Map - NULL 表示不一致:空字符串和 NULL 都用
$-1\r\n表示 - 缺少布尔/双精度:需要客户端自行转换
- 属性推送困难:服务器无法附带元数据(如耗时统计)
RESP3 的改进:
| 新类型 | 前缀 | 解决的问题 |
|---|---|---|
| Null | _ | 统一的 NULL 表示 |
| Boolean | # | 原生布尔值 |
| Double | , | 浮点数 |
| Big Number | ( | 超出 64 位的整数 |
| Verbatim String | = | 带编码提示的字符串 |
| Map | % | 键值对集合 |
| Set | ~ | 无序集合 |
| Attribute | | | 元数据(不影响结果) |
| Push | > | 服务器主动推送 |
版本选择
# 查看当前协议版本
redis-cli INFO clients
# connected_clients:1
# ...
# resp:2
# 使用 RESP3(需要 Redis 6.0+)
redis-cli --resp3
# 或在连接时发送 HELLO 3
客户端切换到 RESP3 的协议握手:
→ HELLO 3
← %7
← $6
← server
← $5
← redis
← $7
← version
← $5
← 7.2.4
← ...
1.3 设计哲学
哲学一:面向文本优先(Text-Oriented)
RESP 的类型标识使用 ASCII 字符(+, -, :, $, *),整数和长度用十进制数字表示。这使得人类可以直接用 telnet 与 Redis 交互:
$ telnet 127.0.0.1 6379
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
*3\r\n
$3\r\n
SET\r\n
$5\r\n
hello\r\n
$5\r\n
world\r\n
+OK
这种"人类可调试"的特性极大地降低了开发和排错成本。
哲学二:二进制安全(Binary-Safe)
虽然类型标识是文本,但 Bulk String 的内容可以包含任意字节。长度前缀($<length>\r\n)确保解析器不会被内容中的 \r\n 截断:
$11\r\n
hello\r\nworld\r\n
这个 Bulk String 包含 hello\r\nworld(11 字节),解析器通过长度字段精确读取,不会误判。
哲学三:递归组合(Recursive Composition)
RESP 的核心思想是:数组可以嵌套。一个命令是"字符串的数组",一个复杂响应是"数组的数组"。这种递归结构足够表达几乎所有数据类型:
# 命令: SET key value
*3 ← 外层数组(3 个元素)
$3 ← 元素 1:Bulk String(3 字节)
SET
$3 ← 元素 2:Bulk String
key
$5 ← 元素 3:Bulk String
value
哲学四:流式解析(Streaming-Friendly)
RESP 的设计天然支持流式处理:每个数据帧的前几个字节就告知了类型和长度,解析器无需等待完整数据即可开始处理。这对于 Pipeline 和大体积响应尤为重要。
1.4 与 Memcached 协议对比
Memcached 也是广泛使用的键值存储,其文本协议与 RESP 有显著差异:
协议格式对比
| 维度 | RESP (Redis) | Memcached Text Protocol |
|---|---|---|
| 分隔符 | CRLF (\r\n) | CRLF (\r\n) |
| 命令格式 | 结构化数组 | 空格分隔的文本行 |
| 响应类型 | 5+ 种带前缀类型 | 关键字开头(VALUE, STORED, END) |
| 二进制安全 | ✅ Bulk String 长度前缀 | ❌ 文本协议不保证 |
| 多行响应 | 用长度前缀声明 | 用 END 标记结束 |
| 管道支持 | 原生支持 | 部分版本支持 |
| 协议升级 | RESP2 → RESP3 | 文本 → 二进制协议 |
命令对比示例
存储操作:
# Memcached
set key 0 3600 5\r\n
hello\r\n
STORED\r\n
# Redis RESP
*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nhello\r\n
+OK\r\n
读取操作:
# Memcached
get key\r\n
VALUE key 0 5\r\n
hello\r\n
END\r\n
# Redis RESP
*2\r\n$3\r\nGET\r\n$3\r\nkey\r\n
$5\r\n
hello\r\n
二进制安全问题
Memcached 文本协议的一个重大缺陷是不支持二进制安全。如果你的值包含 \r\n,文本协议会出错。Memcached 的二进制协议(Binary Protocol)解决了这个问题,但增加了复杂性。
RESP 通过 Bulk String 的长度前缀优雅地解决了这个问题:解析器先读取长度,再精确读取对应字节数,内容中的任何字节都不会导致误解析。
性能对比
| 指标 | RESP | Memcached Text |
|---|---|---|
| 解析复杂度 | O(n),按长度跳过 | O(n),需扫描分隔符 |
| Pipeline 效率 | 原生支持,一次发送多命令 | 需要特殊处理 |
| 网络开销 | 较小(类型前缀很短) | 类似 |
| 扩展性 | 丰富数据结构 | 纯键值对 |
1.5 快速上手:用 telnet 理解协议
最直观的学习方式是用 telnet 手动发送 RESP 数据:
# 终端 1:启动 Redis
redis-server
# 终端 2:用 telnet 连接
telnet 127.0.0.1 6379
发送 PING
*1\r\n
$4\r\n
PING\r\n
服务器响应:
+PONG\r\n
### 发送 SET/GET
*3\r\n $3\r\n SET\r\n $7\r\n mykey\r\n $5\r\n hello\r\n
响应:`+OK\r\n`
*2\r\n $3\r\n GET\r\n $7\r\n mykey\r\n
响应:`$5\r\nhello\r\n`
### 观察错误
*2\r\n $3\r\n GET\r\n $3\r\n nosuch\r\n
响应:`$-1\r\n`(NULL Bulk String,表示 key 不存在)
---
## 1.6 工程中的协议层
在实际的 Redis 客户端库中,协议处理通常分为两层:
┌─────────────────────────────────┐ │ Application Layer │ │ redis.get(“key”) │ ├─────────────────────────────────┤ │ Command Layer │ │ [“GET”, “key”] │ ├─────────────────────────────────┤ │ Protocol Layer │ │ *2\r\n$3\r\nGET\r\n$3\r\nkey\r\n │ ├─────────────────────────────────┤ │ Transport Layer │ │ TCP / Unix Socket │ └─────────────────────────────────┘
理解这个分层有助于定位问题:
- 命令格式错误 → 检查 Command Layer
- 数据解析失败 → 检查 Protocol Layer
- 连接超时 → 检查 Transport Layer
---
## 1.7 注意事项
> **⚠️ CRLF 是底线**
> RESP 协议的行终结符严格使用 `\r\n`(0x0D 0x0A),不是 `\n`。手动构造协议时最容易犯的错误就是只发送 `\n`。
> **⚠️ 长度是字节数,不是字符数**
> `$5\r\nhello\r\n` 中的 5 是字节长度。对于中文等多字节字符,必须按 UTF-8 编码后的字节数计算。
> **⚠️ 协议版本兼容**
> RESP3 向下兼容 RESP2,但服务器必须在 HELLO 握手后才切换。未发送 HELLO 的连接默认使用 RESP2。
---
## 1.8 业务场景
### 场景一:微服务间的轻量消息
由于 RESP 是人类可读的文本协议,在调试阶段可以直接用 tcpdump 抓包查看 Redis 通信内容,比 Protobuf 等二进制协议更友好。
### 场景二:协议网关
如果你需要在 Redis 和其他存储(如 Memcached)之间做代理,理解两套协议的差异是实现转换层的前提。
### 场景三:嵌入式脚本
Redis 的 Lua 脚本引擎通过 RESP 协议与外部通信。理解协议可以让你写出更高效的脚本(减少数据转换开销)。
---
## 1.9 扩展阅读
| 资源 | 链接 |
|------|------|
| RESP2 规范 | [Redis Protocol Specification](https://redis.io/docs/latest/develop/reference/protocol-spec/) |
| RESP3 规范 | [RESP3 Specification](http://antirez.com/news/134) |
| antirez 博客:RESP3 动机 | [RESP3, a new Redis protocol](http://antirez.com/news/134) |
| Redis 源码:协议解析 | `networking.c` → `processInputBuffer()` |
| Memcached 协议 | [Memcached Protocol Wiki](https://github.com/memcached/memcached/wiki/Protocols) |
---
下一章:[RESP2 格式详解](../02-resp2/)