vbnet
================================================================================
JRedisX 项目骨架搭建指南
从零开始 --- 手把手教你搭
================================================================================
【文档说明】
本文档只告诉你"怎么做",不替你写代码。
按步骤执行,你亲手把骨架搭起来,才能真正理解每一层的关系。
================================================================================
第一章 模块划分思路
================================================================================
1.1 为什么要分 9 个模块?
不分模块,所有代码堆在一个 jar 里,后期维护就是灾难。
按职责拆分,好处:
• 每个模块职责单一,边界清晰
• 可以独立编译、独立测试
• 后期替换实现(比如把内存存储换成 RocksDB)只改一个模块
• 新人看代码,先看 common + protocol 就能入门
1.2 模块总览
┌──────────────────┬──────────────────────────────┬──────────┐
│ 模块名 │ 职责 │ 优先级 │
├──────────────────┼──────────────────────────────┼──────────┤
│ jredisx-common │ 公共类:RedisMessage、工具类 │ Phase 1 │
│ jredisx-protocol │ RESP 编解码器 │ Phase 1 │
│ jredisx-command │ 命令接口 + 命令注册表 + 具体命令│ Phase 1 │
│ jredisx-server │ Netty 启动入口 + 配置 + 路由 │ Phase 1 │
│ jredisx-storage │ 存储引擎接口 + 内存数据结构 │ Phase 2 │
│ jredisx-router │ Slot 计算 + 线程绑定 + 路由 │ Phase 2 │
│ jredisx-persistence│ AOF/RDB 持久化 │ Phase 3 │
│ jredisx-replication│ 主从复制 │ Phase 4 │
│ jredisx-ha │ Sentinel + Cluster │ Phase 4 │
└──────────────────┴──────────────────────────────┴──────────┘
1.3 依赖关系(谁依赖谁)
原则:上层依赖下层,下层不依赖上层。
common(最底层,谁都不依赖)
↑
protocol(依赖 common)
↑
storage(依赖 common)
↑
command(依赖 common + storage)
↑
router(依赖 common)
↑
persistence(依赖 common)
↑
replication(依赖 common + protocol)
↑
ha(依赖 common + protocol)
↑
server(依赖以上所有 + Netty)
画成图:
┌─────────────┐
│ server │ ← 唯一入口,聚合所有模块
└──────┬──────┘
┌───────────────┼───────────────┐
↓ ↓ ↓
┌─────────┐ ┌──────────┐ ┌──────────┐
│ command │ │ router │ │ protocol │
└────┬────┘ └──────────┘ └────┬─────┘
↓ ↓
┌─────────┐ ┌──────────┐
│ storage │ │ common │
└────┬────┘ └──────────┘
↓
┌──────────┐
│persistence│
│replication│
│ ha │
└──────────┘
================================================================================
第二章 Maven 多模块搭建步骤
================================================================================
2.1 创建根项目
步骤 1:新建空目录
mkdir JRedisX
cd JRedisX
步骤 2:创建根 pom.xml
这个 pom 只做三件事:
1. 声明 packaging = pom
2. 在 <modules> 里列出 9 个子模块
3. 在 <dependencyManagement> 里统一版本(Netty、JCTools、SLF4J)
关键配置:
• JDK 版本:17(用 ZGC 需要 17+)
• Netty 版本:4.1.110.Final(当前最新稳定版)
• JCTools 版本:4.0.5
• 编码:UTF-8
步骤 3:创建子模块目录
每个子模块一个目录,目录名 = artifactId:
mkdir jredisx-common jredisx-protocol jredisx-storage
mkdir jredisx-command jredisx-router jredisx-persistence
mkdir jredisx-replication jredisx-ha jredisx-server
2.2 每个子模块的 pom.xml 怎么写
模板结构(以 jredisx-common 为例):
<parent>
<groupId>com.jredisx</groupId>
<artifactId>jredisx-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>jredisx-common</artifactId>
依赖规则:
• common:只依赖 slf4j + logback + junit(测试)
• protocol:依赖 common + netty-buffer + netty-codec
• storage:依赖 common
• command:依赖 common + storage
• server:依赖以上所有 + netty-transport + netty-handler
额外加 maven-shade-plugin,打包成可执行 jar
特别注意 server 模块的 shade 插件:
作用:把所有依赖打到一个 fat-jar 里,java -jar 直接运行
配置:指定 mainClass = com.jredisx.server.JRedisXServer
2.3 目录结构规范
每个子模块必须有的目录:
src/main/java/com/jredisx/xxx/ ← 源码
src/main/resources/ ← 配置文件
src/test/java/ ← 测试(Phase 1 可以先空着)
包名规范:
com.jredisx.common → jredisx-common
com.jredisx.protocol → jredisx-protocol
com.jredisx.command → jredisx-command
com.jredisx.server → jredisx-server
以此类推
================================================================================
第三章 Phase 1 骨架内容
================================================================================
Phase 1 目标:服务器能启动,redis-cli 能连上,PING 返回 PONG。
3.1 第一步:common 模块 --- 定义公共消息对象
你需要自己写一个类:com.jredisx.common.RedisMessage
设计思路:
• RESP 协议有 5 种基本类型:SimpleString、Error、Integer、BulkString、Array
• 这个类就是"协议消息的对象化表示"
• 每种类型对应一个枚举值 + 一个 data 字段(Object 类型)
• 提供工厂方法:simpleString("OK")、error("ERR xxx")、integer(123)、bulkString(byte[])、array(List)
• 提供 getter:getType()、getString()、getBytes()、getInteger()、getArray()
• 提供便捷方法:ok()、pong()、nullBulk()、wrongTypeError()
为什么用 Object data?
因为不同类型存储的数据不同:String 存 String,Integer 存 Long,
BulkString 存 byte[],Array 存 List<RedisMessage>。
用 Object 是最简单的方式,后期可以优化为更具体的类型。
3.2 第二步:protocol 模块 --- RESP 编解码器
你需要写两个类:
com.jredisx.protocol.RESPDecoder ← 解码器(ByteBuf → RedisMessage)
com.jredisx.protocol.RESPEncoder ← 编码器(RedisMessage → ByteBuf)
RESPDecoder 设计思路:
• 继承 Netty 的 ReplayingDecoder,它帮你处理"数据不够时暂停"的逻辑
• 状态机:DECODE_TYPE → DECODE_BULK_CONTENT → DECODE_TYPE
• 读取第一个字节判断类型:
'+' → SimpleString,读到 \r\n 为止
'-' → Error,读到 \r\n 为止
':' → Integer,读到 \r\n 转 long
'$' → BulkString,先读长度,再读内容
'*' → Array,先读元素个数,再递归解析每个元素
• 辅助方法:readLine() 读到 \r\n,readLineAsInt() 读到 \r\n 转 int
RESPEncoder 设计思路:
• 继承 Netty 的 MessageToByteEncoder<RedisMessage>
• switch (msg.getType()),每种类型按 RESP 格式写入 ByteBuf
• SimpleString: +content\r\n
• Error: -content\r\n
• Integer: :value\r\n
• BulkString: $length\r\ncontent\r\n
• Array: *count\r\n + 递归编码每个元素
• NullBulk: $-1\r\n
编码细节:
• 字符串统一用 UTF-8
• BulkString 的 content 是原始 byte[],不做编码转换
• CRLF = \r\n = 0x0D 0x0A
3.3 第三步:command 模块 --- 命令体系
你需要写 4 个类:
com.jredisx.command.RedisCommand ← 接口
com.jredisx.command.RedisContext ← 执行上下文
com.jredisx.command.CommandRegistry ← 命令注册表
com.jredisx.command.PingCommand ← PING 命令实现
com.jredisx.command.EchoCommand ← ECHO 命令实现
RedisCommand 接口:
只有一个方法:RedisMessage execute(RedisContext ctx, List<RedisMessage> args)
args[0] 是命令名本身(如 "PING"),args[1] 开始是参数
RedisContext 设计:
• Phase 1 只需要 clientId(Channel 的唯一标识)
• 预留字段:inTransaction、transactionQueue、watchedKeys(Phase 5 用)
• 后续扩展:getDatabase() 获取当前线程的数据库实例
CommandRegistry 设计:
• 内部用 ConcurrentHashMap<String, RedisCommand>
• register(name, command):命令名转大写后存入
• get(name):命令名转大写后查找
• 线程安全:ConcurrentHashMap 天然线程安全
PingCommand 实现:
• 如果 args.size() == 1,返回 PONG
• 如果 args.size() > 1,返回 args[1] 的内容(BulkString)
EchoCommand 实现:
• 如果 args.size() < 2,返回错误:ERR wrong number of arguments
• 否则返回 args[1] 的内容
3.4 第四步:server 模块 --- 启动入口
你需要写 4 个类 + 2 个配置文件:
com.jredisx.server.JRedisXServer ← main 入口
com.jredisx.server.ServerConfig ← 配置类
com.jredisx.server.CommandRouter ← 命令路由
com.jredisx.server.RedisServerHandler ← Netty ChannelHandler
src/main/resources/jredisx.conf ← 配置文件
src/main/resources/logback.xml ← 日志配置
JRedisXServer 设计:
• 持有 ServerConfig、EventLoopGroup(boss, worker)
• start() 方法:
1. 创建 CommandRegistry,注册 PING、ECHO
2. 创建 CommandRouter
3. 配置 ServerBootstrap:
- group(bossGroup, workerGroup)
- channel(NioServerSocketChannel)
- childHandler:pipeline 顺序 = RESPDecoder → RESPEncoder → RedisServerHandler
4. bind(port).sync()
5. 等待 closeFuture
• shutdown() 方法:优雅关闭两个 EventLoopGroup
• main 方法:创建 ServerConfig,new JRedisXServer(config).start()
ServerConfig 设计:
• Phase 1 只需要:port(6379)、bind("0.0.0.0")、maxclients(10000)、ioThreads(1)
• 后续扩展:所有配置项都放这里,支持从文件加载
CommandRouter 设计:
• 持有 CommandRegistry
• route(channelCtx, request) 方法:
1. 检查 request 类型必须是 ARRAY
2. 取 args[0] 作为命令名,转大写
3. 从 registry 查找命令
4. 找不到返回:ERR unknown command 'xxx'
5. 找到则创建 RedisContext,执行 command.execute()
6. 捕获异常,返回 ERR + 异常信息
• Phase 1 直接在当前线程执行(同步)
• Phase 2 改为异步:放入 CommandThread 队列
RedisServerHandler 设计:
• 继承 ChannelInboundHandlerAdapter
• 持有 CommandRouter
• channelActive:打印客户端连接日志
• channelInactive:打印客户端断开日志
• channelRead:收到 RedisMessage,调用 router.route(),writeAndFlush 结果
• exceptionCaught:打印错误,返回 ERR,关闭连接
Netty Pipeline 顺序(重要):
[RESPDecoder] → [RESPEncoder] → [RedisServerHandler]
• Decoder 在前:先解析字节流为对象
• Encoder 在后:把对象序列化为字节流发出去
• Handler 在中间:处理业务逻辑
3.5 第五步:编译运行验证
编译命令:
mvn clean package -DskipTests
运行命令:
java -jar jredisx-server/target/jredisx-server-1.0.0-SNAPSHOT.jar
验证命令(另开终端):
redis-cli -p 6379 PING → 应返回 PONG
redis-cli -p 6379 ECHO hello → 应返回 "hello"
redis-cli -p 6379 GET key → 应返回 ERR unknown command 'GET'
如果 PING 通了就说明:
• Netty 监听正常
• RESP 编解码正常
• 命令路由正常
• 命令执行正常
================================================================================
第四章 关键设计决策说明
================================================================================
4.1 为什么用 ReplayingDecoder 而不是 ByteToMessageDecoder?
ByteToMessageDecoder 需要手动处理"数据不够"的情况(return 等下次)。
ReplayingDecoder 内部会抛出异常来暂停解析,数据够了自动恢复。
对于 RESP 这种变长协议,ReplayingDecoder 代码更简洁。
4.2 为什么 RedisMessage 的 data 用 Object 而不是泛型?
因为一条消息的具体类型在运行时才确定。
用泛型会写成 RedisMessage<T>,但 Array 类型里每个元素类型都不同,
泛型嵌套会爆炸。Object + 工厂方法是最务实的选择。
4.3 为什么 CommandRegistry 用 ConcurrentHashMap?
虽然 Phase 1 是单线程,但注册发生在启动时,查找发生在运行时。
用 ConcurrentHashMap 是为 Phase 2 多线程做准备,
同时也是为了支持 Module 热加载(运行时动态注册命令)。
4.4 为什么 server 模块要加 maven-shade-plugin?
因为 server 依赖了 protocol、command 等模块,还有 Netty。
运行时需要所有 jar 都在 classpath 里。
shade 插件把所有依赖打到一个 jar 里,java -jar 就能直接跑,
不需要写复杂的 -cp 参数。
4.5 为什么先不实现存储?
Phase 1 的目标是"协议通、命令跑通"。
存储是 Phase 2 的事。现在 PING/ECHO 不需要存储。
等 GET/SET 命令时再引入 storage 模块。
================================================================================
第五章 常见问题排查
================================================================================
Q1: mvn clean package 报错 "找不到符号 RedisMessage"
A: 先编译 common 模块:cd jredisx-common && mvn install
或者从根目录执行 mvn clean install(install 会把 jar 装到本地仓库)
Q2: 运行后 redis-cli 连不上
A: 检查:
1. 端口是否被占用:lsof -i :6379
2. 防火墙是否放行
3. 日志是否显示 "Server started"
4. redis-cli 版本是否支持 RESP2(redis-cli 5.0+ 都可以)
Q3: PING 返回乱码或格式不对
A: 检查 RESPEncoder:
• SimpleString 是否写了 '+' 前缀
• 是否写了 \r\n 结尾
• 有没有多写或少写换行
用 telnet localhost 6379 手动输入 "PING\r\n" 看原始返回
Q4: 中文显示乱码
A: 确保所有字符串都用 StandardCharsets.UTF_8
redis-cli 默认也是 UTF-8,如果不对加 --raw 参数
Q5: 怎么调试 Netty 的编解码?
A: 在 Pipeline 最前面加 LoggingHandler:
pipeline.addFirst(new LoggingHandler(LogLevel.DEBUG));
这样能看到原始字节流
================================================================================
第六章 Phase 1 完成 checklist
================================================================================
□ 根 pom.xml 创建完成,9 个子模块声明正确
□ 每个子模块 pom.xml 创建完成,依赖关系正确
□ common 模块:RedisMessage 类编译通过
□ protocol 模块:RESPDecoder + RESPEncoder 编译通过
□ command 模块:RedisCommand 接口 + CommandRegistry + PING/ECHO 编译通过
□ server 模块:JRedisXServer + ServerConfig + CommandRouter + RedisServerHandler 编译通过
□ mvn clean package 成功生成 fat-jar
□ java -jar 启动成功,日志显示 "Server started on 0.0.0.0:6379"
□ redis-cli PING 返回 PONG
□ redis-cli ECHO hello 返回 "hello"
□ 代码提交到 Git:git add . && git commit -m "Phase 1: skeleton + PING/ECHO"
□ 推送到 Gitee:git push origin master
================================================================================
第七章 下一步(Phase 1 后续)
================================================================================
Phase 1 骨架跑通后,按这个顺序继续:
1. 实现内存存储(HashMap<byte[], RedisObject>)
2. 实现 String 命令:GET、SET、DEL、EXISTS
3. 实现 EXPIRE / TTL(过期时间管理)
4. 实现 INFO 命令(返回服务器信息)
5. 写单元测试(RESP 编解码测试、命令测试)
6. JMH 压测 PING 命令 QPS
每完成一个功能就 commit 一次,保持提交历史清晰。
================================================================================
文档结束
================================================================================