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