mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
8.7 KiB
8.7 KiB
name, description
| name | description |
|---|---|
| cache-framework | Wavelet 项目专用:当新增或修改业务缓存(RAM/Redis/DB 三层读路径)、缓存失效、多节点 pub/sub 同步、或评估高频读是否应接入缓存时必须使用。本技能说明系统标准缓存框架、参考实现、禁止写法与分布式一致性要求。 |
系统三层缓存框架
开始前阅读根目录 AGENTS.md(含 Skill 关联索引)。Wavelet 标准读路径为 本地 RAM → Redis → PostgreSQL(由快到慢),不是 DB 优先。
详细性能背景见 docs/PERFORMANCE.md。
关联 Skill
| 关联 | 何时一并阅读 |
|---|---|
| database-migration | 缓存对象对应新表/列/索引,或 seed 变更 |
| new-setting | 系统配置类缓存(GetSystemConfigByKey、ListSystemConfigsByKeys) |
| file-upload | 上传元数据 upload:meta:{id}、ingest/remove/cleanup 失效钩子 |
| clickhouse-batchwriter | 分析写入走 batchwriter,不要用本技能模式缓存 CH flush 队列 |
| new-api | 在 Handler 层接入 GetXxxCached 或评估高频读 |
| new-async-task | Worker/定时任务变更数据后必须 Invalidate*(如 system:cleanup) |
标准模式(金标准)
参考: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() // 订阅 pub/sub,仅 sync.Once
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) // 回写 RAM + Redis
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) // 只广播 RAM 失效
}
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() {
// 解析 payload,Invalidate RAM;勿重复 Del Redis
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/infra/objectstore/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 |
禁止写法
// ❌ 自研 L1,与 pkg/cache/ram 重复
var mu sync.RWMutex
var items = map[uint64]entry{}
// ❌ 只清本机 RAM + Redis,无 pub/sub(多节点 RAM 脏读)
func Invalidate(ctx context.Context, id uint64) {
localDelete(id)
redis.Del(...)
}
// ❌ DB 变更后忘记 Worker 路径
// cleanup 任务删了 upload 行,但未 InvalidateUploadMetaCache
// ❌ 在 Handler 里直接查 DB,绕过已有 GetXxxCached
// ❌ Redis key 不用 PrefixedKey(多环境共 Redis 时冲突)
// ❌ 在 init() 里启动 pub/sub 监听 — 与 bootstrap 规范冲突;用 sync.Once 懒启动
特殊场景
敏感字段(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(或已文档化的例外) - 读路径:RAM → Redis → DB
- 写穿 populate 在 DB load / 创建成功后
- Invalidate:RAM + Redis + Publish
ensureListener+ pub/sub 清他机 RAM- 所有变更入口(含 Worker)已挂钩
- 测试含 Invalidate 与 pub/sub
相关文件
- L1 引擎:
pkg/cache/ram/cache.go - DB/Redis 助手:
internal/infra/persistence/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