Files
schemaCheck/docs/V1.1-非结构写入收敛整改方案.md
2026-08-03 11:03:22 +08:00

13 KiB
Raw Blame History

V1.1 非结构写入收敛 — 整改方案

版本v1.1
日期2026-08-03
状态:已落地
依据:业务仓实跑误报 + 单测复现 RecordingTodoReplaceScenarioTest
关联:docs/redis序列化结构检测实施方案.mddocs/配置说明.md
约束:不新增配置项;收敛为工具默认行为,随 jar 升级生效


1. 背景与已证实问题

1.1 实跑样本jnpf-java-cloud

【序列化结构变更】 jnpf-java-cloud
分支: patrol/dev_patrol_2.0
提交: adbb15c6 → 6ddc72dc
- Key --> recording:todo:replace::
  位置: RecordingTodoDomainServiceImpl#replaceText:437
  类型: <未解析>
  value 新增为: “{}”
- Key --> recording:todo:replace:title::
  位置: RecordingTodoDomainServiceImpl#replaceText:452
  类型: <未解析>
  value 新增为: “{}”

业务代码(摘要):

// 缓存替换前原文,纯 String无 JSON / VO 序列化
stringRedisTemplate.opsForValue().set(contentRedisKey, originalContent);
stringRedisTemplate.opsForValue().set(titleRedisKey, originalTitle);

1.2 复现结论

单测 RecordingTodoReplaceScenarioTest 已稳定复现:

环节 现状
检测 命中 W04(直写对象)
类型 String 不在本仓 SourceIndexresolvedValueType = null → 展示 <未解析>
Schema extract(null) 空结构 → 骨架 "{}"
报告 旧提交无该写入点 → WRITE_POINT_ADDED →「value 新增为」

核心缺口:新增写入点(WRITE_POINT_ADDED)路径上,没有对「无结构 / 不可展开」的 value 做收敛;只要 AST 命中 W04/W05及部分边缘写法就会告警。

1.3 本方案范围

纳入 不纳入(另案)
Redis 非结构写入误报收敛 MQ asyncSend 漏检(作用域过窄)
同类场景枚举与统一收敛规则 新增 Key 企微文案 / 整段染色体验
检测层 + 分析层双闸门设计 业务仓 ignore 配置补丁(仅作临时手段)

2. 问题本质

工具目标是监控 可 JSON/对象 Schema 化的序列化结构变更

当前实现把「能调用 set/insert/put」近似当成「值得监控」,收敛只做了很窄的一层:

现有收敛
  ├─ 方法黑名单setIfAbsent / increment / delete …
  ├─ isTrivialValue字面量、UUID、toString/valueOf/randomUUID
  └─ ignore.key_patternslock / loginCount / Authorization …

缺失收敛
  ├─ value 声明类型为 String / 数字 / 布尔 / byte[](变量,非字面量)
  ├─ value 类型无法在本仓展开null / 空 Schema
  ├─ 先 JSON 序列化到局部 String 再写入Redis 未 unwrap误成 String-W04
  └─ WRITE_POINT_ADDED 不检查「是否有可对比结构」

因此:凡是「新增一段无结构写入」都会变成「类型未解析 + value 新增为 {}」,与 RecordingTodo 同形。


3. 同类场景排查(是否会中招)

判定标准能否在默认配置W01~W05 全开)下产出与实跑同形的告警
WRITE_POINT_ADDED + <未解析> 或空/{} 骨架,或明显无业务结构)。

3.1 高概率同形(与实跑同类,应优先收敛)

ID 场景 典型代码 命中模式 结果形态 与实跑关系
S1 String 变量直写 stringRedisTemplate.opsForValue().set(k, originalContent) W04 <未解析> + {} 已证实
S2 String 方法返回值直写 set(k, todo.getContent()) W04 同 S1返回类型 String 时) 同形;isTrivialValue 不认 getXxx
S3 数字 / 布尔变量直写 set(k, ttlSeconds) / set(k, enabled) W04 <未解析> + {} 同形;标量无字段
S4 Hash 写入 String/标量 opsForHash().put(k, f, nameStr) W05 同 S1 W05 仅挡字面量,不挡变量
S5 redisUtil.insert 写 String 变量 redisUtil.insert(k, token, ttl) W04 同 S1 insert + 含 redis 的 scope → 走 W04 分支
S6 外部依赖类型直写 set(k, externalDto) 且 DTO 不在本仓 W04 <未解析> + {} 同形;「有对象语义」但工具无法展开,告警无信息量
S7 类型擦除 Object / 原始 Map set(k, (Object) x) / set(k, map) 且无法解析元素 W04 常为 <未解析> / 空或弱 Schema 同形或近似

3.2 中概率变形(会误报,但表现略不同)

ID 场景 典型代码 命中模式 结果形态 说明
S8 局部 JSON 字符串再写入 String json = JSON.toJSONString(vo); set(k, json) W04 类型按 String → <未解析>+{} Redis unwrapJsonStringVarMQ 已有);应归 W01~W03却被当成无结构
S9 显式序列化标量 set(k, JSON.toJSONString(someString)) W02 偶发低价值点 少见unwrap 后仍是 String应丢弃
S10 本仓 VO 但字段全 transient / 全忽略注解 set(k, emptyVo) W04 类型有名,骨架仍可能 {} 有类型名,但「新增 {}」仍噪音
S11 删除无结构写入点 删掉 S1 类代码 WRITE_POINT_REMOVED 「原结构 {}」 对称噪音,收敛规则应一并覆盖

3.3 低概率 / 已部分防护(记录边界)

ID 场景 现状 结论
S12 字面量 "1" / UUID.toString() isTrivialValue 已忽略 一般不中招
S13 key 命中 *:lock / loginCount:* ignore.key_patterns 仅覆盖命名约定,挡不住 recording:todo:replace:*
S14 MQ 直传 String / byte[] isBareStringOrBytesPayload 已忽略 Redis 应对齐MQ 数字变量仍可能漏(见下)
S15 MQ 直传业务 VO 正常监控 不属本问题
S16 Redis W01~W03 + 本仓 VO 正常监控 不属本问题

3.4 MQ 侧对照(同类思想,程度不同)

Redis现状 MQ现状
裸 String / byte[] 变量 不忽略 → S1 忽略
数字 / 布尔变量 不忽略 → S3 不忽略(仅 trivial 字面量)→ MQ 也有 S3 风险
局部 toJSONString 再 send/set Redis 不 unwrap → S8 已 unwrap
无法展开类型仍建点 会建点 → S6 会建点(直传对象时)
WRITE_POINT_ADDED 空 Schema 闸门 (同样会「新增 {}」)

结论:

  • Redis 是重灾区;
  • MQ 在 String 上已收敛,但 Integer/Long/Boolean 变量空 Schema 仍告警 与 Redis 同类,应在统一规则里一并处理。

3.5 分析层放大效应(与模式无关)

SchemaCheckAnalyzer 在「新写入点、旧无配对」时:

fileChanged && oldWritePoint == null
  → 无条件 WRITE_POINT_ADDED
  → extract(type) 即使为空也 render → "{}"
  → 进入企微

对比「已有写入点」路径:differ.diff 空则 continue不会为空 Schema 刷屏。

因此同类问题在 新增代码 时被放大;存量无结构写入若从未变更,不一定出告警。整改必须同时考虑:

  1. 检测层少建点(源头)
  2. 分析层对无结构新增/删除做闸门(兜底)

4. 收敛规则设计

4.1 原则

  1. 只监控有结构的序列化 value(本仓可展开的业务类型,或 unwrap 后得到的业务类型)。
  2. 宁漏勿滥:无法证明「有结构」则不告警。
  3. 检测层为主、分析层为辅:避免脏 WritePoint 进入报告链路。
  4. Redis / MQ 规则对齐,避免一边收敛一边漏。

4.2 规则表(建议落码)

规则 适用 行为
R-A 标量类型忽略 Redis W04/W05MQ 直传 payload value/payload 声明类型 ∈ {String, CharSequence, 原始/包装数字, Boolean, UUID, byte/Byte/byte[]}不建点
R-B 本仓可展开 Redis W04/W05MQ 直传对象 resolvedFqn == nullSourceIndex 无此类 → 不建点
R-C 空 Schema 闸门 AnalyzerADDED / REMOVED extractschema.isEmpty()不产生 WRITE_POINT_ADDED/REMOVED字段级 diff 路径保持:空 diff 已 skip
R-D 局部 JSON unwrap Redis对齐 MQ String json = toJSONString(x); set/insert(k, json) → 按 W01~W03 建点,类型为 x;若 x 仍为标量 → 再走 R-A
R-E 方法返回值 与 R-A/R-B 相同 set(k, obj.getFoo()) 按方法返回类型判定,不因「非字面量」直接放行
R-F 集合 W04/W05 / MQ List<T> / Set<T>:元素 T 满足 R-B 才保留;List<String> 丢弃

4.3 明确保留(不应误杀)

写法 期望
set(k, JSON.toJSONString(vo)) / insert + 序列化 W01~W03监控 VO
redisTemplate.set(k, demoVo) 且 DemoVo 在本仓 W04监控
opsForHash().put(k, f, demoVo) 本仓 VO W05监控
MQ syncSend/asyncSend(topic, dto) 本仓 DTO 监控
局部 JSON unwrap 后得到本仓 VO 监控

4.4 配置策略

不新增任何 YAML / 检测开关。 R-A~R-F 均为工具内置默认行为,升级 jar 即生效。

业务侧若需临时止血,仍可沿用既有 ignore.key_patterns / ignore.writer_methods / suppressions,但不作为本问题根治手段。


5. 落地设计

5.1 推荐分层

┌─────────────────────────────────────────┐
│ RedisWritePointDetector / MqWritePointDetector
│   R-A 标量忽略
│   R-D Redis 局部 JSON unwrap
│   R-B 本仓类型门槛(直写对象)
│   R-E/R-F 返回值与集合元素
└──────────────────┬──────────────────────┘
                   ▼
┌─────────────────────────────────────────┐
│ SchemaCheckAnalyzer
│   R-C 空 Schema → 跳过 ADDED/REMOVED
│   (字段级 diff 逻辑不变)
└─────────────────────────────────────────┘

5.2 涉及文件(实施时)

文件 变更要点
RedisWritePointDetector.java R-A / R-B / R-D / R-E / R-F
MqWritePointDetector.java R-A 扩展数字/布尔;直传对象可加 R-B与 Redis 对齐)
SchemaCheckAnalyzer.java R-C 空 Schema 闸门
(可选)抽取 BareValueTypes 工具类 Redis/MQ 共用标量集合,防漂移
RecordingTodoReplaceScenarioTest 修复后期望 0 写入点(或拆「修复前/后」)
新增场景单测 覆盖 §3.1 / §3.2 表中 S2~S8、S11
docs/配置说明.md 同步 W04/W05 与空 Schema 行为

5.3 实施顺序建议

本版本一次性落地 R-A~R-FRedis + Analyzer + MQ 对齐),无分阶段开关、无灰度配置。


6. 测试与验收

6.1 复现用例(已有)

  • RecordingTodoReplaceScenarioTest:当前断言「会误报」。
  • 整改后应改为:检测结果为空,或 Analyzer 不产出 keyChanges。

6.2 建议补充用例

用例 期望(整改后)
set(k, getContent()) 返回 String 0 点
set(k, longVar) 0 点
opsForHash().put(k, f, strVar) 0 点
redisUtil.insert(k, tokenStr, ttl) 0 点
set(k, externalDto) 不在 SourceIndex 0 点
String json = toJSONString(vo); set(k, json) 1 点,类型 = VOW02/W01
set(k, demoVo) 本仓 VO 仍 1 点 W04
删除 String 直写 不产生 WRITE_POINT_REMOVED
MQ send(topic, longVar) 0 点Phase 4

6.3 回归

  • TenantScenarioTest、既有 W01~W05 / MQ fixture 全绿。
  • 业务仓抽样RecordingTodo 类告警消失;真实 VO 包装层变更仍能检出。

7. 临时手段与风险

说明
无新配置 不增加 keep_empty_schema_* 等开关;行为固化在检测/分析逻辑
业务 ignore 可对 recording:todo:replace:* 加 key 忽略,仅止血,不解决 S1~S7 类问题
误杀风险 依赖 jar 内 DTO 的 W04 将被忽略 → 可用既有 manual_mappings 或把类型源码纳入仓
Object 擦除 可能漏检真实结构 → 接受宁漏勿滥;或要求业务写明 VO 类型
与「展示优化」关系 即使将来给新增 Key 染色,空 {} 仍无意义;必须先收敛再建点

8. 总结

问题 结论
RecordingTodo 为何告警? W04 把 String 原文当成对象写入;新增路径无空结构闸门
是否只有这一处? 。S2~S7 同形S8 变形S11 对称MQ 在数字变量与空 Schema 上同类
根因一句话 新增写入检测缺少「有结构才告警」的收敛
整改抓手 检测层标量/本仓类型收敛 + Analyzer 空 Schema 闸门 + Redis JSON 局部 unwrap

9. 检查表

  • 认可 R-A~R-F 与「宁漏勿滥」、不新增配置
  • 外部 jar DTOS6默认不监控
  • 代码落地 + RecordingTodoReplaceScenarioTest 期望 0 点
  • 同步 配置说明.md
  • 全量 mvn test 通过