1. RESP 的设计哲学
RESP 是 Redis 自己设计的文本协议,有几个关键设计目标:
- 简单 :人能直接看懂,用
\r\n作为分隔符。 - 解析快:每种类型第一个字节就决定了类型,解析时无需回溯。
- 二进制安全 :可以传输任意字节(包括
\r\n本身),通过长度前缀保证。 - 支持多路复用(Redis 6.0+ 才支持):但早期设计只支持单请求-单回复,所以解析器也是简单的顺序解析。
2. 五种核心数据类型的编码规则
| 类型 | 第一个字节 | 示例 | 说明 |
|---|---|---|---|
| 简单字符串 | + |
+OK\r\n |
状态信息,不能包含 \r\n |
| 错误 | - |
-ERR unknown command\r\n |
错误信息,格式类似简单字符串 |
| 整数 | : |
:1000\r\n |
有符号整数,如 INCR 返回值 |
| 批量字符串 | $ |
$6\r\nfoobar\r\n |
二进制安全:$ 后跟字节长度,再跟数据和 \r\n |
| 数组 | * |
*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n |
嵌套结构,可包含任何类型 |
特殊值:
- 空批量字符串:
$-1\r\n→ 对应 Python 的None - 空数组:
*-1\r\n→ 也代表None
3. 解析器的核心原理
解析器的本质是一个 有限状态机(FSM),它逐字节读取数据,根据当前状态决定下一步动作。
3.1 状态定义(简化版)
- 状态 0:等待第一个字节,确定类型
- 状态 1 :正在读取
\r\n分隔的整数(长度或数组元素个数) - 状态 2:正在读取批量字符串的实际内容
- 状态 3:遇到数组,准备递归解析数组内的元素
3.2 解析流程(以批量字符串为例)
假设输入字节流:b"$5\r\nhello\r\n"
- 读第一个字节
$→ 进入批量字符串模式。 - 继续读取直到遇到
\r\n,中间部分"5"解析为整数length = 5。 - 跳过
\r\n(2 字节)。 - 从当前位置读取
length个字节("hello")。 - 然后跳过结尾的
\r\n(2 字节)。 - 将读到的
b"hello"按需解码(例如 UTF-8)返回。
关键点 :解析器必须 严格按长度读取 ,不能依赖 \r\n 来查找结尾(因为数据本身可能包含 \r\n)。这正是二进制安全的实现方式。
3.3 数组解析(嵌套结构)
数组的解析是递归的:
*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n
- 读到
*,count = 2。 - 递归解析第一个元素:
$3\r\nfoo\r\n→ 得到"foo"。 - 递归解析第二个元素:
$3\r\nbar\r\n→ 得到"bar"。 - 返回
["foo", "bar"]。
3.4 流式解析(TCP 分片问题)
网络数据可能分多个 TCP 包到达,例如:
第一个包:b"*2\r\n$3\r\nfo"
第二个包:b"o\r\n$3\r\nbar\r\n"
解析器不能假设一次性收到完整数据。所以解析器会维护一个 缓冲区 ,每次 feed 新数据时,尝试从缓冲区中提取一个完整的 RESP 回复。如果数据不足,就暂停并等待更多数据。
实现技巧:解析器在读取长度字段后,知道还需要多少字节,如果缓冲区不够,就记录当前状态并返回"需要更多数据"。
4. 手动实现一个最小但完整的 RESP 解析器(Python)
python
class RespParser:
def __init__(self):
self.buffer = b''
def feed(self, data: bytes):
"""接收新数据,返回所有已解析的完整回复(列表)"""
self.buffer += data
replies = []
while True:
reply, consumed = self._parse_one(self.buffer)
if reply is None: # 数据不完整
break
replies.append(reply)
self.buffer = self.buffer[consumed:]
return replies
def _parse_one(self, data: bytes):
"""尝试解析一个完整的 RESP 数据类型,返回 (result, consumed_bytes) 或 (None, 0) 表示需要更多数据"""
if not data:
return None, 0
# 根据第一个字节分发
type_byte = data[0]
if type_byte == ord('+'):
# 简单字符串:找到 \r\n
end = data.find(b'\r\n', 1)
if end == -1:
return None, 0
result = data[1:end].decode()
return result, end + 2
elif type_byte == ord('-'):
end = data.find(b'\r\n', 1)
if end == -1:
return None, 0
result = Exception(data[1:end].decode()) # 当作异常抛出
return result, end + 2
elif type_byte == ord(':'):
end = data.find(b'\r\n', 1)
if end == -1:
return None, 0
result = int(data[1:end])
return result, end + 2
elif type_byte == ord('$'):
# 批量字符串
end = data.find(b'\r\n', 1)
if end == -1:
return None, 0
length = int(data[1:end])
if length == -1:
return None, end + 2 # NULL 值
payload_start = end + 2
payload_end = payload_start + length
if len(data) < payload_end + 2:
return None, 0 # 数据不足
# 检查结尾 \r\n
if data[payload_end:payload_end+2] != b'\r\n':
raise ValueError("Invalid RESP format")
result = data[payload_start:payload_end] # 返回 bytes
return result, payload_end + 2
elif type_byte == ord('*'):
end = data.find(b'\r\n', 1)
if end == -1:
return None, 0
count = int(data[1:end])
if count == -1:
return None, end + 2
# 递归解析 count 个元素
consumed = end + 2
result = []
for _ in range(count):
if consumed >= len(data):
return None, 0
elem, n = self._parse_one(data[consumed:])
if elem is None and n == 0: # 数据不完整
return None, 0
result.append(elem)
consumed += n
return result, consumed
else:
raise ValueError(f"Unknown RESP type byte: {type_byte}")
说明:
_parse_one返回(结果, 消费字节数),当数据不足时返回(None, 0)。- 批量字符串返回
bytes,调用者可按需解码。 - 错误类型这里简单转为
Exception对象。
5. 反向过程:编码(生成 RESP 命令)
客户端向 Redis 发送命令也需要 RESP 格式。例如 SET key value 编码为:
*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n
实现一个简单的编码函数:
python
def encode_resp(obj):
"""将 Python 对象编码为 RESP 字节串"""
if obj is None:
return b'$-1\r\n'
if isinstance(obj, int):
return f':{obj}\r\n'.encode()
if isinstance(obj, str):
data = obj.encode()
return f'${len(data)}\r\n'.encode() + data + b'\r\n'
if isinstance(obj, bytes):
return f'${len(obj)}\r\n'.encode() + obj + b'\r\n'
if isinstance(obj, (list, tuple)):
parts = [f'*{len(obj)}\r\n'.encode()]
for item in obj:
parts.append(encode_resp(item))
return b''.join(parts)
if isinstance(obj, Exception):
return f'-{obj}\r\n'.encode()
raise TypeError(f"Unsupported type: {type(obj)}")
6. 性能优化原理
生产级解析器(如 hiredis)会做很多优化:
- 零拷贝 :对于大的批量字符串,不复制数据,直接返回内存视图(
memoryview)。 - 预分配缓冲区:避免频繁的内存分配。
- 使用查表法解析整数 :比
int()快。 - 避免递归:用迭代栈处理嵌套数组。
但这些都是在理解基本原理之上的工程优化。
7. 总结:核心原理一句话
RESP 解析器 = 前缀驱动的状态机 + 长度前缀保证二进制安全 + 递归处理嵌套数组