强曰为道

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

03 - RESP3 新类型详解

RESP3 新类型详解

3.1 为什么需要 RESP3

RESP2 虽然简洁高效,但存在几个设计缺陷,在大规模应用中逐渐暴露:

RESP2 的痛点

问题具体表现影响
类型信息丢失HGETALL 返回扁平数组,客户端无法区分 Map 和 List客户端需要"硬编码"命令返回类型
NULL 表示不统一只有 $-1\r\n*-1\r\n 两种 NULL无法区分"空集合"和"NULL"
缺少布尔类型SISMEMBER 返回 0/1,需客户端转换语义不清晰
缺少浮点数ZSCORE 返回字符串 “3.14”客户端需要自行解析
无属性机制服务器无法附带调试/统计信息性能分析困难
无主动推送服务器无法主动发送消息(除了 Pub/Sub)扩展受限

RESP3 的设计目标

antirez 在 2019 年提出 RESP3 规范,核心目标:

  1. 类型自描述:响应本身携带足够的类型信息,客户端无需"猜"
  2. 语义精确:每种数据类型有独立的表示
  3. 向后兼容:RESP3 客户端可以与 RESP2 服务器通信(降级)
  4. 可扩展:通过 Attribute 和 Push 类型支持未来扩展

3.2 版本握手

客户端需要通过 HELLO 命令切换到 RESP3:

→ HELLO 3
← %7
← $6
← server
← $5
← redis
← $7
← version
← $5
← 7.2.4
← $5
← proto
← :3
← $2
← id
← :1
← $4
← mode
← $10
← standalone
← $4
← role
← $6
← master
← $7
← modules
← *0

HELLO 响应是一个 Map(% 前缀),包含服务器信息和协商后的协议版本。

协议降级

如果服务器不支持 RESP3(Redis < 6.0),HELLO 命令会报错,客户端应回退到 RESP2:

def connect_with_fallback(host, port, password=None):
    """连接 Redis,自动协商协议版本"""
    sock = socket.create_connection((host, port))

    # 先尝试 RESP3
    send_command(sock, "HELLO", "3")
    resp = read_response(sock)

    if isinstance(resp, Exception):
        # 服务器不支持 RESP3,回退到 RESP2
        send_command(sock, "HELLO", "2")
        resp = read_response(sock)
        protocol_version = 2
    else:
        protocol_version = 3

    if password:
        send_command(sock, "AUTH", password)
        read_response(sock)

    return sock, protocol_version

3.3 Null(空值)

编码规则

_\r\n

RESP3 用统一的 _ 前缀表示 NULL,解决了 RESP2 中 $-1*-1 的不一致问题。

对比 RESP2

场景RESP2RESP3
GET 不存在的 key$-1\r\n_\r\n
不存在的 Hash 字段$-1\r\n_\r\n
NULL 数组元素*-1\r\n_\r\n

注意事项

⚠️ 空字符串不是 NULL RESP3 的 NULL(_\r\n)和空字符串($0\r\n\r\n)语义不同。客户端必须严格区分。


3.4 Boolean(布尔)

编码规则

#t\r\n    → true
#f\r\n    → false

使用场景

→ SISMEMBER myset element
← #t       # RESP3: true(元素存在)

→ EXISTS mykey
← :1       # 注意:EXISTS 仍然返回整数,保持向后兼容

Redis 中返回布尔语义的命令在 RESP3 模式下可能返回 Boolean 类型,但不是所有命令都会自动切换。具体行为取决于 Redis 版本和命令实现。

客户端处理

def handle_boolean(value: str) -> bool:
    if value == "t":
        return True
    elif value == "f":
        return False
    else:
        raise ValueError(f"Invalid boolean: {value}")

3.5 Double(双精度浮点数)

编码规则

,<浮点数>\r\n

使用场景

→ ZSCORE myset member
← ,3.14159

→ INCRBYFLOAT mykey 1.5
← ,2.5

特殊值

,inf\r\n      → 正无穷
,-inf\r\n     → 负无穷
,nan\r\n      → 非数字

对比 RESP2

命令RESP2RESP3
ZSCORE$5\r\n3.14159\r\n(字符串),3.14159\r\n(浮点数)
INCRBYFLOAT$3\r\n2.5\r\n(字符串),2.5\r\n(浮点数)

RESP2 中这些值是字符串,客户端需要自行用 atof() 转换。RESP3 直接提供浮点数表示。


3.6 Big Number(大数)

编码规则

(<大整数>\r\n

使用场景

当整数超出 64 位有符号范围时,RESP3 使用 Big Number:

→ INCRBY bigkey 999999999999999999999999999999
← (999999999999999999999999999999

客户端处理

from decimal import Decimal

def handle_big_number(line: bytes) -> Decimal:
    """解析 Big Number,使用 Decimal 避免精度丢失"""
    return Decimal(line[1:].decode("utf-8"))

注意:大多数应用中 Big Number 不常见。如果客户端不需要处理超大整数,可以将其作为字符串存储。


3.7 Verbatim String(原样字符串)

编码规则

=<总长度>\r\n<3字符编码>:<内容>\r\n

设计动机

在 RESP2 中,GET key 返回的 Bulk String 不携带编码信息。但某些场景(如 DEBUG OBJECT 或 Lua 脚本返回值)可能需要知道内容的格式。

Verbatim String 在内容前附加 3 个字符的编码提示:

=15\r\ntxt:Hello World\r\n
  • 15:总长度(包含 txt: 前缀 4 字节 + 内容 11 字节)
  • txt:编码类型(txt 表示文本,bin 表示二进制)
  • ::分隔符
  • Hello World:实际内容

编码类型

前缀含义示例
txt:文本内容调试信息、描述
bin:二进制内容序列化数据

客户端处理

def handle_verbatim_string(data: bytes) -> tuple[str, str]:
    """
    解析 Verbatim String
    返回 (encoding, content)
    """
    # data 不包含 = 和长度前缀,如 b"txt:Hello World"
    encoding = data[:3].decode()
    content = data[4:]  # 跳过 "txt:"
    return encoding, content

3.8 Map(映射)

编码规则

%<键值对数量>\r\n
<键 1><值 1>
<键 2><值 2>
...

设计动机

RESP2 中 HGETALL 返回扁平数组 ["field1", "value1", "field2", "value2"],客户端必须知道这是 Map 才能正确解析。RESP3 的 Map 类型让响应自描述:

# HGETALL myhash 的 RESP3 响应
%2
$6
field1
$6
value1
$6
field2
$6
value2

与 RESP2 对比

# RESP2: HGETALL myhash
*4
$6
field1
$6
value1
$6
field2
$6
value2

# RESP3: HGETALL myhash
%2
$6
field1
$6
value1
$6
field2
$6
value2

唯一区别是 *4 变成了 %2(元素数量从 4 变为键值对数量 2)。客户端可以直接识别这是 Map,无需依赖命令语义。

嵌套 Map

Map 的键和值可以是任意 RESP3 类型:

%2
$7
user:1
%2
$4
name
$5
Alice
$3
age
$2
30
$7
user:2
%2
$4
name
$3
Bob
$3
age
$2
25

结构化表示:

{
  "user:1": {"name": "Alice", "age": 30},
  "user:2": {"name": "Bob", "age": 25}
}

3.9 Set(集合)

编码规则

~<元素数量>\r\n
<元素 1>
<元素 2>
...

使用场景

→ SMEMBERS myset
← ~3
← $5
← hello
← $5
← world
← $3
← foo

与 Array 的区别

特性Array (*)Set (~)
有序性有序无序(语义上)
重复元素允许不允许(语义上)
客户端映射列表/数组集合/哈希集

注意:从传输格式看,Set 和 Array 的编码几乎相同,区别主要在语义层面。客户端应根据类型前缀映射到语言中的集合类型(如 Python 的 set)。


3.10 Attribute(属性)

编码规则

|<属性数量>\r\n
<键 1><值 1>
<键 2><值 2>
...
<实际响应>

设计动机

Attribute 允许服务器在返回结果的同时附带元数据,且不影响结果本身。客户端可以选择读取或忽略属性。

使用场景

→ DEBUG SET-ACTIVE-EXPIRE 1
← |1
← $7
← elapsed
← ,0.000123
← +OK

这个响应包含:

  • 属性:elapsed = 0.000123(命令执行耗时)
  • 实际结果:+OK

属性消费规则

  1. 客户端在解析响应时,如果遇到 | 前缀,应先读取所有属性键值对
  2. 属性之后紧跟的才是实际响应
  3. 如果客户端不支持 Attribute,应忽略整个属性块
def parse_response_with_attributes(reader):
    """解析可能带属性的响应"""
    # 预读一个字节判断类型
    first_byte = reader.peek_byte()

    if first_byte == ord("|"):
        # 这是属性,读取并存储
        attrs = reader.parse_map()
        # 继续读取实际响应
        result = reader.parse()
        return result, attrs
    else:
        return reader.parse(), None

3.11 Push(推送)

编码规则

><元素数量>\r\n
<元素 1>
<元素 2>
...

设计动机

RESP2 中,服务器只能在客户端发送命令后才能返回响应。Pub/Sub 消息是唯一的"例外",但实现方式比较 hack——客户端需要在读取响应时特殊处理 Pub/Sub 消息。

Push 类型是 RESP3 中正式引入的"服务器主动推送"机制:

Push 事件触发条件
messagePub/Sub 收到消息
subscribe订阅成功确认
invalidate缓存失效通知(客户端缓存)
redirection集群重定向通知
tracking-redir-broken追踪重定向失败

客户端缓存的 Push 示例

# 客户端启用缓存追踪
→ CLIENT TRACKING ON
← +OK

# 读取数据
→ GET mykey
← $5
← hello

# 另一个客户端修改了 mykey
# 服务器推送失效通知
← |1
← $7
← caching
← #t
← *1
← $5
← mykey

客户端收到 invalidate Push 后,应清除本地缓存中对应的 key。

Push 与 Pub/Sub 的关系

在 RESP3 中,Pub/Sub 消息也通过 Push 类型传输:

→ SUBSCRIBE channel
← >3           ← Push 类型
← $9
← subscribe
← $7
← channel
← :1           ← 当前订阅数

# 收到消息时
← >3
← $7
← message
← $7
← channel
← $5
← hello

3.12 完整类型速查表

前缀类型RESP2RESP3说明
+Simple String不变
-Error不变
:Integer不变
$Bulk String不变
*Array不变
_Null新增
#Boolean新增
,Double新增
(Big Number新增
=Verbatim String新增
%Map新增
~Set新增
|Attribute新增
>Push新增

3.13 RESP3 解析器实现

class RESP3Parser:
    """支持 RESP2 和 RESP3 的混合解析器"""

    def __init__(self, sock):
        self.sock = sock
        self.buffer = b""

    def _read_line(self) -> bytes:
        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 _read_bytes(self, n: int) -> bytes:
        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 parse(self):
        line = self._read_line()
        prefix = chr(line[0])

        # RESP2 类型
        if prefix == "+":
            return line[1:].decode("utf-8")
        elif prefix == "-":
            raise RedisError(line[1:].decode("utf-8"))
        elif prefix == ":":
            return int(line[1:])
        elif prefix == "$":
            length = int(line[1:])
            if length == -1:
                return None
            data = self._read_bytes(length + 2)
            return data[:length]
        elif prefix == "*":
            count = int(line[1:])
            if count == -1:
                return None
            return [self.parse() for _ in range(count)]

        # RESP3 新类型
        elif prefix == "_":
            return None
        elif prefix == "#":
            return line[1:] == b"t"
        elif prefix == ",":
            val = line[1:].decode()
            if val == "inf": return float("inf")
            if val == "-inf": return float("-inf")
            if val == "nan": return float("nan")
            return float(val)
        elif prefix == "(":
            return int(line[1:])  # 或使用 Decimal
        elif prefix == "=":
            length = int(line[1:])
            data = self._read_bytes(length + 2)[:length]
            encoding = data[:3].decode()
            content = data[4:]
            return {"encoding": encoding, "content": content}
        elif prefix == "%":
            count = int(line[1:])
            result = {}
            for _ in range(count):
                key = self.parse()
                value = self.parse()
                result[key] = value
            return result
        elif prefix == "~":
            count = int(line[1:])
            return {self.parse() for _ in range(count)}
        elif prefix == "|":
            count = int(line[1:])
            attrs = {}
            for _ in range(count):
                key = self.parse()
                value = self.parse()
                attrs[key] = value
            result = self.parse()
            return {"_attrs": attrs, "_value": result}
        elif prefix == ">":
            count = int(line[1:])
            return {"_type": "push", "data": [self.parse() for _ in range(count)]}
        else:
            raise ValueError(f"Unknown RESP type prefix: {prefix}")

3.14 注意事项

⚠️ 并非所有命令都会返回 RESP3 类型 切换到 RESP3 协议后,大多数命令的行为不变。只有部分命令(如 HGETALLLMPOP 等)会利用新类型。具体行为取决于 Redis 版本。

⚠️ 客户端兼容性 并非所有客户端库都支持 RESP3。在选择客户端时,需要确认是否支持 HELLO 3 握手和新类型的解析。

⚠️ 代理兼容性 如果 Redis 前面有代理(如 Codis、Twemproxy),代理可能不支持 RESP3 的新类型。升级前需要验证代理的兼容性。


3.15 扩展阅读

资源说明
RESP3 规范antirez 的原始设计文档
Redis HELLO 命令协议版本切换
客户端缓存Push 类型的核心应用场景
Redis 6.0 Release NotesRESP3 首次发布

上一章:RESP2 格式详解 | 下一章:命令格式与发送