3. 通信协议基础:Redis RESP 协议详解
Redis 客户端与服务器之间的通信并非基于 HTTP 或 JSON,而是采用一种名为 REdis Serialization Protocol(RESP) 的专有协议。RESP 是一种二进制安全的文本协议,设计核心是“简单、快速、易于解析”。在缓存鉴权拦截场景中,理解 RESP 协议是调试通信异常、分析拦截器行为的基础。
3.1 协议基础与数据类型总览
RESP 协议定义了五种核心数据类型,每种类型通过特定的首字节标识符来区分。服务器返回的响应总是以这五种类型之一的形式呈现,客户端根据首字节决定后续的解析策略。
| 类型 | 首字节 | 格式示例 | 典型用途 |
|---|---|---|---|
| 简单字符串 | + |
+OK\r\n |
命令成功响应(如 SET、PING) |
| 错误 | - |
-ERR unknown command\r\n |
命令执行失败或鉴权拒绝 |
| 整数 | : |
:1\r\n |
返回计数(如 EXISTS、INCR) |
| 批量字符串 | $ |
$5\r\nhello\r\n |
返回单个数据值(如 GET 返回值) |
| 数组 | * |
*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n |
返回多个值(如 LRANGE、MGET) |
-NOAUTH Authentication required.\r\n。客户端解析到首字节为 - 时,应立即抛出异常,停止后续命令执行。
3.2 五种数据类型详解
3.2.1 简单字符串(Simple String)
以 + 开头,后跟字符串内容,以 \r\n 结尾。它用于表示非二进制安全的、简短的状态回复。客户端收到后,直接提取 + 与 \r\n 之间的内容作为结果。
# 客户端发送 PING
PING\r\n
# 服务器响应
+OK\r\n
3.2.2 错误(Error)
以 - 开头,格式与简单字符串类似,但语义表示异常。错误消息的第一个单词通常是错误类型(如 ERR、WRONGTYPE、NOAUTH)。在鉴权拦截场景中,这是最常见的拒绝响应类型。
# 未认证时执行命令
GET mykey\r\n
# 服务器响应
-NOAUTH Authentication required.\r\n
# 命令参数错误
SET key\r\n
# 服务器响应
-ERR wrong number of arguments for 'set' command\r\n
3.2.3 整数(Integer)
以 : 开头,后跟十进制数字,以 \r\n 结尾。用于返回计数或布尔值(0 表示 false,1 表示 true)。
# 客户端
EXISTS mykey\r\n
# 服务器响应(键存在)
:1\r\n
# 客户端
INCR counter\r\n
# 服务器响应
:10\r\n
3.2.4 批量字符串(Bulk String)
这是 Redis 中最常用的数据类型,用于传输二进制安全的字符串(包括空字符串和 null)。格式为:$<长度>\r\n<数据>\r\n。
- 非空字符串:
$5\r\nhello\r\n(长度为 5,数据为 "hello") - 空字符串:
$0\r\n\r\n(长度为 0,数据为空) - Null 值:
$-1\r\n(长度为 -1,表示键不存在)
# 客户端
GET name\r\n
# 服务器响应(键存在,值为 "Alice")
$5\r\nAlice\r\n
# 服务器响应(键不存在)
$-1\r\n
$-1 与空字符串 $0\r\n\r\n 在语义上完全不同。在鉴权拦截器中,如果错误地将 $-1 当作空字符串处理,可能导致缓存穿透或空值缓存逻辑错误。
3.2.5 数组(Array)
以 * 开头,后跟数组元素个数,以 \r\n 结尾。数组元素可以是任意 RESP 数据类型(包括嵌套数组)。格式为:*<元素个数>\r\n<元素1><元素2>...<元素N>。
# 客户端
LRANGE mylist 0 2\r\n
# 服务器响应(返回三个元素)
*3\r\n
$5\r\nhello\r\n
$5\r\nworld\r\n
$3\r\nfoo\r\n
# 空数组
*0\r\n
# Null 数组(命令不存在或错误)
*-1\r\n
3.3 RESP 协议通信流程与鉴权拦截
下图展示了在鉴权拦截场景下,客户端与 Redis 服务器之间完整的 RESP 协议通信流程,包括正常执行与鉴权拒绝两种路径。
3.4 调试技巧:使用 redis-cli 观察原始 RESP
在调试鉴权拦截问题时,直接观察原始 RESP 协议数据是最有效的手段。Redis 提供了多种方式查看协议细节。
3.4.1 使用 --raw 模式
默认情况下,redis-cli 会美化输出。使用 --raw 参数可以查看原始 RESP 格式(但不会显示首字节和 \r\n)。
# 普通模式
$ redis-cli
127.0.0.1:6379> GET mykey
"hello"
# Raw 模式(显示原始数据,不含协议首字节)
$ redis-cli --raw
127.0.0.1:6379> GET mykey
hello
3.4.2 使用 --pipe 模式与 nc 命令
通过管道模式配合 nc(netcat)可以手动构造并观察完整的 RESP 协议数据包。
# 手动发送 RESP 命令并观察原始响应
$ echo -e '*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n' | nc localhost 6379
+OK\r\n
# 发送未认证命令,观察鉴权拒绝响应
$ echo -e '*1\r\n$3\r\nGET\r\n$3\r\nkey\r\n' | nc localhost 6379
-NOAUTH Authentication required.\r\n
nc 手动构造 RESP 命令,可以绕过客户端库的封装,直接观察服务器返回的原始 RESP 数据。如果返回 -NOAUTH 或 -ERR 错误,说明拦截器正常工作;如果返回 +OK 或正常数据,说明拦截器未生效或配置错误。
3.5 协议对比:RESP vs HTTP/JSON
理解 RESP 与 HTTP/JSON 的差异,有助于在调试时快速定位问题根源。
| 对比维度 | RESP 协议 | HTTP/JSON |
|---|---|---|
| 传输格式 | 二进制安全文本协议 | 纯文本(JSON 为文本子集) |
| 解析复杂度 | 低(基于首字节和长度前缀) | 高(需要 JSON 解析器) |
| 性能 | 极高(零拷贝解析) | 中等(序列化/反序列化开销) |
| 错误表示 | 首字节 - + 错误类型 |
HTTP 状态码 + 错误体 |
| Null 表示 | $-1\r\n(批量字符串 null) |
null 关键字 |
| 批量操作 | 数组类型原生支持 | 需要自定义数组结构 |
3.6 常见 RESP 调试场景与解决方案
- 场景一:客户端收到
-NOAUTH Authentication required.- 原因: 连接未通过 AUTH 命令认证,或鉴权拦截器已触发。
- 解决: 检查连接池配置中的密码参数,或确认鉴权拦截器是否误判。
- 场景二:客户端收到
$-1\r\n但期望返回空字符串- 原因: 键不存在,客户端库可能返回 null 而非空字符串。
- 解决: 在鉴权拦截器中统一处理 null 与空字符串的缓存策略。
- 场景三:客户端收到
*-1\r\n(Null 数组)- 原因: 命令不存在或执行错误,服务器返回 Null 数组表示异常。
- 解决: 检查命令拼写,或确认鉴权拦截器是否返回了错误类型。
- 场景四:使用
nc调试时看到乱码- 原因: 手动构造的 RESP 命令缺少
\r\n或长度计算错误。 - 解决: 使用
printf或echo -e确保换行符正确,批量字符串长度必须与实际数据一致。
- 原因: 手动构造的 RESP 命令缺少
nc 或 telnet 手动发送 RESP 命令,观察原始响应。如果原始响应正确,问题出在客户端库;如果原始响应错误,问题出在服务器或拦截器。