cc 5 өдөр өмнө
parent
commit
00cea018c1

+ 53 - 0
.zcode/plans/plan-session_23268d4b80aa40a09fc2f1c4bb565275.md

@@ -0,0 +1,53 @@
+# CaseDataCache BUCKETS 迁移到 Redis(纯 Redis 直读直写)
+
+## 目标
+
+把 `CaseDataCache` 中的 `BUCKETS`(caseId → CaseBucket)JVM 内存缓存整体迁移到 Redis:删除 `CaseBucket` 内部类、`ReentrantReadWriteLock` 全套锁、快照重建逻辑。**公开静态 API 保持不变,20+ 调用方零改动**。
+
+## Redis 数据结构
+
+前缀 `qing_jian:case:{caseId}:`(复用 `StrConsts.CACHE_TAG_QING_JIAN`,管理端 `SystemService.cleanCache()` 的 `KEYS qing_jian*` 全量清理自动覆盖)。`{caseId}` 花括号天然是 Redis Cluster hash tag,将来上集群时同案 key 同 slot。
+
+| Key | 类型 | 内容 | 服务的查询 |
+|---|---|---|---|
+| `qing_jian:case:{id}:pno:{libType}:{personName}` | SET | personNo 集合 | getPerPhones/getPerCards(SMEMBERS)、putPerLib(SADD) |
+| `qing_jian:case:{id}:val2name` | HASH | personNo → personName | getPersonPhone/getPersonCard/findPersonVal(HGET)、containsPersonValue(HEXISTS)——O(1) 替代现在 findPersonVal 的全表线性扫描 |
+| `qing_jian:case:{id}:pno-idx` | SET | 全部正向 key 后缀 | cleanCache/initCache 精确删除,避免 SCAN |
+
+说明:现在 personPhone/personCard 按 libType 分两个 Map,统一为 val2name 后理论上同一字符串既是手机号又是卡号会互串——实际号码位数不同不可能冲突,接受(代码注释注明)。
+
+## 新 CaseDataCache 实现(重写)
+
+- `GOVERN_CONFIG` 及 `cleanGovernConfig` **保持不变**(本地 ConcurrentHashMap,key 已含 caseId;GovernConfService/SystemService 不动)。
+- 静态懒取 `StringRedisTemplate`(`SpringUtil.getBean`,Boot 自动配置的 bean,纯字符串数据无需 JSON 序列化器)。
+- `initCache(caseId)`:requireCaseId → 查库(`CaseContextHolder.callWith` 包裹,不变)→ 按 pno-idx 删旧 key → 每批 ~1000 行 pipeline 写入 SADD 正向 + HSET 反向 + SADD idx。失败抛异常(`CaseInfoService.open` 已有 try/catch 兜底)。
+- `putPerLib` 两个重载:解析 key(沿用现有 split 逻辑)→ **pipeline 一次往返**执行 SADD + HSET + SADD idx → `CleanCache.addPersonLibNo(...)` 不变。Redis 异常 log.error 后继续,不杀长时清洗任务(数据仍经 CleanCache 落库,下次 initCache 修复)。
+- 读方法(getPerPhones/getPerCards/getPersonPhone/getPersonCard/containsPersonValue/findPersonVal):对应 SMEMBERS/HGET/HEXISTS;Redis 异常或无案件上下文 → log.error + 降级返回空集/null/false(与今天 bucket 缺失降级一致)。
+- `cleanCache(caseId)`:按 idx 删除三组 key;异常吞掉记日志(调用点在 listener/reaper)。
+- 删除 `cleanAll()`(无调用方);保留 `findPersonVal`(HGET 一行实现,虽生产已无调用但保持 API 完整)。
+- 保留语义:写操作缺案件上下文抛异常、读操作缺上下文降级并打日志。
+
+## 测试改造
+
+重写 `CaseDataCacheConcurrencyTest`:
+- 保留:cleanGovernConfig 精确匹配、缺上下文写抛异常/读降级(读降级在 Redis 不可达时同样成立)。
+- 改造:案件隔离、put/get 回环、cleanCache 删除 → 需真实 Redis,前置 `Assumptions.assumeTrue(ping 通)`,不可达时跳过(离线 `mvn test` 保持绿)。
+- 删除:读写锁 CME 竞争测试(锁已不存在,Redis 单命令原子)。
+
+## 验证
+
+1. 编译 ai-server 模块。
+2. 跑 `CaseDataCacheConcurrencyTest`(Redis 可达全跑,不可达自动跳过)。
+3. 手工验证(需 PG/ES/Redis):启动 → 开案 → `redis-cli --scan --pattern 'qing_jian:case:*'` 确认 key 存在 → 清洗小文件后 val2name 出现新号码 → 退案后 key 全部删除。
+
+## 文件清单
+
+- 改:`ai-server/.../common/cache/CaseDataCache.java`(重写,约 310 行 → 约 200 行,无锁无内部类)
+- 改:`ai-server/src/test/.../CaseDataCacheConcurrencyTest.java`(重写匹配新语义)
+- 不动:所有调用方、GOVERN_CONFIG 相关、CleanCache
+
+## 已知取舍(已与你确认)
+
+- 清洗热路径逐行 1~2 次 Redis 往返,大文件清洗变慢(分钟级),已接受。
+- initCache 重建窗口内并发 putPerLib 可能丢写——与今天快照换引用的竞态同级,非回归。
+- 不设 TTL:依赖 cleanCache 触发点 + 管理端 `qing_jian*` 清理;进程崩溃可能留孤儿 key(上限=案件数),后续可按需补 TTL。