1.3 客户端与RESP协议


文档摘要

1.3 客户端与RESP协议 本节摘要:RESP 是 Redis 客户端与服务端之间的文本序列化协议,简单到可以用肉眼读。看懂它,pipeline 为什么快、大响应为什么伤带宽、订阅模式为什么"推"给你,这些问题都有直接答案。 协议长什么样 无需抓包,redis-cli 就能替你打印命令的原始报文: 那条字节数组的含义: 每个参数自带长度前缀,服务端不需要扫描分隔符,解析是 O(1) 定位的。简单、可读、长度前缀,这三点让 RESP 的解析开销低到可以忽略,也让你用任何语言几十行代码就能写个客户端。 不信可以亲手写一个。背景:怀疑客户端库的行为异常,需要一个"绝对干净"的对照客户端;操作:用原生套接字拼一条 GET 报文;结果如下: 二十行不到就有了一个能发命令的客户端。

1.3 客户端与RESP协议

本节摘要:RESP 是 Redis 客户端与服务端之间的文本序列化协议,简单到可以用肉眼读。看懂它,pipeline 为什么快、大响应为什么伤带宽、订阅模式为什么"推"给你,这些问题都有直接答案。

协议长什么样

无需抓包,redis-cli 就能替你打印命令的原始报文:

redis-cli -p 6390 > SET name zhang OK # 用协议模式重放 redis-cli -p 6390 --no-raw # 或者直接手写一条 RESP: printf '*3\r\n$3\r\nSET\r\n$4\r\nname\r\n$5\r\nzhang\r\n' | nc 127.0.0.1 6390

那条字节数组的含义:

*3 → 数组,共 3 个元素 $3 SET → 长度 3 的参数 $4 name → 长度 4 的参数 $5 zhang → 长度 5 的参数

每个参数自带长度前缀,服务端不需要扫描分隔符,解析是 O(1) 定位的。简单、可读、长度前缀,这三点让 RESP 的解析开销低到可以忽略,也让你用任何语言几十行代码就能写个客户端。

不信可以亲手写一个。背景:怀疑客户端库的行为异常,需要一个"绝对干净"的对照客户端;操作:用原生套接字拼一条 GET 报文;结果如下:

import socket def cmd(*args): # 拼装:*参数个数 + 每个参数的$长度前缀 buf = f"*{len(args)}\r\n".encode() for a in args: b = a.encode() buf += f"${len(b)}\r\n".encode() + b + b"\r\n" return buf s = socket.create_connection(("127.0.0.1", 6390)) s.sendall(cmd("SET", "name", "zhang")) # 发送 print(s.recv(100)) # b'+OK\r\n' # 服务端返回简单字符串 s.sendall(cmd("GET", "name")) print(s.recv(100)) # b'$5\r\nzhang\r\n' # 批量字符串带长度

二十行不到就有了一个能发命令的客户端。解读这段输出:加号开头是简单字符串 OK,美元符开头是批量字符串、先报 5 个字节再给内容。变式:把它改成循环发送就能自测 pipeline;加上密码参数就能测 AUTH 流程。抓到"库的行为"与"协议的真相"不一致时,这个小工具就是裁判。

通信时序

普通命令是严格的一问一答;订阅之后方向反转,服务端开始主动推——这是第 4 章发布订阅的协议基础。

各语言客户端的选择

语言 主流客户端 一句话建议
Java Jedis / Lettuce Spring 生态默认 Lettuce,连接基于 Netty,支持异步
Python redis-py 自带连接池与 pipeline,够用
Go go-redis 集群模式开箱即用
Node.js ioredis 对 Cluster 与 Sentinel 支持成熟

选型上真正影响命运的不是库,而是你有没有用连接池。每条命令都新建 TCP 连接的系统,瓶颈永远不在 Redis。

从协议推出来的三个工程结论

第一,pipeline 快在哪。一次网络往返发 1000 条命令打包、响应一次返回,省掉的是 999 次往返时延:

import redis r = redis.Redis(port=6390) pipe = r.pipeline(transaction=False) for i in range(1000): pipe.set(f"key:{i}", i) pipe.execute() # 一次往返完成

注意 pipeline 不是事务——中间可能插进别的客户端的命令。

第二,大响应是带宽炸弹。协议是文本的,一个 10MB 的 VALUE 会以 10MB 加转义的形式打满网卡,还会让主线程在序列化上耗时。HGETALL 一个十万 field 的大哈希就是典型事故现场,改成 HMGET 分批取。

第三,RESP3 带来了 map、set 等新类型响应,客户端可以少做一层转换,但协议升级是握手可选的,老客户端照旧工作,不必焦虑。

💡 关键直觉:把 Redis 客户端当成"协议适配层 + 连接池 + 序列化器"三件事来看,任何客户端库的差异都能对号入座。

连接层的排错速查

协议之上的连接管理,是另一类高频故障,报错信息直接指向病因:

报错 病因 处置
max number of clients reached 连接数打满上限或泄漏 查连接池上限与释放路径,调 maxclients
Cannot assign requested address 客户端频繁新建连接耗尽本地端口 连接池长连复用
Connection reset by peer 空闲连接被 timeout 回收 客户端开 keepalive 探活
LOADING Redis is loading the dataset 重启后正在加载持久化文件 等待加载完成,启动逻辑要容忍

诊断的第一现场是一条命令:CLIENT LIST 会打出每个连接的年龄、空闲时长、订阅状态,连接泄漏一眼就能看出来——成片的连接年龄大、空闲时间大、却没人认领。

表里的四条报错覆盖了九成的连接层工单,还有一类问题不报错更隐蔽:连接池配了一万但业务只用两百,多余连接白占服务端内存与文件句柄。CLIENT LIST 的输出按空闲时长排个序,超过分钟级的空闲连接就该回收——连接池的最大空闲数配置就是为这个准备的。

本节要点回顾

  • RESP 是带长度前缀的文本协议,一条 SET 就是星号开头的字节数组
  • 请求响应模型:普通命令一问一答,订阅后转为服务端推送
  • pipeline 省往返不保原子,要原子得用事务或 Lua(第 4 章)
  • 大 Key 的伤害先体现在协议层:响应体积打满带宽与序列化时间
  • 客户端选型看连接池与异步支持,库本身的命令封装差异不大

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U