Redis 传输协议精讲 / 02 - RESP2 格式详解
RESP2 格式详解
2.1 类型总览
RESP2 定义了 5 种基础类型,每种类型用一个 ASCII 字符作为前缀标识:
| 类型 | 前缀 | 终结方式 | 典型用途 |
|---|---|---|---|
| Simple String | + | \r\n 结尾 | OK、PONG 等简短响应 |
| Error | - | \r\n 结尾 | 错误信息 |
| Integer | : | \r\n 结尾 | 计数、影响行数 |
| Bulk String | $ | 长度前缀 + \r\n | 字符串值、命令参数 |
| Array | * | 元素计数前缀 | 命令编码、列表响应 |
所有类型都以 \r\n(CRLF, 0x0D 0x0A)作为行终结符。这是协议的"原子单位"——一行数据。
2.2 Simple String(简单字符串)
编码规则
+<内容>\r\n
- 以
+开头 - 内容为不包含
\r或\n的文本 - 以
\r\n结束
典型响应
+OK\r\n
+PONG\r\n
+QUEUED\r\n
特点
| 优点 | 限制 |
|---|---|
| 解析最简单 | 不能包含 \r\n |
| 人类可读 | 不能表示二进制数据 |
| 开销最小 | 不能表示 NULL |
使用场景
Simple String 主要用于简短的状态响应。在 Redis 命令中,以下情况返回 Simple String:
SET key value → +OK
PING → +PONG
MULTI → +QUEUED
SUBSCRIBE channel → +subscribe
注意:虽然 Simple String 看起来像纯文本,但它是 RESP 类型——客户端必须按照
+...\\r\\n格式解析,不能简单地当作"一行文本"处理。
2.3 Error(错误)
编码规则
-<错误信息>\r\n
- 以
-开头 - 格式通常为
ERR <描述>或<错误码> <描述> - 以
\r\n结束
错误分类
Redis 错误有明确的分类前缀:
| 错误前缀 | 含义 | 示例 |
|---|---|---|
ERR | 通用错误 | ERR unknown command 'foo' |
WRONGTYPE | 类型不匹配 | WRONGTYPE Operation against a key holding the wrong kind of value |
ERR wrong number of arguments | 参数数量错误 | ERR wrong number of arguments for 'get' command |
NOAUTH | 未认证 | NOAUTH Authentication required. |
READONLY | 只读节点写入 | READONLY You can't write against a read only replica. |
NOSCRIPT | 脚本不存在 | NOSCRIPT No matching script. |
MOVED | 槽位重定向 | MOVED 1234 127.0.0.1:6380 |
ASK | 槽位迁移中 | ASK 1234 127.0.0.1:6380 |
CROSSSLOT | 跨槽位操作 | CROSSSLOT Keys in request don't hash to the same slot |
LOADING | 服务器加载中 | LOADING Redis is loading the dataset in memory |
OOM | 内存不足 | OOM command not allowed when used memory > 'maxmemory' |
BUSYKEY | Key 已存在 | BUSYKEY Target key name already exists. |
错误码规范(Redis 6.2+)
从 Redis 6.2 开始,错误信息遵循更规范的格式:
-<MODULE|ERR|LOADING|OOM|...> <错误码> <描述>
Python 示例:解析错误
import socket
def read_resp_error(sock: socket.socket) -> str:
"""从 socket 读取一个 RESP Error"""
buf = b""
while True:
byte = sock.recv(1)
if byte == b"\r":
sock.recv(1) # 跳过 \n
break
buf += byte
return buf.decode("utf-8")
# 连接 Redis 并触发错误
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("127.0.0.1", 6379))
# 发送一个不存在的命令
s.sendall(b"*1\r\n$7\r\nINVALID\r\n")
resp = s.recv(1024)
print(f"Raw response: {resp}")
# Raw response: b"-ERR unknown command 'INVALID'\r\n"
s.close()
2.4 Integer(整数)
编码规则
:<整数>\r\n
- 以
:开头 - 整数为十进制表示,可带正负号
- 以
\r\n结束 - 范围:64 位有符号整数(
-9223372036854775808到9223372036854775807)
典型响应
:0\r\n # 如 EXISTS key(key 不存在时)
:1\r\n # 如 EXISTS key(key 存在时)
:100\r\n # 如 DBSIZE(数据库有 100 个 key)
:-1\r\n # 如 TTL key(key 没有过期时间)
:42\r\n # 如 INCR counter 后的值
返回 Integer 的常见命令
| 命令 | 返回值含义 |
|---|---|
EXISTS key | 1(存在)或 0(不存在) |
DEL key [key ...] | 删除的 key 数量 |
DBSIZE | 当前数据库的 key 数量 |
INCR key | 递增后的值 |
TTL key | 剩余秒数(-1 无过期,-2 不存在) |
LLEN key | 列表长度 |
SCARD key | 集合元素数量 |
HSET key field value | 1(新增)或 0(更新) |
LPUSH key value [value ...] | 列表长度 |
SUBSCRIBE channel | 当前订阅数 |
解析要点
Integer 的解析相对简单,但需要注意:
- 负号处理:
:后面可能紧跟-,如:-1\r\n - 零值:
:0\r\n是合法的 - 大整数:必须支持 64 位,否则会溢出
// Go 语言解析 Integer 示例
func parseInteger(line []byte) (int64, error) {
// line 不包含 \r\n,格式如 ":123"
if len(line) < 2 || line[0] != ':' {
return 0, fmt.Errorf("invalid integer: %s", line)
}
return strconv.ParseInt(string(line[1:]), 10, 64)
}
2.5 Bulk String(批量字符串)
编码规则
Bulk String 是 RESP2 中最重要的类型,也是协议二进制安全的保证。
正常字符串:
$<字节长度>\r\n<内容>\r\n
NULL 值:
$-1\r\n
空字符串:
$0\r\n
\r\n
编码示例
# "hello"(5 字节)
$5\r\nhello\r\n
# 空字符串(0 字节)
$0\r\n\r\n
# 包含换行的二进制数据(11 字节)
$11\r\nhello\r\nworld\r\n
# NULL 值(key 不存在)
$-1\r\n
解析算法
1. 读取第一行(以 \r\n 结尾),得到 "$<length>"
2. 如果 length == -1,返回 NULL
3. 读取 length 个字节
4. 读取并丢弃后续的 \r\n
5. 返回读取到的字节
class RESPReader:
def __init__(self, data: bytes):
self.data = data
self.pos = 0
def read_line(self) -> bytes:
"""读取一行(到 \r\n 为止),不包含 \r\n"""
end = self.data.index(b"\r\n", self.pos)
line = self.data[self.pos:end]
self.pos = end + 2
return line
def read_bulk_string(self) -> bytes | None:
"""解析一个 Bulk String"""
line = self.read_line()
assert line.startswith(b"$"), f"Expected $, got {line}"
length = int(line[1:])
if length == -1:
return None # NULL
# 读取 length 个字节
value = self.data[self.pos:self.pos + length]
self.pos += length + 2 # +2 跳过 \r\n
return value
关键细节
| 场景 | 编码 | 含义 |
|---|---|---|
| key 不存在 | $-1\r\n | NULL(不是空字符串) |
| 空字符串 | $0\r\n\r\n | 长度为 0 的有效字符串 |
| 二进制数据 | $<len>\r\n<data>\r\n | 安全传输任意字节 |
| 中文(UTF-8) | $<字节数>\r\n<UTF-8字节>\r\n | 长度按字节计算 |
⚠️ 常见错误:长度计算
$5\r\n你好\r\n是错误的——“你好"在 UTF-8 中是 6 个字节(每个汉字 3 字节),正确编码是$6\r\n你好\r\n。
2.6 Array(数组)
编码规则
数组是 RESP2 的复合类型,用于编码命令和复杂响应。
正常数组:
*<元素数量>\r\n
<元素 1>
<元素 2>
...
NULL 数组:
*-1\r\n
空数组:
*0\r\n
命令编码示例
Redis 命令本质是一个 Bulk String 数组:
# SET key value
*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n
# GET key
*2\r\n$3\r\nGET\r\n$3\r\nkey\r\n
# MGET key1 key2 key3
*4\r\n$4\r\nMGET\r\n$4\r\nkey1\r\n$4\r\nkey2\r\n$4\r\nkey3\r\n
# 无参数命令
*1\r\n$4\r\nINFO\r\n
响应编码示例
# LRANGE list 0 -1 的响应(3 个元素)
*3\r\n$5\r\nhello\r\n$5\r\nworld\r\n$3\r\nfoo\r\n
# 空列表
*0\r\n
# key 不存在时的列表
*-1\r\n
嵌套数组
数组元素本身可以是数组,这在某些命令响应中出现:
# SCAN 响应格式
*2\r\n
$1\r\n
0\r\n
*3\r\n
$5\r\n
key:1\r\n
$5\r\n
key:2\r\n
$5\r\n
key:3\r\n
结构化表示:
[
"0", # cursor
[ # 数组元素
"key:1",
"key:2",
"key:3"
]
]
完整解析器实现
import socket
from typing import Any
class RESP2Parser:
def __init__(self, sock: socket.socket):
self.sock = sock
self.buffer = b""
def _read_bytes(self, n: int) -> bytes:
"""从 socket 读取 n 个字节"""
while len(self.buffer) < n:
chunk = self.sock.recv(4096)
if not chunk:
raise ConnectionError("Connection closed")
self.buffer += chunk
result = self.buffer[:n]
self.buffer = self.buffer[n:]
return result
def _read_line(self) -> bytes:
"""读取一行(到 \r\n 为止)"""
while b"\r\n" not in self.buffer:
chunk = self.sock.recv(4096)
if not chunk:
raise ConnectionError("Connection closed")
self.buffer += chunk
pos = self.buffer.index(b"\r\n")
line = self.buffer[:pos]
self.buffer = self.buffer[pos + 2:]
return line
def parse(self) -> Any:
"""解析一个 RESP2 值"""
line = self._read_line()
type_byte = line[0:1]
if type_byte == b"+":
# Simple String
return line[1:].decode("utf-8")
elif type_byte == b"-":
# Error
raise RedisError(line[1:].decode("utf-8"))
elif type_byte == b":":
# Integer
return int(line[1:])
elif type_byte == b"$":
# Bulk String
length = int(line[1:])
if length == -1:
return None
data = self._read_bytes(length + 2) # +2 for \r\n
return data[:length].decode("utf-8")
elif type_byte == b"*":
# Array
count = int(line[1:])
if count == -1:
return None
return [self.parse() for _ in range(count)]
else:
raise ValueError(f"Unknown type: {type_byte}")
class RedisError(Exception):
pass
2.7 类型判断速查表
在实际应用中,客户端需要根据前缀快速判断类型:
接收到的首字节 → 类型 → 处理方式
───────────────────────────────────────────────────
'+' → Simple String → 读取到 \r\n,返回字符串
'-' → Error → 读取到 \r\n,抛出异常
':' → Integer → 读取到 \r\n,解析为 int64
'$' → Bulk String → 读取长度,再读取对应字节
'*' → Array → 读取元素数,递归解析每个元素
2.8 边界情况与陷阱
陷阱一:空 Bulk String vs NULL
| 表示 | 编码 | 语义 |
|---|---|---|
| NULL | $-1\r\n | key 不存在 |
| 空字符串 | $0\r\n\r\n | key 存在但值为空 |
GET 一个不存在的 key 返回 $-1\r\n,而一个值为空字符串的 key 返回 $0\r\n\r\n。客户端必须区分这两种情况。
陷阱二:数组元素类型混合
RESP2 数组的元素可以是任意类型,包括不同类型的混合:
*3\r\n
$5\r\nhello\r\n
:42\r\n
$-1\r\n
这个数组包含:一个字符串、一个整数、一个 NULL。客户端解析器必须逐个元素判断类型。
陷阱三:大响应的内存问题
当 KEYS * 返回百万个 key 时,客户端会收到一个包含百万元素的数组。解析器应该考虑流式解析(逐个元素处理)而非一次性全部加载到内存。
陷阱四:协议同步
如果客户端发送了格式错误的数据(如长度字段与实际内容不符),Redis 会关闭连接。这是因为协议解析器会进入"未定义状态”,无法确定后续数据的边界。
2.9 Python 实战:手动构造 RESP2 命令
import socket
def encode_command(*args: str) -> bytes:
"""将命令编码为 RESP2 格式"""
parts = [f"*{len(args)}\r\n"]
for arg in args:
arg_bytes = arg.encode("utf-8")
parts.append(f"${len(arg_bytes)}\r\n")
parts.append(arg.decode("utf-8") if isinstance(arg, bytes) else arg)
parts.append("\r\n")
return "".join(parts).encode("utf-8")
def encode_command_binary(*args: bytes) -> bytes:
"""将命令编码为 RESP2 格式(二进制安全版本)"""
header = f"*{len(args)}\r\n".encode()
body = b""
for arg in args:
body += f"${len(arg)}\r\n".encode() + arg + b"\r\n"
return header + body
# 使用示例
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("127.0.0.1", 6379))
# SET
s.sendall(encode_command_binary(b"SET", b"greeting", b"hello world"))
print(s.recv(1024)) # b'+OK\r\n'
# GET
s.sendall(encode_command_binary(b"GET", b"greeting"))
print(s.recv(1024)) # b'$11\r\nhello world\r\n'
# INCR
s.sendall(encode_command_binary(b"INCR", b"counter"))
print(s.recv(1024)) # b':1\r\n'
s.close()
2.10 扩展阅读
| 资源 | 说明 |
|---|---|
| RESP2 协议规范 | Redis 官方文档 |
| redis-py 源码 | Python 客户端的协议实现 |
| jedis 源码 | Java 客户端的协议实现 |
| Redis 源码 networking.c | 服务端协议解析核心 |