【大白话说Java面试题 第184题】【07_Redis篇】第20题:Redis 底层使用的什么协议?
2026/7/23 23:09:47 网站建设 项目流程

📌PDF:大白话说Java面试题 — 07_Redis篇

第20题:Redis 底层使用的什么协议?

📚回答:

  • 核心考点: Redis 底层通信协议 RESP(Redis Serialization Protocol)是面试中看似简单实则深藏玄机的考点。大厂面试官不会只问"RESP 有哪些数据类型",而是深入考察RESP 的完整设计哲学(为什么不用 JSON/XML)、RESP2 与 RESP3 的演进差异(Redis 6.0+ 引入的 Push 类型、Big Number、Verbatim String 等)、协议解析的零拷贝优化Pipeline 和 Pub/Sub 在协议层面的实现差异,以及自定义 Redis 客户端时协议解析的性能陷阱。面试官真正想判断的是:你是否理解"简单协议设计"背后的工程权衡,以及能否在协议层面优化 Redis 通信性能。
1. RESP 协议的设计哲学与版本演进
  • 1.1 为什么 Redis 选择自定义协议而非 JSON/XML?Redis 作者 antirez 在设计 RESP 时遵循三个核心原则:

    设计原则RESP 实现JSON/XML 对比
    解析效率前缀标识符 + CRLF 分隔,状态机解析 O(1)JSON 需递归解析,XML 需 DOM/SAX
    内存友好流式解析,无需预读完整数据JSON 通常需完整加载到内存
    类型丰富原生支持二进制安全字符串、整数、数组JSON 只有字符串/数字/对象/数组
    实现简单任何语言 100 行代码即可实现客户端JSON 库通常数千行
    可读性文本协议,telnet 可直接调试同样文本可读

    关键决策:RESP 是二进制安全的——$6 foobar中的foobar可以是任意二进制数据(包括 ``),而 JSON 的字符串必须转义控制字符。

  • 1.2 RESP2(Redis 2.x~5.x)——五种基础类型RESP2 定义了五种基础数据类型,每种以特定前缀字符标识:

    类型前缀名称用途示例
    +Simple String简单状态回复`+OK

| |-| Error | 错误信息 |-ERR wrong number of arguments
| |:| Integer | 整数回复 |:1000
| |$| Bulk String | 二进制安全字符串 |6 f o o b a r ‘ 、 ‘ 6 foobar `、 `6foobar-1
(nil) | |*| Array | 数组/命令参数 |*2
$3
foo
$3
bar
` |

协议格式规则

  • 每行以 (CRLF)结尾;

  • 批量字符串($)先传长度(十进制),再传内容,长度-1表示 nil;

  • 数组(*)先传元素个数,再依次传各元素;

  • 空数组用*0,nil 数组用*-1

  • 1.3 RESP3(Redis 6.0+)——类型系统大升级Redis 6.0 引入 RESP3,新增多种类型以支持更丰富的语义:

    类型前缀名称用途示例
    _Null统一 nil 表示`_

| |#| Boolean | 布尔值 |#t
/#f
| |,| Double | 浮点数 |,3.14159
| |(| Big Number | 大整数 |(349289352840923850932485094385094
| |!| Bulk Error | 二进制安全错误 |!21
ERR: 二进制错误内容
| |=| Verbatim String | 原始文本(带格式标记) |=15
txt:Some text
| |%| Map | 键值对映射 |%2
$3
foo
$3
bar
| |~| Set | 无序集合 |~2
$3
foo
$3
bar
` |
| `|` | Attribute | 元数据属性 | `|1
+key-popularity
%2
…` |
| `>` | Push | 服务端主动推送 | `>3
$7
message
…` |

RESP3 的核心改进

  • 类型语义化:客户端无需猜测返回值类型(如HGETALL返回 Map 而非 Array);
  • Push 类型:支持服务端主动推送(Pub/Sub、客户端缓存失效通知);
  • Null 统一:RESP2 中 Bulk String 的$-1和 Array 的*-1都表示 nil,RESP3 统一为_
2. 协议解析的底层实现与性能优化
  • 2.1 Redis 服务端的协议解析流程Redis 服务端通过readQueryFromClient读取客户端数据,经processInputBuffer解析 RESP:

    // networking.c 简化逻辑voidprocessInputBuffer(client*c){while(c->qb_pos<sdslen(c->querybuf)){// 1. 查找第一个

定位行尾
char *newline = strchr(c->querybuf + c->qb_pos, ’
');
if (newline == NULL) break; // 数据不完整,等待更多数据

// 2. 根据首字符判断类型,调用对应解析函数 char type = c->querybuf[c->qb_pos]; switch(type) { case '*': processMultiBulkBuffer(c); break; // 数组(命令) case '$': processBulkBuffer(c); break; // 批量字符串 // ... 其他类型 } }

}

**关键优化**:Redis 使用 **查询缓冲区(querybuf)** 累积数据,避免每次读取都进行系统调用。`c->qb_pos` 记录已解析位置,支持流式解析不完整的 RESP 数据。 - **2.2 零拷贝与内存优化** | 优化技术 | 实现方式 | 效果 | | -------- | -------- | ---- | | **查询缓冲区复用** | `querybuf` 采用 SDS(Simple Dynamic String),扩容时预分配额外空间 | 减少内存分配次数 | | **对象共享** | 小整数(0~9999)共享同一个 `robj` 对象 | 减少内存占用 | | **Pipeline 批量解析** | 一次 read 读取多个命令,批量解析执行 | 减少系统调用和上下文切换 | | **RESP3 属性剥离** | `\|` 类型的 Attribute 数据不进入主回复流 | 减少客户端解析开销 | - **2.3 Pipeline 的协议层实现** Pipeline 不是 Redis 的新协议,而是客户端的 **批量发送策略**:

客户端发送(单次 write):
*3
$3
SET
$4
key1
$6
value1

*3
$3
SET
$4
key2
$6
value2

*2
$3
GET
$4
key1

服务端返回(单次 read 或多次 read):
+OK

+OK

$6
value1

**性能提升**:Pipeline 将多个命令的 RTT(Round-Trip Time)从 N 次降为 1 次,配合 TCP_NODELAY 禁用 Nagle 算法,吞吐量可提升 10 倍以上。 ##### **3. Pub/Sub 与 RESP 协议的特殊交互** - **3.1 普通命令 vs Pub/Sub 的协议差异** | 模式 | 通信方向 | 协议类型 | 连接状态 | | ---- | -------- | -------- | -------- | | **普通命令** | 请求-响应 | RESP2/RESP3 | 无状态 | | **Pub/Sub** | 双向推送 | RESP2 Array / RESP3 Push | 进入 Pub/Sub 模式 | **RESP2 的 Pub/Sub 消息格式**:

*3
– 数组,3 个元素
$7
– 第一个元素长度 7
message
– 消息类型:message
$5
– 第二个元素长度 5
mychannel
– 频道名
$7
– 第三个元素长度 7
hello!
– 消息内容

**RESP3 的改进**:使用 `>` Push 类型替代 `*` Array,语义更清晰,客户端可区分"命令回复"和"服务端推送"。 - **3.2 客户端缓存(Client-Side Caching)的 RESP3 支持** Redis 6.0 引入客户端缓存,通过 RESP3 的 `>` Push 类型发送键失效通知:

3
– Push 类型,3 个元素
$10
– 第一个元素长度 10
invalidate
– Push 类型:invalidate
*1
– 第二个元素是数组,1 个元素
$4
– 元素长度 4
key1
– 失效的键名

客户端收到 `invalidate` 推送后,从本地缓存中删除对应键,实现近实时的缓存一致性。 ##### **4. 自定义客户端的协议解析陷阱** - **4.1 常见陷阱与正确做法** | 陷阱 | 错误做法 | 正确做法 | | ---- | -------- | -------- | | **行尾解析** | 用 ` ` 分割,忽略 ` ` | 严格匹配 ` `,处理 ` ` 残留 | | **长度字段** | 用 `int` 解析长度 | 用 `long long`,Redis 支持 512MB 的 Bulk String | | **二进制安全** | 用 C 字符串函数(`strlen`)处理内容 | 按长度字段精确读取,内容可含 `` | | **嵌套解析** | 递归深度无限制 | 设置最大递归深度(如 128),防止恶意攻击 | | **内存分配** | 按长度字段直接 `malloc` | 限制单次分配上限,防止 OOM 攻击 | | **不完整数据** | 假设每次 read 都能读到完整命令 | 维护解析状态机,支持跨包解析 | - **4.2 协议解析状态机示例** ```c // 简化的 RESP 解析状态机 typedef enum { STATE_READ_TYPE, // 读取类型前缀(+ - : $ * 等) STATE_READ_LENGTH, // 读取批量长度($ 或 * 后的数字) STATE_READ_DATA, // 读取具体内容 STATE_READ_CRLF // 读取行尾 } ParseState; // 关键:必须支持跨包解析 // 如果 read 只返回了 "$6 foo",需要缓存并等待下次 read 拿到 "bar "
5. RESP 与 Memcached 文本协议、HTTP/2 的对比
协议类型特点适用场景
RESP文本 + 二进制安全前缀标识符,状态机解析,极简实现Redis 专用,高性能缓存
Memcached 文本协议纯文本类似 HTTP,命令 + 参数 +
Memcached,简单但功能有限
Memcached 二进制协议二进制固定头部 + 可变体,高效Memcached,减少解析开销
HTTP/1.1文本通用、可扩展、人类可读Web 服务,但解析开销大
HTTP/2二进制多路复用、头部压缩、流控制现代 Web,复杂但高效
gRPC/Protobuf二进制强类型、IDL 定义、高效序列化微服务通信

RESP 的独特优势:在"简单性"和"功能性"之间取得了极致平衡。没有 HTTP 的复杂头部,没有 gRPC 的强类型约束,但比 Memcached 协议更丰富的类型系统。

6. 面试官追问与高分回答模板
  • 追问 1:“Redis 底层使用什么协议?RESP 有哪些数据类型?”

    低分回答:“RESP 协议,有简单字符串、错误、整数、批量字符串和数组五种。”(没有区分 RESP2 和 RESP3)

    高分回答

    "Redis 底层使用RESP(Redis Serialization Protocol)协议,目前有两个版本:

    1. RESP2(Redis 2.x~5.x):定义了五种基础类型——+Simple String、-Error、:Integer、$Bulk String、*Array。特点是前缀标识符 + CRLF 分隔,二进制安全,任何语言 100 行代码即可实现客户端。
    2. RESP3(Redis 6.0+):在 RESP2 基础上新增了 Boolean、Double、Big Number、Null、Bulk Error、Verbatim String、Map、Set、Push 等类型。核心改进是类型语义化(客户端无需猜测返回值类型)和服务端主动推送(Push 类型支持 Pub/Sub 和客户端缓存失效通知)。
      选型上,Redis 6.0+ 默认使用 RESP3,但客户端可通过HELLO 2降级到 RESP2。"
  • 追问 2:“RESP 协议为什么不用 JSON?”

    低分回答:“JSON 解析慢。”(太笼统)

    高分回答

    "RESP 选择自定义协议而非 JSON,基于四个工程权衡:

    1. 解析效率:RESP 是前缀标识符 + 固定分隔符,状态机解析即可,O(1) 定位每行;JSON 需要递归解析,遇到嵌套对象时解析复杂度上升。
    2. 二进制安全:RESP 的 Bulk String 通过长度字段标识内容边界,内容可以是任意二进制数据(含 ``);JSON 字符串必须转义控制字符,不适合传输二进制数据(如序列化后的 Java 对象)。
    3. 内存友好:RESP 支持流式解析,收到部分数据即可开始解析;JSON 通常需要完整加载到内存才能解析(除非用 SAX 风格解析器)。
    4. 实现简单:RESP 客户端在任何语言中 100 行代码即可实现;JSON 库通常数千行,且不同语言实现差异大。
      当然,代价是 RESP 是 Redis 专用协议,不如 JSON 通用。"
  • 追问 3:“Pipeline 在协议层面是怎么实现的?”

    低分回答:“就是一次发送多个命令。”(没有触及协议格式)

    高分回答

    "Pipeline 不是 Redis 的新协议,而是客户端的批量发送策略。协议层面:

    1. 发送端:客户端将多个 RESP 命令拼接成一个字节流,通过单次write系统调用发送。例如两个 SET 命令拼接为:
      *3 $3 SET $4 key1 $6 value1 *3 $3 SET $4 key2 $6 value2
    2. 服务端:Redis 单线程依次解析执行,将多个回复放入输出缓冲区,通过单次或多次write返回。
    3. 性能收益:将 N 个命令的 N 次 RTT 降为 1 次 RTT。配合TCP_NODELAY禁用 Nagle 算法,吞吐量可提升 10 倍以上。
      注意:Pipeline 只是减少网络往返,不保证原子性。如果需要原子性,用MULTI/EXEC或 Lua 脚本。"
  • 追问 4:“RESP3 相比 RESP2 有哪些重要改进?”

    高分回答

    "RESP3 是 Redis 6.0 引入的重大升级,核心改进有三方面:

    1. 类型系统扩展:新增 Boolean、Double、Big Number、Null、Map、Set 等类型。例如HGETALL在 RESP2 返回*4 ...(数组),客户端需按索引解析键值对;RESP3 返回%2 ...(Map),语义更清晰。
    2. Push 类型(>:服务端可主动向客户端推送消息。这是客户端缓存(Client-Side Caching)的基础——当缓存的键被修改时,Redis 通过> invalidate推送通知客户端删除本地缓存。
    3. Null 统一:RESP2 中 Bulk String 的$-1和 Array 的*-1都表示 nil,RESP3 统一为_,减少客户端判断逻辑。
      兼容性:Redis 6.0+ 服务端同时支持 RESP2 和 RESP3,客户端通过HELLO 2HELLO 3协商版本。"
  • 追问 5:“自定义 Redis 客户端时,协议解析有哪些陷阱?”

    高分回答

    "自定义客户端时,RESP 解析有六个常见陷阱:

    1. 行尾匹配:必须用 严格匹配,不能只找 。某些实现中 残留会导致解析错位。
    2. 长度字段越界:Bulk String 的长度字段是十进制字符串,需用 64 位整数解析。Redis 支持最大 512MB 的字符串,32 位整数会溢出。
    3. 二进制安全:内容按长度字段精确读取,不能用 C 的strlen或字符串函数处理,因为内容可含 ``。
    4. 嵌套深度限制:数组可嵌套数组(如*2 *2 ...),必须设置最大递归深度(如 128),防止恶意客户端发送深层嵌套导致栈溢出。
    5. 内存分配上限:按长度字段malloc前必须限制上限,防止$999999999这类攻击导致 OOM。
    6. 跨包解析:TCP 是流式协议,单次read可能只收到部分 RESP 数据。必须维护解析状态机,支持从中间状态恢复。"
  • 追问 6:“Redis 的 Pub/Sub 在协议层面是怎么实现的?”

    高分回答

    "Redis Pub/Sub 的协议实现分 RESP2 和 RESP3 两个阶段:

    1. RESP2 阶段:Pub/Sub 消息以*Array 类型发送。例如订阅mychannel后收到消息:
      *3 $7 message $9 mychannel $5 hello
      三个元素分别是:消息类型(message/pmessage/subscribe)、频道名/模式、消息内容。
    2. RESP3 阶段:使用>Push 类型替代*Array,语义更清晰。客户端可明确区分’命令回复’和’服务端推送’。
    3. 连接状态:客户端执行SUBSCRIBE后,连接进入 Pub/Sub 模式,只能接收推送消息,不能再发送普通命令(除非使用SSUBSCRIBESUNSUBSCRIBE的 sharded Pub/Sub)。
    4. 协议限制:Pub/Sub 消息不持久化、不确认,网络闪断期间的消息会丢失。Redis 5.0 引入 Stream 作为更可靠的消息队列替代方案。"
7. 方案选型速查表
业务场景推荐协议/模式核心理由注意事项
简单 KV 读写RESP2/RESP3 普通命令简单直接高并发时用连接池
批量操作(无原子性要求)Pipeline + RESP2/3减少 RTT,提升吞吐量命令数不宜过多(避免阻塞)
批量操作(需原子性)MULTI/EXEC + RESP2/3入队原子性避免运行时错误
复杂原子计算Lua 脚本 + RESP2/3执行原子性脚本不宜过长
实时消息推送RESP3 Push 类型语义清晰,支持客户端缓存消息不持久化
跨语言服务通信不选 RESP,用 gRPC/HTTPRESP 是 Redis 专用Redis 协议不适合通用 RPC
自定义轻量客户端RESP2实现简单,兼容性好注意跨包解析和内存安全

💡面试官想要的满分总结

Redis 底层使用RESP(Redis Serialization Protocol)协议,其设计哲学是“极致的简单性换取极致的性能”。RESP2 通过+ - : $ *五种前缀标识符实现了二进制安全、流式解析、极简实现;RESP3 在此基础上扩展了类型系统,引入 Push 类型支撑客户端缓存等高级特性。

理解 RESP 不能停留在"五种数据类型"的表层,而要深入到协议解析的零拷贝优化(查询缓冲区复用、SDS 预分配)、Pipeline 的批量发送原理(单次 write 减少 RTT)、以及自定义客户端的六个解析陷阱(行尾匹配、长度越界、二进制安全、嵌套深度、内存上限、跨包解析)。

工程选型上,简单操作用普通命令,批量操作用 Pipeline,原子操作用 Lua 脚本,实时推送用 RESP3 Push。RESP 是 Redis 高性能的基石之一,但它是专用协议,不要试图将其泛化为通用 RPC 协议。


觉得对您有帮助,麻烦点点关注啦,您的关注是我创作的最大动力~ 🎯

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询