目录
[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 结果,写入输出缓冲区;网络层 "消费" 输出缓冲区,通过网络发送
二、我们的做法
整个架构通过命令分配器的学习就可以串通起来
cpp1. 用户在 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++ 版本) redisCommandProcCommandInfo命令元信息(名字、处理函数、参数个数、标志位) redisCommandCommandDispatcher命令分配器核心类(封装命令表、哈希表索引、注册 / 分发逻辑) 无(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 字符串 → 网络层写入输出缓冲
cppmain() 函数 ↓ 【步骤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=2→GET必须是 2 个参数(GET key)-N(负数)至少 N 个参数 arity=-3→SET至少 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; }示例:
GET的arity=-2,若传入GET(1 个参数),直接返回ERR wrong number of arguments for 'get' command。4. 权限校验(ACL / 禁用命令)
- 校验当前客户端是否有该命令的执行权限(ACL 规则);
- 校验命令是否被配置禁用(如
rename-command重命名 / 禁用);- 无权限则返回
ERR ACL denied或ERR 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。