JRedisX 项目骨架搭建指南 从零开始 — 手把手教你搭

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 一次,保持提交历史清晰。

================================================================================
                              文档结束
================================================================================
相关推荐
zx1154508 小时前
Java 反射 (SpringBoot篇)
java·架构
2301_794461578 小时前
Activiti/BPMN 2.0 的 4 种网关
java·服务器·开发语言
她的男孩8 小时前
低代码只能做单表 CRUD?我们一行代码没写,搭了个完整进销存
java·后端·架构
147API8 小时前
Claude Tag 进入 Slack 后,团队智能体需要哪些任务与审计字段
java·开发语言·数据库
dyonggan9 小时前
IDEA从零搭建SpringCloud Alibaba完整工程
java·spring cloud·intellij-idea
代码雕刻家9 小时前
编程语法细节
java·c语言·开发语言
SimonKing9 小时前
Agnes AI出桌面版了,可图可视频,免费用
java·后端·程序员
左左右右左右摇晃9 小时前
手写Tomcat原理整理
java·开发语言·笔记·tomcat
孫治AllenSun9 小时前
【JVM】四大引用类型分析
java·开发语言·jvm