redis项目之命令解析器

目录

一、redis官方做法

二、我们的做法

[1. 核心数据结构](#1. 核心数据结构)

[2. 解耦后的职责划分](#2. 解耦后的职责划分)

[3. 数据流向](#3. 数据流向)

三、命令解析器校验(所有命令都要)

[1. 基础格式校验(RESP 协议层面)](#1. 基础格式校验(RESP 协议层面))

[2. 命令存在性校验](#2. 命令存在性校验)

[3. 参数个数合法性校验(基于 arity 字段)](#3. 参数个数合法性校验(基于 arity 字段))

[4. 权限校验(ACL / 禁用命令)](#4. 权限校验(ACL / 禁用命令))

[5. 状态校验(服务器 / 客户端状态)](#5. 状态校验(服务器 / 客户端状态))

[6. 内存 / 资源校验(可选)](#6. 内存 / 资源校验(可选))

[7. 命令标志前置校验(基于 sflags)](#7. 命令标志前置校验(基于 sflags))

[8. 集群模式专属校验](#8. 集群模式专属校验)


一、redis官方做法

cpp 复制代码
// Redis 官方源码简化版(src/redis.h)
struct redisCommand {
    char *name;             // 命令名(比如 "SET"、"GET")
    redisCommandProc *proc; // 函数指针:指向该命令的处理函数
    int arity;              // 参数个数(-N 表示至少 N 个参数)
    char *sflags;           // 字符串形式的标志位(比如 "w" 表示写命令)
    int flags;              // 二进制形式的标志位(由 sflags 转换而来)
    // ... 其他字段(比如命令分类、统计信息等)
};

// 命令处理函数的类型定义
typedef void redisCommandProc(redisClient *c);
cpp 复制代码
// 源码位置:src/redis.h(简化核心字段)
typedef struct client {
    int fd;                 // 客户端的 socket 文件描述符
    robj *name;             // 客户端名字(CLIENT SETNAME 设置)
    
    // 输入/输出缓冲区
    sds querybuf;           // 输入缓冲区:存储客户端发来的原始字节流
    size_t qb_pos;          // 输入缓冲区的当前解析位置
    list *reply;            // 输出缓冲区链表:存储要发回客户端的 RESP 数据
    size_t sentlen;         // 已发送的字节数
    
    // 命令相关
    int argc;               // 当前命令的参数个数
    robj **argv;            // 当前命令的参数数组(argv[0] 是命令名)
    struct redisCommand *cmd; // 当前命令对应的 redisCommand 结构体指针
    
    // 数据库相关
    redisDb *db;            // 当前选中的数据库指针
    int dictid;             // 当前数据库的 ID
    
    // 其他状态(事务、阻塞、复制等)
    // ... 省略大量字段
} client;
cpp 复制代码
客户端发来 TCP 字节流
    ↓
存入 client->querybuf(输入缓冲区)
    ↓
processInputBuffer() 开始解析
    ↓
【步骤1】RESP 协议解析:把 querybuf 解析成 argc/argv
    ↓
【步骤2】命令名转小写:argv[0] 转成小写(Redis 命令不区分大小写)
    ↓
【步骤3】哈希表 O(1) 查找:在 commands 字典里找命令名对应的 redisCommand*
    ↓
【步骤4】参数校验:检查 argc 是否符合 arity 的要求
    ↓
【步骤5】权限/状态校验:检查命令标志位(如是否是写命令、是否 OOM)
    ↓
找到处理函数,准备执行

总结:就是一开始网络库读取上来的内容,存入client结构体,后续把这个结构体传给命令分配器,命令分配器在一开始服务器初始化的时候,每个命令都注册在了静态数组struct redisCommand redisCommandTable[] ,然后服务器初始化的时候把命令都转换成哈希存储便于O(1)查找,命令分配器根据注册的命令和client结构体里的信息对比,然后看一下是否允许执行命令,如果允许就执行,然后把结果写入到client结构体输出缓冲区,命令分配器 "生产" RESP 结果,写入输出缓冲区;网络层 "消费" 输出缓冲区,通过网络发送

二、我们的做法

整个架构通过命令分配器的学习就可以串通起来

cpp 复制代码
1. 用户在 Web 前端点击"保存数据"按钮
    ↓
2. Web 前端发 HTTP POST 请求:POST /save_data,Body 是 {"key":"user1","value":"zhangsan"}
    ↓
3. Web 后端(muduo)的 HTTP 解析器解析请求,拿到 method、path、body
    ↓
4. 业务逻辑层判断:这是"操作数据"的请求
    ↓
5. 协议转换层:HTTP → RESP,封装成 RESP 命令:
   *3\r\n$3\r\nSET\r\n$5\r\nuser1\r\n$8\r\nzhangsan\r\n
    ↓
6. mini-redis 客户端通过 TCP 把 RESP 命令发给 mini-redis 服务器
    ↓
7. mini-redis 服务器处理命令:
   a. 解析 RESP 命令
   b. 调用命令分配器
   c. 调用 g_store.set("user1", "zhangsan")
   d. 生成 RESP 结果:+OK\r\n
    ↓
8. mini-redis 服务器把 RESP 结果发回 Web 后端的 mini-redis 客户端
    ↓
9. 协议转换层:RESP → HTTP,把 +OK\r\n 转换成 JSON:{"status":"OK"}
    ↓
10. Web 后端把 JSON 写入 HTTP 响应,发回 Web 前端
    ↓
11. Web 前端收到响应,提示"保存成功"
方式 架构 通信方式 适用场景
同进程部署 mini-redis 的核心存储(KeyValueStore)直接嵌入 muduo Web 后端进程 直接函数调用(不用网络、不用 RESP) 学习项目、小型项目、快速验证
跨进程部署(生产级) mini-redis 是独立服务器进程,muduo Web 后端是另一个进程 TCP + RESP 协议(网络通信) 生产环境、需要解耦、需要多客户端连接

一、整体框架梳理

1. 核心数据结构

结构 作用 对应 Redis 官方
CommandHandler 命令处理函数的类型(函数指针的 C++ 版本) redisCommandProc
CommandInfo 命令元信息(名字、处理函数、参数个数、标志位) redisCommand
CommandDispatcher 命令分配器核心类(封装命令表、哈希表索引、注册 / 分发逻辑) 无(Redis 是全局函数 + 全局哈希表,我封装成了类)

2. 解耦后的职责划分

模块 职责
CommandDispatcher 命令注册、哈希表索引、命令查找、参数校验、权限检查、调用处理函数
独立的命令处理函数(handleSet/handleGet 等) 具体命令的业务逻辑(调用 KeyValueStore、记录 AOF、广播复制)
网络层(server.cpp 只负责收发数据、RESP 解析,不关心命令逻辑

3. 数据流向

复制代码
网络层 → RespParser → RespValue
    ↓
CommandDispatcher::dispatch()
    ↓
【步骤1】命令名转小写
【步骤2】哈希表 O(1) 查找 CommandInfo
【步骤3】参数个数校验
【步骤4】调用对应的命令处理函数
    ↓
命令处理函数 → 调用 KeyValueStore/AofLogger/Rdb
    ↓
返回 RESP 字符串 → 网络层写入输出缓冲
cpp 复制代码
main() 函数
    ↓
【步骤1】实例化核心组件
    - KeyValueStore g_store;      // 存储引擎
    - AofLogger g_aof;            // AOF 持久化
    - Rdb g_rdb;                  // RDB 持久化
    ↓
【步骤2】初始化 AOF/RDB
    - g_aof.init(config.aof, err);
    - g_rdb.load(g_store, err);
    ↓
【步骤3】实例化 CommandDispatcher(把核心组件传进去,存成成员变量)
    - CommandDispatcher dispatcher(g_store, g_aof, g_rdb);
    ↓
【步骤4】启动服务器,进入事件循环
    - 网络层收到数据 → 解析 RESP → 调用 dispatcher.dispatch(v, raw)
    - dispatch() 函数里:
      a. 查找命令
      b. 调用对应的处理函数(比如 handleSet())
      c. 处理函数直接用 this->store_/this->aof_/this->rdb_(成员变量)

三、命令解析器校验(所有命令都要)

这些校验在找到命令处理函数前 执行,是所有命令的通用规则,由 processCommand 函数(server.c)统一处理,核心包含 8 类关键校验:

1. 基础格式校验(RESP 协议层面)

  • 校验客户端请求是否为 RESP 数组类型 (Redis 命令必须是数组,如 ["GET", "name"]);
  • 校验数组非空(至少包含命令名);
  • 校验数组元素均为字符串类型(命令名 / 参数必须是 Bulk String)。

2. 命令存在性校验

  • 将命令名转为小写后,在 redisCommandTable 中查找对应的 redisCommand 结构体;
  • 找不到则返回 ERR unknown command '<cmd>'(如 ERR unknown command 'GETT')。

3. 参数个数合法性校验(基于 arity 字段)

arity 是 Redis 命令的核心参数规则,取值规则:

arity 含义 示例
N(正数) 必须恰好 N 个参数 arity=2GET 必须是 2 个参数(GET key
-N(负数) 至少 N 个参数 arity=-3SET 至少 3 个参数(SET key value
0 无参数 仅特殊命令(如 PING

校验逻辑:

复制代码
// 简化版源码逻辑
if (c->argc < cmd->arity || (cmd->arity > 0 && c->argc != cmd->arity)) {
    addReplyErrorFormat(c, "wrong number of arguments for '%s' command", cmd->name);
    return C_OK;
}

示例:GETarity=-2,若传入 GET(1 个参数),直接返回 ERR wrong number of arguments for 'get' command

4. 权限校验(ACL / 禁用命令)

  • 校验当前客户端是否有该命令的执行权限(ACL 规则);
  • 校验命令是否被配置禁用(如 rename-command 重命名 / 禁用);
  • 无权限则返回 ERR ACL deniedERR command '<cmd>' is disabled

5. 状态校验(服务器 / 客户端状态)

  • 校验服务器是否处于只读模式(如主从复制中从节点),写命令(SET/DEL)直接拒绝;
  • 校验客户端是否处于事务 / 订阅等特殊状态(如订阅状态下仅允许 PUBLISH/UNSUBSCRIBE 等命令);
  • 状态非法则返回对应错误(如 ERR READONLY You can't write against a read-only replica)。

6. 内存 / 资源校验(可选)

  • 校验服务器内存是否达到上限(maxmemory),且淘汰策略无法释放内存时,拒绝写命令;
  • 校验客户端缓冲区是否溢出(防止恶意占用内存)。

7. 命令标志前置校验(基于 sflags

  • sflags 中的标志位提前过滤非法场景:
    • r(读命令):主从复制中从节点允许执行;
    • w(写命令):只读模式下拒绝;
    • m(多键命令):集群模式下校验键是否在同一槽位;
    • a(管理命令):仅允许管理员执行。

8. 集群模式专属校验

  • 若为集群部署,校验命令涉及的所有键是否在同一哈希槽 (如 DEL key1 key2 需 key1/key2 槽位相同);
  • 跨槽则返回 ERR CROSSSLOT Keys in request don't hash to the same slot
相关推荐
老纪5 小时前
如何解决OUI图形界面无法调用_xhost与DISPLAY变量设置
jvm·数据库·python
TDengine (老段)5 小时前
TDengine 一条 SQL 从客户端到执行完成的全链路
大数据·数据库·sql·物联网·时序数据库·tdengine·涛思数据
それども5 小时前
怎么理解 LEFT JOIN 和 LEFT SEMI JOIN
java·数据库·mysql
rGzywSmDg5 小时前
如何在Dev-C++中设置TDM-GCC为默认编译器
开发语言·c++
qxwlcsdn6 小时前
CSS如何实现元素镜像翻转_使用transformscalex负值
jvm·数据库·python
2301_803934616 小时前
mysql如何处理大量重复值索引_mysql索引存储特征分析
jvm·数据库·python
jran-6 小时前
MySQL 用户与权限
数据库·mysql
csdn_aspnet6 小时前
C++ Lomuto分区算法(Lomuto Partition Algorithm)
开发语言·c++·算法