命令执行与 RESP
一条
SET key value从客户端发出到 Redis 返回OK,中间经历了协议编码、网络传输、协议解析、命令查找、执行、响应写回。理解这条链路,才能解释命令是如何被可靠、高效地执行的。
1. RESP 协议
RESP(REdis Serialization Protocol)是客户端与 Redis 通信的协议。它简单、易解析、二进制安全。
1.1 五种数据类型
| 类型 | 首字节 | 含义 | 示例 |
|---|---|---|---|
| 简单字符串 | + | 简单回复 | +OK\r\n |
| 错误 | - | 错误信息 | -ERR unknown command\r\n |
| 整数 | : | 整数回复 | :1000\r\n |
| 批量字符串 | $ | 二进制安全的字符串 | $3\r\nfoo\r\n |
| 数组 | * | 多个元素 | *2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n |
1.2 命令编码格式
客户端把命令编码为「数组 + 批量字符串」的格式发给 Redis:
客户端发送:SET mykey myvalue
实际字节流:*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n
↑ ↑ ↑ ↑
数组3 SET(长度3) mykey(长度5) myvalue(长度7)*3:这是一个包含 3 个元素的数组$3\r\nSET\r\n:第一个元素是长度 3 的字符串 "SET"- 依次类推
1.3 内联命令
除了 RESP 格式,Redis 也支持内联命令(空格分隔的纯文本),例如直接发送 PING。这只用于 telnet 等手工调试场景,客户端库一般都用 RESP 格式。
1.4 RESP3(Redis 6.0+)
RESP2 只有 5 种类型,RESP3 新增了以下 10 种类型(完整清单):
| 新增类型 | 首字节 | 说明 |
|---|---|---|
| 双精度浮点 | , | 浮点数 |
| 布尔 | # | true/false |
| 空值 | _ | NULL |
| Blob 错误 | ! | 带长度的错误信息 |
| 大整数 | ( | 任意精度整数 |
| Verbatim 字符串 | = | 带格式提示的字符串 |
| 属性 | | | 附加元数据 |
| 推送 | > | 服务端主动推送的数据 |
| Map | % | 键值对映射 |
| Set | ~ | 集合类型 |
其中 Push(推送)允许服务端在普通请求响应之外主动推送数据,常用于客户端缓存失效通知。
# 启用 RESP3
HELLO 3RESP3 的价值在于更丰富的类型系统,客户端可以更精确地反序列化响应,减少类型转换。
2. 命令解析
Redis 收到网络字节流后,需要把它解析成内部可执行的命令结构。
解析流程:
解析的关键点:
| 环节 | 说明 |
|---|---|
| 读缓冲区 | 客户端连接有一个输入缓冲区(默认硬上限 1GB),读到的字节先暂存于此 |
| 协议判断 | 首字节是 * 按 RESP 数组格式解析,否则尝试按内联命令格式解析 |
| 参数数组 | 解析出 argv(参数数组)与 argc(参数个数) |
| 二进制安全 | 批量字符串按 $长度 精确读取,不依赖 \0 结尾 |
2.1 客户端输入缓冲区
每个客户端连接都有一个输入缓冲区(querybuf),用于暂存从网络读到的数据:
# 查看客户端缓冲区
redis-cli CLIENT LIST
# qbuf=1024 qbuf-free=32678 ...| 字段 | 含义 |
|---|---|
qbuf | 已使用的缓冲区大小 |
qbuf-free | 剩余可用空间 |
输入缓冲区用于累计客户端发来的命令。超大命令、批量事务或协议不同步可能使其增长;
KEYS *返回百万个 key 产生的大响应则占用服务端到客户端的输出缓冲区。两者应分别使用client-query-buffer-limit和client-output-buffer-limit检查。
3. 命令表查找
解析出命令名后,Redis 需要找到对应的命令实现。它维护一个命令表——所有命令的哈希表。
命令表(哈希表,O(1) 查找)
"SET" → redisCommand { 实现函数, 参数个数, 属性标志 }
"GET" → redisCommand { ... }
"HSET" → redisCommand { ... }
...redisCommand 结构的关键字段:
| 字段 | 作用 |
|---|---|
proc | 实现函数指针,指向真正执行命令的 C 函数 |
arity | 参数个数(含命令名本身),正数=精确个数,负数=最小个数 |
flags | 命令属性(写命令/只读命令/管理命令等) |
microseconds | 累计执行耗时(用于 INFO commandstats 命令统计) |
calls | 累计调用次数 |
查找特点:
- O(1)(平均)哈希查找:命令名作为 key,哈希表直接命中
- 大小写不敏感:
SET与set等价 - 未知命令报错:找不到返回
-ERR unknown command
4. 执行流程与钩子
找到命令后,执行过程会依次经过参数校验、键过期检查、命令执行、传播等环节,这些环节就像挂在命令执行链路上的钩子,承载了 Redis 的很多附加能力。
执行流程各环节:
| 环节 | 说明 |
|---|---|
| 参数校验 | 检查参数个数是否符合 arity 要求 |
| 键过期检查 | 惰性删除:访问到已过期 Key 时先删除再执行 |
| 执行命令 | 调用命令对应的实现函数,操作数据结构 |
| 慢查询日志 | 记录执行耗时超过阈值的命令 |
| AOF 传播 | AOF 启用时,写命令追加到 AOF 缓冲区(见 持久化) |
| 复制传播 | 按复制机制把需要同步的写操作传播给从库(见 主从复制) |
| 返回响应 | 把结果编码为 RESP 格式写回客户端 |
这种设计的价值:Redis 把「持久化」「复制」等能力内建在命令执行流程中,写命令执行后进入传播环节,由 Redis 根据 AOF 是否启用、复制是否配置来决定实际处理,无需命令实现自己操心。这是 Redis 架构简洁的关键设计之一。
5. 慢查询日志
慢查询日志记录执行时间超过阈值的命令,是排查性能问题的第一手资料。它统计的是命令执行阶段的耗时,不包含客户端通信与响应发送等 I/O 时间,因此客户端请求延迟高但慢查询无记录时,还需检查网络与连接处理环节。
# 配置
slowlog-log-slower-than 10000 # 超过 10ms 记录(微秒)
slowlog-max-len 128 # 最多保存 128 条
# 查看
SLOWLOG GET 10 # 获取最近 10 条慢查询
SLOWLOG LEN # 慢查询总数
SLOWLOG RESET # 清空慢查询日志输出示例:
1) 1) (integer) 1 # 日志 ID
2) (integer) 1712500000 # 时间戳
3) (integer) 15000 # 耗时(微秒)
4) 1) "KEYS" # 命令
2) "*"
5) "127.0.0.1:54321" # 客户端地址
6) "" # 客户端名称| 常见慢命令 | 原因 | 替代方案 |
|---|---|---|
KEYS * | 遍历所有 key,O(n) | SCAN 分批迭代 |
HGETALL | 大 Hash 全量获取 | HSCAN 或只取需要的字段 |
SMEMBERS | 大 Set 全量获取 | SSCAN 分批迭代 |
DEL 大 Key | 同步删除阻塞 | UNLINK 异步删除 |
SORT | 排序开销大 | 业务层排序,或提前用有序集合按 score 维护 |
慢查询日志是排查性能问题的第一步。如果发现慢命令,优先用替代方案;如果没有替代方案,考虑拆分数据或异步处理。