强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

04 - 命令格式与发送

命令格式与发送

4.1 命令的本质

在 Redis 中,命令就是一个字符串数组。无论命令多复杂,最终都编码为 RESP Array:

*<参数数量>\r\n
$<参数1长度>\r\n<参数1>\r\n
$<参数2长度>\r\n<参数2>\r\n
...

命令解析流程

客户端发送                    服务端处理
─────────                   ──────────
"SET key value"
       ↓
编码为 RESP Array
       ↓
*3\r\n$3\r\nSET\r\n...  ──→  读取 Array → 提取参数列表
                                     ↓
                              查找命令处理器
                                     ↓
                              执行命令逻辑
                                     ↓
                              编码响应为 RESP
                              ←── +OK\r\n

4.2 标准命令格式

单键命令

# SET key value
*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n

# GET key
*2\r\n$3\r\nGET\r\n$3\r\nkey\r\n

# DEL key
*2\r\n$3\r\nDEL\r\n$3\r\nkey\r\n

多键命令

# MGET key1 key2 key3
*4\r\n$4\r\nMGET\r\n$4\r\nkey1\r\n$4\r\nkey2\r\n$4\r\nkey3\r\n

# DEL key1 key2 key3
*4\r\n$3\r\nDEL\r\n$4\r\nkey1\r\n$4\r\nkey2\r\n$4\r\nkey3\r\n

带可选参数的命令

# SET key value EX 3600 NX
*6\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n$2\r\nEX\r\n$4\r\n3600\r\n$2\r\nNX\r\n

# KEYS pattern
*2\r\n$4\r\nKEYS\r\n$1\r\n*\r\n

无参数命令

# PING
*1\r\n$4\r\nPING\r\n

# DBSIZE
*1\r\n$6\r\nDBSIZE\r\n

# INFO
*1\r\n$4\r\nINFO\r\n

# INFO server(带子命令)
*2\r\n$4\r\nINFO\r\n$6\r\nserver\r\n

4.3 命令大小写

Redis 命令不区分大小写,但按惯例使用大写:

写法是否合法推荐
SET key value✅ 推荐
set key value⚠️ 可用但不推荐
Set key value⚠️ 可用但不推荐

服务端在查找命令处理器时会将命令名转为大写。但从协议角度,发送什么字节就是什么字节。


4.4 内联命令(Inline Command)

什么是内联命令

除了标准的 RESP 数组格式,Redis 还支持一种简化的"内联"格式。内联命令是用空格分隔的纯文本行:

PING\r\n
SET key value\r\n
GET key\r\n

内联 vs RESP 对比

# 内联格式
SET key value\r\n

# RESP 格式(等价)
*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n

内联命令的检测逻辑

Redis 服务端在接收到数据时,根据第一个字节判断格式:

第一个字节 == '*'  →  RESP 数组格式
第一个字节 != '*'  →  内联命令格式

源码位置(networking.c):

void processInputBuffer(client *c) {
    while (c->qb_pos < sdslen(c->querybuf)) {
        if (!c->reqtype) {
            if (c->querybuf[0] == '*') {
                c->reqtype = PROTO_REQ_MULTIBULK;
            } else {
                c->reqtype = PROTO_REQ_INLINE;
            }
        }

        if (c->reqtype == PROTO_REQ_MULTIBULK) {
            if (processMultibulkBuffer(c) != C_OK) break;
        } else {
            if (processInlineBuffer(c) != C_OK) break;
        }
    }
}

内联命令的限制

特性内联命令RESP 命令
参数中的空格❌ 不能包含✅ 可以包含
二进制数据❌ 不支持✅ 支持
换行符参数❌ 不支持✅ 支持
性能略低(需要文本解析)更高(长度前缀直接跳转)
适用场景手动调试生产环境

telnet 中使用内联命令

$ telnet 127.0.0.1 6379
PING
+PONG
SET mykey hello
+OK
GET mykey
$5
hello
DBSIZE
:1

内联命令在 telnet 调试时非常方便,但生产环境应始终使用 RESP 格式。


4.5 命令别名与子命令

命令别名

某些命令有别名,它们在协议层面上是等价的:

别名等价于说明
DELDEL删除 key
UNLINKDEL(异步)Redis 4.0+
SUBSTRGETRANGE已废弃

子命令

某些命令有子命令,在 RESP 中作为独立的参数:

# CLIENT LIST
*2\r\n$6\r\nCLIENT\r\n$4\r\nLIST\r\n

# CLIENT SETNAME myapp
*3\r\n$6\r\nCLIENT\r\n$7\r\nSETNAME\r\n$5\r\nmyapp\r\n

# CONFIG GET maxmemory
*3\r\n$6\r\nCONFIG\r\n$3\r\nGET\r\n$10\r\nmaxmemory\r\n

# ACL WHOAMI
*2\r\n$3\r\nACL\r\n$6\r\nWHOAMI\r\n

子命令在协议层面就是数组中的普通参数,Redis 通过两层分派(主命令 + 子命令)来处理。


4.6 大小写敏感的参数

虽然命令名不区分大小写,但以下参数区分大小写

参数正确错误
Key 名称myKeymykey(不同的 key)
字段名userNameusername(不同的字段)
Hellohello(不同的值)
EX/NX/XXEXex(通常不区分,但建议大写)

4.7 命令编码实战

Python 实现

def encode_resp_command(*args) -> bytes:
    """
    将命令参数编码为 RESP 格式

    参数可以是 str 或 bytes:
    - str: 自动编码为 UTF-8
    - bytes: 直接使用(二进制安全)
    """
    parts = []

    # 数组头
    parts.append(f"*{len(args)}\r\n".encode())

    for arg in args:
        if isinstance(arg, str):
            arg = arg.encode("utf-8")
        # 长度前缀 + 内容 + CRLF
        parts.append(f"${len(arg)}\r\n".encode())
        parts.append(arg)
        parts.append(b"\r\n")

    return b"".join(parts)


# 测试
assert encode_resp_command("PING") == b"*1\r\n$4\r\nPING\r\n"
assert encode_resp_command("GET", "mykey") == b"*2\r\n$3\r\nGET\r\n$5\r\nmykey\r\n"
assert encode_resp_command("SET", "key", "value") == \
    b"*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n"

Go 实现

package resp

import (
    "fmt"
    "strings"
)

func EncodeCommand(args ...string) string {
    var sb strings.Builder

    // 数组头
    sb.WriteString(fmt.Sprintf("*%d\r\n", len(args)))

    for _, arg := range args {
        sb.WriteString(fmt.Sprintf("$%d\r\n", len(arg)))
        sb.WriteString(arg)
        sb.WriteString("\r\n")
    }

    return sb.String()
}

// 二进制安全版本
func EncodeCommandBytes(args ...[]byte) []byte {
    parts := [][]byte{
        []byte(fmt.Sprintf("*%d\r\n", len(args))),
    }

    for _, arg := range args {
        parts = append(parts, []byte(fmt.Sprintf("$%d\r\n", len(arg))))
        parts = append(parts, arg)
        parts = append(parts, []byte("\r\n"))
    }

    return bytes.Join(parts, nil)
}

Rust 实现

fn encode_command(args: &[&[u8]]) -> Vec<u8> {
    let mut buf = Vec::new();

    // 数组头
    buf.extend_from_slice(format!("*{}\r\n", args.len()).as_bytes());

    for arg in args {
        buf.extend_from_slice(format!("${}\r\n", arg.len()).as_bytes());
        buf.extend_from_slice(arg);
        buf.extend_from_slice(b"\r\n");
    }

    buf
}

fn main() {
    let cmd = encode_command(&[b"SET", b"key", b"value"]);
    assert_eq!(cmd, b"*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n");
}

4.8 二进制安全验证

RESP 的 Bulk String 支持任意二进制数据。让我们验证这个特性:

import socket

def send_raw(sock, data):
    """发送原始 RESP 数据"""
    sock.sendall(data)

def recv_all(sock):
    """接收所有可用数据"""
    chunks = []
    while True:
        chunk = sock.recv(4096)
        if not chunk:
            break
        chunks.append(chunk)
    return b"".join(chunks)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("127.0.0.1", 6379))

# 设置一个包含特殊字节的值
# 值包含:null 字节(0x00)、换行(0x0A)、回车(0x0D)、高位字节(0xFF)
value = b"\x00\x0A\x0D\xFF\x00hello\r\nworld\x00"

cmd = b"*3\r\n$3\r\nSET\r\n$7\r\nbinkey\r\n"
cmd += f"${len(value)}\r\n".encode()
cmd += value + b"\r\n"

send_raw(s, cmd)
resp = s.recv(1024)
print(f"SET response: {resp}")  # +OK

# 读取回来
s.sendall(b"*2\r\n$3\r\nGET\r\n$7\r\nbinkey\r\n")
resp = s.recv(1024)
print(f"GET response: {resp}")
# $17\r\n\x00\n\r\xff\x00hello\r\nworld\x00\r\n

s.close()

验证结果:值中的 \r\n\x00\xFF 等特殊字节都能正确存储和读取,证明了 RESP 的二进制安全性。


4.9 命令执行流程(服务端视角)

1. 接收 TCP 数据 → processInputBuffer()
2. 判断格式(RESP / Inline)
3. 解析为参数数组 → argv[], argc
4. 查找命令 → lookupCommand(argv[0])
5. 权限检查 → ACL 检查
6. 参数校验 → 验证参数数量、类型
7. 执行命令 → cmd->proc(c)
8. 传播命令 → propagate()(用于主从复制和 AOF)
9. 返回响应 → addReply*()

命令传播

命令在执行后可能需要传播到:

  • AOF:持久化日志
  • 从节点:主从复制
  • Sentinel:监控通知

传播的格式与原始 RESP 命令相同。这就是为什么 RESP 协议的设计如此重要——它不仅是网络传输格式,也是持久化和复制的格式。


4.10 特殊命令格式

SELECT(切换数据库)

*2\r\n$6\r\nSELECT\r\n$1\r\n0\r\n

AUTH(认证)

# 单参数(Redis < 6.0)
*2\r\n$4\r\nAUTH\r\n$8\r\npassword\r\n

# 双参数(Redis 6.0+,支持 ACL)
*3\r\n$4\r\nAUTH\r\n$8\r\nusername\r\n$8\r\npassword\r\n

MULTI/EXEC(事务)

*1\r\n$5\r\nMULTI\r\n
+QUEUED

*2\r\n$3\r\nSET\r\n$4\r\nkey1\r\n$6\r\nvalue1\r\n
+QUEUED

*2\r\n$3\r\nSET\r\n$4\r\nkey2\r\n$6\r\nvalue2\r\n
+QUEUED

*1\r\n$4\r\nEXEC\r\n
*2
+OK
+OK

EVAL(Lua 脚本)

# EVAL "return 1" 0
*3\r\n$4\r\nEVAL\r\n$9\r\nreturn 1\r\n$1\r\n0\r\n

# EVAL "return redis.call('GET', KEYS[1])" 1 mykey
*4\r\n$4\r\nEVAL\r\n$38\r\nreturn redis.call('GET', KEYS[1])\r\n$1\r\n1\r\n$5\r\nmykey\r\n

4.11 注意事项

⚠️ 命令名是第一个参数 RESP 数组的第一个元素是命令名,其余是参数。*3\r\n 表示有 3 个参数(1 个命令名 + 2 个参数)。

⚠️ 不要手动拼接 RESP 字符串 手动拼接容易出错(如长度计算、CRLF 遗漏)。建议使用经过测试的编码函数。

⚠️ 内联命令不支持二进制数据 如果参数包含空格、换行或二进制字节,必须使用 RESP 格式。

⚠️ 命令长度限制 默认情况下,Redis 限制单个命令的大小为 proto-max-bulk-len(默认 512MB)。超过此限制会返回错误。


4.12 扩展阅读

资源说明
Redis 命令参考所有命令的参数和返回值
Redis 源码 server.c命令表定义
Redis 源码 networking.c协议解析核心

上一章:RESP3 新类型 | 下一章:Pipeline 管道机制