强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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'
BUSYKEYKey 已存在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 位有符号整数(-92233720368547758089223372036854775807

典型响应

: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 key1(存在)或 0(不存在)
DEL key [key ...]删除的 key 数量
DBSIZE当前数据库的 key 数量
INCR key递增后的值
TTL key剩余秒数(-1 无过期,-2 不存在)
LLEN key列表长度
SCARD key集合元素数量
HSET key field value1(新增)或 0(更新)
LPUSH key value [value ...]列表长度
SUBSCRIBE channel当前订阅数

解析要点

Integer 的解析相对简单,但需要注意:

  1. 负号处理: 后面可能紧跟 -,如 :-1\r\n
  2. 零值:0\r\n 是合法的
  3. 大整数:必须支持 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\nNULL(不是空字符串)
空字符串$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\nkey 不存在
空字符串$0\r\n\r\nkey 存在但值为空

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服务端协议解析核心

上一章:RESP 协议概述 | 下一章:RESP3 新类型