| name | cache-framework |
| description | Wavelet 项目专用:当新增或修改业务缓存(RAM/Redis/DB 三层读路径)、缓存失效、多节点 pub/sub 同步、或评估高频读是否应接入缓存时必须使用。本技能说明系统标准缓存框架、参考实现、禁止写法与分布式一致性要求。 |
系统三层缓存框架
开始前阅读根目录 AGENTS.md(含 Skill 关联索引)。Wavelet 标准读路径为 本地 RAM → Redis → PostgreSQL(由快到慢),不是 DB 优先。
详细性能背景见 docs/PERFORMANCE.md。
关联 Skill
标准模式(金标准)
参考:internal/repository/system_config_cache.go + GetSystemConfigByKey / ListSystemConfigsByKeys。
| 层级 | 技术 | 职责 |
|---|
| L1 本地 | pkg/cache/ram(Otter v2) | 进程内热数据,最低延迟 |
| L2 共享 | Redis db.GetJSON / SetJSON / HSetJSON + db.PrefixedKey | 跨节点共享,带 TTL 或写穿 |
| L3 权威 | PostgreSQL via db.DB(ctx) | 唯一数据源 |
读路径模板
func GetThingCached(ctx context.Context, key string) (Thing, error) {
ensureThingCacheListener()
if v, ok := thingRAM.GetIfPresent(key); ok {
return cloneThing(v), nil
}
if db.Redis != nil {
var v Thing
if err := db.GetJSON(ctx, redisKey(key), &v); err == nil {
thingRAM.Set(key, cloneThing(v))
return v, nil
}
}
v, err := loadThingFromDB(ctx, key)
if err != nil {
return Thing{}, err
}
populateThingCache(ctx, v)
return v, nil
}
写穿(populate)
DB miss 或业务创建成功后,必须回写上层:
func populateThingCache(ctx context.Context, v Thing) {
thingRAM.Set(v.Key, cloneThing(v))
if db.Redis != nil {
_ = db.SetJSON(ctx, redisKey(v.Key), v, cacheTTL)
}
}
失效(Invalidate)— 分布式必做三步
数据变更(Admin 更新、软删除、状态迁移)时:
- 本机 RAM —
thingRAM.Invalidate(key) 或 InvalidateAll()
- Redis —
Del / HDel 对应 key
- pub/sub 广播 — 通知其他节点清除 RAM(Redis 已由写节点清掉)
func InvalidateThingCache(ctx context.Context, key string) error {
ensureThingCacheListener()
thingRAM.Invalidate(key)
if db.Redis != nil {
if err := db.Redis.Del(ctx, db.PrefixedKey(redisKey(key))).Err(); err != nil {
return err
}
publishThingRAMInvalidation(ctx, key)
}
return nil
}
pub/sub 监听模板
const thingInvalidationChannel = "domain:thing_invalidation"
func startThingCacheInvalidationListener() {
if db.Redis == nil {
return
}
go func() {
pubsub := db.Redis.Subscribe(context.Background(), thingInvalidationChannel)
defer func() { _ = pubsub.Close() }()
for msg := range pubsub.Channel() {
thingRAM.Invalidate(parsedKey)
}
}()
}
- 使用
sync.Once 启动监听;ensureListener 必须在 db.Redis == nil 时直接 return,不可消费 Once(否则测试或 Redis 晚初始化时监听器永不启动)。
- 测试可提供
StopThingCacheListener + 重置 Once(参考 StopUploadMetaCacheListener、StopAuthSourceCacheListener)。
- 其他节点收到消息后只清 RAM,不再删 Redis。
现有实现速查
| 域 | 文件 | L1 | L2 | pub/sub |
|---|
| 系统配置 | repository/system_config_cache.go | pkg/cache/store | ❌ 无 Redis 缓存 | system:config_broadcast (别名 system:config_invalidation) ✅ |
| CAPTCHA 运行时 | apps/cap/runtime_settings.go | atomic.Pointer | (借配置 Redis) | 订阅 system:config_invalidation ✅ |
| 上传元数据 | apps/upload/cache/meta_cache.go | Otter | Redis JSON | upload:meta_invalidation ✅ |
| 上传访问白名单 | apps/upload/cache/access_cache.go | 进程内 TTL | (借配置读路径) | upload:file_access_invalidation ✅ |
| Auth Source | repository/auth_source_cache.go | Otter | Redis JSON | oauth:auth_source_invalidation ✅ |
| OAuth 用户/Token | apps/oauth/cache.go | 自研 map | Redis JSON | ❌ 无 pub/sub(历史债) |
| 推送渠道 | repository/push_channel.go | 无 | Redis JSON | ❌ 仅 Redis Del |
| Storage 驱动 | internal/storage/storage.go | RWMutex 快照 | — | storage:config_invalidation ✅ |
新增缓存工作流
- 判定是否需要缓存:高频读、低变更、可容忍短暂 TTL;写路径必须能统一失效。
- 选型 L1:优先
pkg/cache/ram.MustNew;禁止自研 map+mutex+TTL,除非有充分理由并文档说明。
- 选型 L2:小对象
SetJSON;配置类多条目用 Redis Hash(HSetJSON)。
- 定义 Redis key:小写蛇形,带业务前缀(
upload:meta:{id});统一 db.PrefixedKey。
- 实现 Invalidate + pub/sub:凡多实例部署可读的 RAM 缓存必须有失效广播。
- 挂载变更钩子:在所有 DB 变更入口调用 Invalidate(含 Worker/定时任务,不只 HTTP Handler)。
- 测试:
- RAM hit / Redis hit / DB fallback
- Invalidate 清 L1+L2
- pub/sub 触发他机 RAM 失效(可用 miniredis Publish 模拟)
Reset*RAMCacheForTest 仅清本机 RAM
- 运行
go test 相关包 + make code-check。
变更钩子清单(上传元数据示例)
| 入口 | 动作 |
|---|
ingest.persistUploadRecord 创建成功 | SetUploadMetaCache |
ingest.Remove / RemoveOwned | InvalidateUploadMetaCache |
task/cleanup.go 软删除 pending 文件 | InvalidateUploadMetaCache |
直接 repository.SoftDeleteUpload | 禁止 — 必须走 upload.Remove |
禁止写法
var mu sync.RWMutex
var items = map[uint64]entry{}
func Invalidate(ctx context.Context, id uint64) {
localDelete(id)
redis.Del(...)
}
特殊场景
敏感字段(ClientSecret)
模型 json:"-" 时,Redis DTO 用独立 *RedisRecord struct 显式序列化字段(见 auth_source_cache.go)。
批量读配置
批量接口必须与单 key 一致走 Redis(ListSystemConfigsByKeys 在 RAM miss 后逐 key HGetJSON,再 DB IN)。
仅进程内、短 TTL、配置衍生
可用进程内快照 + 订阅上游 pub/sub(access_cache.go、cap/runtime_settings.go),不必强行 Redis L2。
OAuth 用户/Token
沿用 oauth/cache.go;新增逻辑调用 SetCachedUser / SetCachedToken 预热,变更调用 InvalidateCachedUser / InvalidateCachedToken。
验证清单
go test ./internal/repository/... ./internal/apps/upload/cache/...
make code-check
相关文件
- L1 引擎:
pkg/cache/ram/cache.go
- DB/Redis 助手:
internal/db/redis.go(GetJSON, SetJSON, HGetJSON, PrefixedKey)
- 金标准:
internal/repository/system_config_cache.go
- 上传元数据:
internal/apps/upload/cache/meta_cache.go
- Auth Source:
internal/repository/auth_source_cache.go
- 性能文档:
docs/PERFORMANCE.md