一、前言:高效通信的基石

当我们使用 redis-cli 执行一条 SET name "Redis" 命令时,背后发生了一系列精妙的通信过程。客户端如何告诉服务器它想执行什么命令?服务器又如何将“OK”这个结果准确无误地返回给客户端?

这一切的奥秘,都藏在 Redis 自研的通信协议——RESP(REdis Serialization Protocol) 之中。

RESP 协议是 Redis 客户端与服务端之间沟通的“通用语言”。理解它,不仅能让你更深刻地认识 Redis 的工作原理,还能帮助你从零开始实现一个自己的 Redis 客户端!

💡 核心价值
RESP 协议以其简单、高效、可读性强和二进制安全的特性,成为了 Redis 高性能的基石之一。本文将带你彻底掌握 RESP 协议的五种数据类型、编码规则以及完整的请求-响应流程


二、RESP 协议简介

2.1 什么是 RESP?

RESP 全称 REdis Serialization Protocol(Redis 序列化协议)。它由 Redis 作者 Salvatore Sanfilippo 设计,于 Redis 1.2 版本引入,并在 Redis 2.0 中成为标准通信协议。

虽然它是为 Redis 量身定制的,但其简洁的设计使其完全可以应用于其他客户端-服务器(C/S)软件项目。

2.2 核心设计哲学

  • 简单性:协议格式直观,易于人类阅读和程序解析。
  • 高效性:解析开销极低,无需复杂的词法或语法分析器。
  • 二进制安全:能够安全地传输任何二进制数据,不会因为特殊字符(如 \r\n)而产生歧义。
  • 可扩展性:支持多种数据类型,能表达复杂的嵌套结构。

三、RESP 的五种数据类型

RESP 协议通过在每条消息的开头使用一个特定的字节来标识其数据类型。共有五种基本类型:

类型 首字节 描述 示例
简单字符串 (Simple Strings) + 用于表示非二进制安全的简单状态或成功消息。不能包含 \r 或 \n +OK\r\n
错误 (Errors) - 用于返回错误信息。本质上也是一种字符串,但客户端应将其视为异常。 -ERR unknown command 'FOO'\r\n
整数 (Integers) : 用于表示有符号的64位整数。 :1000\r\n
批量字符串 (Bulk Strings) $ 最常用!用于表示二进制安全的单个字符串,可以包含任意数据,包括空值 (null)。 $5\r\nhello\r\n
$-1\r\n (表示 null)
数组 (Arrays) * 用于表示元素列表,元素可以是任意 RESP 数据类型,包括嵌套数组。 *2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n

⚠️ 注意:所有 RESP 消息都以 \r\n (CRLF) 结尾。


四、深度解析:每种类型的编码规则

4.1 简单字符串 (Simple Strings)

  • 格式+<字符串>\r\n
  • 用途:通常用于返回简单的状态码,如 OK
  • 限制:不能包含换行符,因此不适用于传输任意数据。
// 服务器响应
+OK\r\n

4.2 错误 (Errors)

  • 格式-<错误信息>\r\n
  • 用途:当服务器遇到错误时返回。客户端库通常会将此转换为异常抛出。
  • 惯例:错误信息通常以错误码开头,如 ERRWRONGTYPE
// 服务器响应
-ERR Operation against a key holding the wrong kind of value\r\n

4.3 整数 (Integers)

  • 格式:<数字>\r\n
  • 用途:返回整数值,如 INCR 命令的结果、LLEN 返回的列表长度等。
// 服务器响应
:42\r\n

4.4 批量字符串 (Bulk Strings) - 重点

  • 格式
    • 非空字符串:$<长度>\r\n<数据>\r\n
    • 空字符串:$0\r\n\r\n
    • Null 值:$-1\r\n
  • 用途:这是传输实际数据(如 key, value)的主要方式,因为它二进制安全
  • 优势:通过前置长度,接收方可以精确地知道要读取多少字节,无需寻找终止符,效率极高。
// 传输 "hello"
$5\r\nhello\r\n

// 传输一个包含换行符的二进制数据 "\r\n"
$2\r\n\r\n\r\n

// 表示 GET 一个不存在的 key
$-1\r\n

4.5 数组 (Arrays) - 重点

  • 格式*<元素数量>\r\n<元素1><元素2>...<元素N>
  • 用途客户端发送的所有命令都是以数组形式编码的!同时,服务器返回复杂结果(如 LRANGEHGETALL)也用数组。
  • 灵活性:数组中的每个元素可以是任意 RESP 类型,支持嵌套。
// 客户端发送命令: SET mykey "Hello World"
*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$11\r\nHello World\r\n

// 服务器响应 LRANGE list 0 -1 (假设list有两个元素 "a", "b")
*2\r\n$1\r\na\r\n$1\r\nb\r\n

// 服务器返回一个空列表
*0\r\n

五、一次完整的通信流程

让我们通过 GET mykey 这个简单命令,走完一次完整的客户端-服务器交互。

5.1 客户端 -> 服务器 (请求)

客户端需要将命令序列化为一个 Bulk String 数组

  • 命令本身是一个字符串 "GET"
  • 参数 mykey 是另一个字符串

因此,请求被编码为:

*2\r\n$3\r\nGET\r\n$5\r\nmykey\r\n

分解:

  • *2:这是一个包含 2 个元素的数组。
  • $3\r\nGET\r\n:第一个元素,长度为 3 的字符串 "GET"。
  • $5\r\nmykey\r\n:第二个元素,长度为 5 的字符串 "mykey"。

5.2 服务器 -> 客户端 (响应)

假设 mykey 的值是 "Hello",服务器会返回一个 Bulk String。

$5\r\nHello\r\n

如果 mykey 不存在,则返回 Null Bulk String:

$-1\r\n

六、为什么选择文本协议?

很多人可能会疑惑:文本协议不是比二进制协议更“浪费”带宽吗?为什么高性能的 Redis 要选择它?

Redis 作者对此有清晰的解释:

  1. 瓶颈不在网络:对于内存数据库而言,真正的瓶颈是 CPU 处理命令的逻辑和内存带宽,而非网络传输的数据量。节省几个字节的协议头带来的收益微乎其微。
  2. 极致的简单性:文本协议极大地简化了客户端和服务器的实现与调试。你可以直接用 telnet 或 nc 连接到 Redis 服务器并手动输入命令进行测试!
  3. 可读性强:在抓包分析或日志记录时,文本协议的内容一目了然,无需额外的解析工具。

这种“在非瓶颈处做简单设计”的哲学,正是 Redis 成功的关键因素之一。


七、结语

感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐