plan-session_23268d4b80aa40a09fc2f1c4bb565275.md 4.3 KB

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。