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

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 位有符号整数(-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 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. 负号处理: 后面可能紧跟 -,如 :-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\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 服务端协议解析核心

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