Files
schemaCheck/docs/实施方案.md
dongzi 114b053733
All checks were successful
Redis序列化结构检查 / redis-schema-check (push) Has been skipped
feat: 1、流水线总开关配置 2、删除p0p1p2级别阻断
2026-07-14 10:58:05 +08:00

20 KiB
Raw Blame History

Redis 序列化结构变更检测 — 实施方案

版本v0.1
日期2026-07-13
技术栈Java 11 + Maven + JavaParser
目标仓库:redisCheck(工具) / jnpf-java-cloud(被检测业务仓库)


1. 背景与目标

1.1 背景

业务仓库 jnpf-java-cloud 是多模块 Java 微服务项目,广泛使用 Redis 缓存业务对象。当开发者修改 VO/DTO 字段、调整序列化包装结构、或变更 Redis 写入逻辑时,线上 Redis 中可能仍存在旧结构数据,导致:

  • 反序列化失败
  • 字段读取为空
  • 新旧结构并存引发隐蔽 Bug

典型变更示例(租户库信息缓存):

变更前(逻辑上等价于直接缓存 TenantVO

{
  "dbName": "",
  "linkList": [{ "id": "", "serviceName": "", "...": "..." }]
}

变更后(TenantDbContentCacheHelper 包装为 CacheEnvelope

{
  "vo": {
    "dbName": "",
    "linkList": [{ "id": "", "serviceName": "", "...": "..." }]
  },
  "expiresAtMs": 0
}

该变更在业务代码中已有真实对应:

  • Keytenant:db:content:{encode}
  • 写入类:jnpf.util.TenantDbContentCacheHelper#cacheSuccess
  • Value 类型:CacheEnvelope { TenantVO vo; Long expiresAtMs; }

1.2 目标

push 时 自动执行检测:

  1. 对比两次提交(old-sha vs new-sha)之间的代码差异
  2. 识别 Redis value 序列化结构是否发生变更
  3. 生成结构化变更报告
  4. 通过企微机器人发送通知
  5. 通过开关控制 仅通知阻断流水线

1.3 非目标(第一版不做)

  • 不连接真实 Redis 实例做运行时校验
  • 不扫描 Maven 依赖 jar 中的类(仅分析业务仓库源码)
  • 不做全量历史扫描(仅 diff 触发)
  • 不替代 CodeChecker / AI Code Review 等现有能力

2. 业务仓库调研结论

基于对 jnpf-java-cloud 的静态检索,得出以下判断(作为方案输入,不再向业务方重复确认):

2.1 Redis 序列化方式

类型 出现频率 第一版策略
JSON.toJSONString(obj) / JSONObject.toJSONString(obj) 重点支持
JsonUtil.getObjectToString(obj) 重点支持(按 Fastjson/Jackson 默认字段规则推断)
RedisTemplate.opsForValue().set(key, obj) 直接写对象 第二阶段支持
RedisTemplate.opsForHash().put(key, field, obj) 第二阶段支持
StringRedisTemplate 写 JSON 字符串 重点支持
锁 / 计数器 / token 简单值 默认忽略

2.2 Key 与实体映射

  • 不存在统一的「Key → 类型」注册中心
  • 存在大量 static final StringString.format(...)buildCacheKey(...) 等模式
  • 第一版采用:写入点静态推断 + 可选 YAML 人工补充映射

2.3 多模块特征

  • pom.xml 下约 30+ 顶层模块、200+ 子模块
  • Java 版本混用8/9/10/11工具统一使用 JDK 11 编译运行
  • 公共工具类 RedisUtilJsonUtil 来自外部依赖,不在业务仓源码内 — 仅分析调用方,不深入依赖实现

3. 总体架构

3.1 交付形态

沿用现有 code-checker 模式(见 redisCheck/.gitea/demo.yaml

redisCheck 仓库
  ├── 开发 Java 分析工具
  ├── mvn package 打 fat-jar
  ├── 发布到 Nexuscom.codechecker:redis-schema-checker:{version}
  └── 提供默认配置模板

jnpf-java-cloud 仓库
  ├── .gitea/workflows/redis-schema-check.yaml
  ├── .gitea/config/redis-schema-check-config.yaml
  └── push 时下载 jar 并执行检测

3.2 架构图

flowchart TB
    subgraph Gitea["Gitea Push Pipeline"]
        A[push 事件] --> B[浅克隆 old/new 提交]
        B --> C[下载 redis-schema-checker.jar]
        C --> D[java -jar 执行检测]
    end

    subgraph Checker["redis-schema-checker (JDK 11)"]
        D --> E[GitDiffScanner]
        E --> F[RedisWritePointDetector]
        F --> G[JavaSchemaExtractor]
        G --> H[SchemaDiffer]
        H --> I{有结构变更?}
        I -->|否| J[exit 0]
        I -->|是| K[ReportBuilder]
        K --> L[WeComNotifier]
        L --> M{mode=block 且含变更?}
        M -->|是| N[exit 1]
        M -->|否| J
    end

3.3 核心设计原则

  1. 纯静态分析:基于 Java 源码 AST + 符号解析,不启动 Spring 容器
  2. Diff 驱动:只分析本次 push 变更涉及的文件及其关联类型
  3. 本仓限定:类型解析仅在业务仓库 src/main/java 范围内
  4. 可配置:忽略规则、严重级别、通知开关、阻断开关均可 YAML 配置
  5. 可演进:第一版聚焦 JSON 字符串写入,后续扩展 Template 直写对象

4. 技术选型

组件 选型 版本建议 说明
语言 Java 11 与 CI Runner jdk11 对齐
构建 Maven 3.8+ 与现有私库发布流程一致
AST 解析 JavaParser 3.25.x 完整 Java 语法树
符号解析 javaparser-symbol-solver-core 3.25.x 跨文件类型推断
配置 SnakeYAML 2.x 读取检测配置
HTTP 通知 JDK HttpClient / OkHttp 11 内置 / 4.x 企微 Webhook
报告 Jackson 2.15.x JSON/Markdown 报告序列化
测试 JUnit 5 5.10.x 单元测试 + 夹具样本

不采用 Spoon / Eclipse JDT 的原因JavaParser 足够覆盖第一版需求依赖更轻CLI 启动更快。

Lombok 处理策略:第一版基于源码字段 + @Data 等注解推断序列化字段;对 @Builder@SuperBuilder 等复杂场景标记为「低置信度」并降级为 P2 提示。后续可选集成 lombok.ast 或 delombok 预处理。


5. 工程结构redisCheck 仓库)

redisCheck/
├── pom.xml                               # 单模块工程(无父子结构)
├── docs/
│   ├── 实施方案.md
│   ├── 配置说明.md
│   └── CI集成说明.md
├── src/
│   ├── main/
│   │   ├── resources/
│   │   │   └── default-config.yaml       # 内置默认配置(随 jar 发布)
│   │   └── java/com/codechecker/redis/
│   │       ├── cli/                      # 命令行入口
│   │       ├── config/                   # 配置模型
│   │       ├── git/                      # Git 操作
│   │       ├── analyze/                  # 编排与工作树扫描
│   │       ├── detector/                 # Redis 写入点检测
│   │       ├── schema/                   # Schema 提取
│   │       ├── diff/                     # 结构对比
│   │       ├── key/                      # Key 推断
│   │       ├── report/                   # 报告
│   │       └── notify/                   # 企微通知
│   └── test/
│       ├── resources/fixtures/tenant/
│       └── java/...
├── .gitea/
│   ├── workflows/redis-schema-check.yaml
│   └── config/redis-schema-check-config.yaml
└── target/                               # 构建产物

5.1 Maven 坐标

<groupId>com.codechecker</groupId>
<artifactId>redis-schema-checker</artifactId>
<version>1.0.0-SNAPSHOT</version>

打包为 shaded/fat jar,主类:com.codechecker.redis.cli.RedisSchemaCheckerMain


6. 执行流程详解

6.1 CLI 参数

java -jar redis-schema-checker-1.0.0.jar \
  --config .gitea/config/redis-schema-check-config.yaml \
  --repo-root /path/to/jnpf-java-cloud \
  --old-sha abc123 \
  --new-sha def456 \
  --branch feature/xxx \
  --modifier zhangsan \
  --modify-time "2026-07-13 14:00:00"
参数 必填 说明
--config 检测配置文件路径
--repo-root 业务仓库根目录
--old-sha 对比基准提交
--new-sha 当前提交
--branch 分支名,用于报告展示
--modifier 提交人Gitea actor
--modify-time 提交时间

6.2 对比基准old-sha获取策略

demo.yaml 保持一致,优先级:

  1. 流水线显式传入 --old-sha(通常为 HEAD~1
  2. HEAD~1 不存在(首次提交)→ 跳过检测,exit 0
  3. 浅克隆 --depth 2 确保 HEAD~1 可用

不支持一次 push 多个 commit 时逐个分析;第一版仅对比 HEAD~1..HEAD。后续可扩展为 before..after 范围分析。

6.3 处理步骤

Step 1加载配置

读取 redis-schema-check-config.yaml,合并默认值(见 docs/配置说明.md)。

Step 2Git Diff 扫描

git diff --name-only {old-sha} {new-sha} -- '*.java'

输出变更 Java 文件列表。同时记录 diff hunks用于判断「是否仅注释/格式变更」。

Step 3构建双版本源码索引

old-shanew-sha 分别:

  1. git show {sha}:path/to/File.java 提取文件内容(无需完整 checkout 两个 worktree
  2. 解析为 CompilationUnit
  3. 建立 类全名 → CompilationUnit 索引(仅本仓 src/main/java

Step 4Redis 写入点检测

变更文件 中扫描以下 AST 模式:

模式 ID 匹配表达式 提取信息
W01 redisUtil.insert(key, JSON.toJSONString(expr), ttl) key 表达式、value 表达式
W02 redisTemplate.opsForValue().set(key, JSON.toJSONString(expr), ...) 同上
W03 stringRedisTemplate.opsForValue().set(key, JsonUtil.getObjectToString(expr), ...) 同上
W04 redisTemplate.opsForValue().set(key, expr, ...) 且 expr 非字面量 第二阶段
W05 redisTemplate.opsForHash().put(key, field, expr) 第二阶段
W06 JSON.parseObject(cacheValue, Xxx.class) 辅助反向确认读取类型

忽略规则(自动):

  • value 为字符串字面量、数字、UUID"1"
  • 方法名含 setIfAbsentincrementdeleteremoveexpire
  • key 匹配 ignore_key_patterns 配置

Step 5类型推断

对每个写入点的 expr,使用 JavaParser Symbol Solver 推断类型:

// 示例TenantDbContentCacheHelper.cacheSuccess
CacheEnvelope envelope = new CacheEnvelope();
envelope.setVo(vo);
envelope.setExpiresAtMs(expiresAtMs);
redisUtil.insert(buildCacheKey(encode), JSON.toJSONString(envelope), ttl);

推断链:

  1. JSON.toJSONString(envelope) → 实参类型 CacheEnvelope
  2. 定位 CacheEnvelope 类(内部类需支持 Outer$Inner
  3. 读取字段 vo: TenantVOexpiresAtMs: Long
  4. 递归展开 TenantVOdbName: StringlinkList: List<TenantLinkModel>
  5. 继续展开 TenantLinkModel 全部字段

注解处理(第一版):

注解 行为
@JSONField(serialize = false) 排除字段
@JSONField(name = "xxx") 字段名映射
@JsonIgnore 排除字段
@JsonProperty("xxx") 字段名映射
@Schema 忽略(不影响序列化)

Step 6生成 JSON Schema

将 Java 类型转为统一的 TypeSchema 树:

{
  "typeName": "jnpf.util.TenantDbContentCacheHelper.CacheEnvelope",
  "fields": [
  {
    "path": "vo",
    "javaType": "jnpf.model.TenantVO",
    "jsonType": "object",
    "children": [
      { "path": "vo.dbName", "jsonType": "string" },
      { "path": "vo.linkList", "jsonType": "array", "itemType": "jnpf.model.TenantLinkModel" }
    ]
  },
  {
    "path": "expiresAtMs",
    "javaType": "java.lang.Long",
    "jsonType": "number"
  }
  ]
}

Step 7Schema Diff

对比同一写入点在 old/new 两个版本的 TypeSchema,输出 SchemaChange 列表。

变更类型与严重级别

变更类型 示例 默认级别
FIELD_REMOVED 删除 dbName P0
TYPE_CHANGED linkList 从数组变对象 P0
WRAPPER_ADDED 顶层增加 vo 包装 P0
FIELD_PATH_MOVED dbNamevo.dbName P0
FIELD_ADDED 新增 expiresAtMs P1
KEY_PATTERN_CHANGED key 常量变更 P1
WRITE_POINT_REMOVED 删除缓存写入 P1
WRITE_POINT_ADDED 新增缓存写入 P2
LOW_CONFIDENCE 类型推断失败 P2

Step 8报告与通知

生成 CheckReport,包含:

  • 仓库名、分支、old/new sha、提交人、时间
  • 变更列表(按严重级别排序)
  • 每项Key 模式、写入位置(类#方法:行号)、旧结构、新结构、变更摘要

调用企微 Webhook 发送 Markdown 消息。

Step 9退出码

条件 退出码
无变更 / 仅 P2 0
mode=notify 且存在 P0/P1 0仍通知
mode=block 且存在 P0/P1/P2 1
配置错误 / 执行异常 2

7. Redis Key 推断策略

7.1 自动推断

优先级 模式 示例 结果
1 字符串字面量 "tenant:db:content:" + encode tenant:db:content:*
2 常量引用 CACHE_KEY_PREFIX + encode 追溯常量值
3 String.format(CONST, args) String.format(ATTENDANCE_BASE_SETTING_CACHE_KEY, tenantId) fbt:attendance:base_setting:cache:*
4 方法调用 buildCacheKey(encode) 读取方法内 return 表达式
5 变量 redisKey unknown-key

7.2 人工补充(配置)

manual_mappings:
  - id: tenant-db-content
    writer_method: "jnpf.util.TenantDbContentCacheHelper#cacheSuccess"
    key_pattern: "tenant:db:content:*"
    value_type: "jnpf.util.TenantDbContentCacheHelper.CacheEnvelope"

当自动推断置信度低时,以 manual_mappings 为准。


8. 配置与开关设计

采用 双层配置合并 策略:

层级 位置 职责
默认配置 工具 jar 内 default-config.yaml 检测模式、忽略规则、严重级别默认值
业务覆盖 jnpf-java-cloud/.gitea/config/redis-schema-check-config.yaml mode、notify、include_modules、manual_mappings

合并规则:业务配置覆盖默认配置,未声明的项沿用默认值。

CLI 调用:

java -jar redis-schema-checker.jar \
  --config .gitea/config/redis-schema-check-config.yaml \
  ...

工具启动时自动加载 jar 内 default-config.yaml,再与 --config 指定的业务配置深度合并。

详见 docs/配置说明.md。核心开关:

# 总开关false 时跳过检测与通知,流水线直接通过
enabled: true

# 运行模式notify仅通知| block检测到结构变更即阻断流水线
mode: notify

# 是否发送企微通知
notify:
  enabled: true
  webhook_env: WECOM_ROBOT_WEBHOOK

9. CI 集成方案

详见 docs/CI集成说明.md。核心流程:

# jnpf-java-cloud/.gitea/workflows/redis-schema-check.yaml
name: Redis序列化结构检查
on: [push]

jobs:
  redis-schema-check:
    if: ${{ gitea.ref != 'refs/heads/pre' && gitea.ref != 'refs/heads/dev' && gitea.ref != 'refs/heads/master-2.0' }}
    runs-on: jdk11
    steps:
      - name: 检出代码
        run: |
          git clone --depth 2 --single-branch --branch "${{ gitea.ref_name }}" \
            "https://${{ gitea.token }}@git.niujiekeji.com/${{ gitea.repository }}.git" .
          git checkout -B "${{ gitea.ref_name }}" "${{ gitea.sha }}"

      - name: 下载检测工具
        run: |
          # 从 Nexus 下载 redis-schema-checker jar
          ...

      - name: 执行检测
        env:
          WECOM_ROBOT_WEBHOOK: ${{ secrets.WECOM_ROBOT_WEBHOOK }}
        run: |
          OLD_SHA=$(git rev-parse HEAD~1 2>/dev/null || echo "")
          [ -z "$OLD_SHA" ] && exit 0
          java -jar /tmp/redis-schema-checker-1.0.0.jar \
            --config .gitea/config/redis-schema-check-config.yaml \
            --repo-root . \
            --old-sha "$OLD_SHA" \
            --new-sha "$(git rev-parse HEAD)" \
            --branch "${{ gitea.ref_name }}" \
            --modifier "${{ gitea.actor }}" \
            --modify-time "$(git log -1 --format=%cd --date=format:'%Y-%m-%d %H:%M:%S')"

10. 分阶段交付计划

Phase 1 — MVP约 1.5 周)

目标:跑通端到端链路,覆盖租户缓存典型场景。

任务 产出
Maven 工程骨架 + CLI 可执行 fat-jar
Git diff 扫描 变更文件列表
W01~W03 写入点检测 覆盖 JSON 字符串写入
基础 Schema 提取 支持普通类、内部类、List、嵌套
Schema Diff P0/P1 字段增删、包装、路径迁移
企微通知 Markdown 消息
notify/block 开关 配置驱动
夹具测试 TenantVO/CacheEnvelope 样本

验收标准

  • TenantDbContentCacheHelper 的结构变更能输出 P0 报告
  • 流水线 push 后能收到企微通知
  • mode=block 时任意 P0/P1/P2 变更导致 exit 1

Phase 2 — 增强(约 1 周)

任务 说明
W04/W05 模式 RedisTemplate 直写对象、Hash 写入
注解完整支持 Fastjson/Jackson 注解
Key 推断增强 String.format、常量追溯
忽略规则完善 锁/计数器/token 自动过滤
多模块性能优化 并行解析、缓存索引

Phase 3 — 运营(约 0.5 周)

任务 说明
报告落盘 可选输出 JSON 报告文件
误报反馈 suppressions 配置支持按写入点忽略
更多业务场景覆盖 考勤、文件下载进度等

11. 测试策略

11.1 单元测试

  • SchemaDifferTest:纯字段路径对比逻辑
  • JavaSchemaExtractorTest:类字段展开、注解、内部类
  • RedisWritePointDetectorTest:各种写入 AST 模式匹配
  • RedisKeyResolverTest常量、format、拼接推断

11.2 夹具集成测试

src/test/resources/fixtures/ 放置真实业务代码片段(从 jnpf-java-cloud 提取并脱敏),模拟 old/new 两个版本:

夹具 验证点
tenant-cache/ 包装结构变更 P0
attendance-base-setting/ Map 结构缓存
evaluate-config/ VO 字段新增 P1
lock-only/ 应被忽略

11.3 端到端测试

jnpf-java-cloud 开测试分支,故意提交一个 VO 字段变更,验证流水线 + 企微通知。


12. 风险与限制

风险 影响 缓解措施
类型推断失败 漏报 标记 LOW_CONFIDENCE,配置 manual_mappings
Lombok 复杂注解 字段遗漏 基于源码字段 + 注解;后续 delombok
同一 key 多分支写不同类型 误报 报告注明置信度;人工 suppression
浅克隆 parent 不可用 跳过检测 --depth 2;文档明确要求
一次 push 多 commit 仅检最后一个 文档说明;后续扩展 range
依赖 jar 中的类型 字段展开不完整 配置 manual_mappings 补充
JsonUtil 实现不可见 序列化规则猜测 默认按字段名序列化;与 Fastjson 对齐

13. 已确认决策Grill 共识)

# 决策项 结论
1 阻断范围 block 模式下 P0/P1/P2 全部阻断exit 1
2 发布坐标 独立产物 com.codechecker:redis-schema-checker:1.0.0
3 配置归属 双层配置jar 内 default-config.yaml + 业务仓覆盖合并
4 上线策略 notify 观察 1 周,稳定后手动切 block
5 检测范围 src/main/java,不扫描测试代码

以上决策已纳入实施方案,可进入开发阶段。


14. 附录:关键类设计草图

WritePoint

public class WritePoint {
    String filePath;
    int lineNumber;
    String enclosingClass;
    String enclosingMethod;
    String keyExpression;       // 原始 AST 表达式
    String resolvedKeyPattern;  // 推断结果,如 tenant:db:content:*
    String valueExpression;
    String resolvedValueType;   // 全限定类名
    double confidence;          // 0.0 ~ 1.0
}

SchemaChange

public class SchemaChange {
    Severity severity;          // P0, P1, P2
    ChangeType changeType;      // FIELD_REMOVED, WRAPPER_ADDED, ...
    String keyPattern;
    String writeLocation;       // class#method:line
    String fieldPath;           // 如 vo.dbName
    String oldValue;
    String newValue;
    String message;             // 人类可读描述
}

CheckReport

public class CheckReport {
    String repository;
    String branch;
    String oldSha;
    String newSha;
    String modifier;
    String modifyTime;
    String mode;
    List<SchemaChange> changes;
    boolean blocked;
    int exitCode;
}