mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-05 15:26:36 +08:00
docs(agents): update AGENTS.md and development skills for cordis architecture
This commit is contained in:
@@ -1,217 +1,108 @@
|
||||
---
|
||||
name: "cache-framework"
|
||||
description: "Wavelet 项目专用:当新增或修改业务缓存(RAM/Redis/DB 三层读路径)、缓存失效、多节点 pub/sub 同步、或评估高频读是否应接入缓存时必须使用。本技能说明系统标准缓存框架、参考实现、禁止写法与分布式一致性要求。"
|
||||
description: "Wavelet 项目专用:当新增或修改基于 Cordis 插件的业务缓存、ctx.Cache() / contracts.CacheService 访问、三层读路径(RAM L1 + Redis L2 + DB L3)、多节点 Pub/Sub 失效同步时必须使用。"
|
||||
---
|
||||
|
||||
# 系统三层缓存框架
|
||||
# 系统三层缓存框架与开发规范 (Cordis 插件化架构)
|
||||
|
||||
开始前阅读根目录 `AGENTS.md`(含 **Skill 关联索引**)。Wavelet 标准读路径为 **本地 RAM → Redis → PostgreSQL**(由快到慢),不是 DB 优先。
|
||||
本技能指导 Wavelet 在 Cordis 架构下,如何使用平台统一提供的三层缓存服务(`ctx.Cache()` 与 `contracts.CacheService`)进行高性能缓存读写与分布式失效同步。
|
||||
|
||||
详细性能背景见 `docs/PERFORMANCE.md`。
|
||||
---
|
||||
|
||||
## 关联 Skill
|
||||
## 1. 三层读路径与标准契约
|
||||
|
||||
| 关联 | 何时一并阅读 |
|
||||
| :--- | :--- |
|
||||
| [database-migration](../database-migration/SKILL.md) | 缓存对象对应新表/列/索引,或 seed 变更 |
|
||||
| [new-setting](../new-setting/SKILL.md) | 系统配置类缓存(`GetSystemConfigByKey`、`ListSystemConfigsByKeys`) |
|
||||
| [file-upload](../file-upload/SKILL.md) | 上传元数据 `upload:meta:{id}`、ingest/remove/cleanup 失效钩子 |
|
||||
| [clickhouse-batchwriter](../clickhouse-batchwriter/SKILL.md) | 分析写入走 batchwriter,**不要**用本技能模式缓存 CH flush 队列 |
|
||||
| [new-api](../new-api/SKILL.md) | 在 Handler 层接入 `GetXxxCached` 或评估高频读 |
|
||||
| [new-async-task](../new-async-task/SKILL.md) | Worker/定时任务变更数据后必须 `Invalidate*`(如 `system:cleanup`) |
|
||||
|
||||
## 标准模式(金标准)
|
||||
|
||||
参考:`internal/repository/system_config_cache.go` + `GetSystemConfigByKey` / `ListSystemConfigsByKeys`。
|
||||
Wavelet 标准读路径为 **本地 RAM (L1) → Redis (L2) → Database (L3)**(由快到慢):
|
||||
|
||||
| 层级 | 技术 | 职责 |
|
||||
| :--- | :--- | :--- |
|
||||
| L1 本地 | `pkg/cache/ram`(Otter v2) | 进程内热数据,最低延迟 |
|
||||
| L2 共享 | Redis `db.GetJSON` / `SetJSON` / `HSetJSON` + `db.PrefixedKey` | 跨节点共享,带 TTL 或写穿 |
|
||||
| L3 权威 | PostgreSQL via `db.DB(ctx)` | 唯一数据源 |
|
||||
| **L1 本地** | `pkg/cache/ram` (Otter) | 进程内纳秒级极速读取,抗最高频热点流量 |
|
||||
| **L2 共享** | Redis 序列化缓存 | 跨节点共享,具备 TTL 与防击穿保护 |
|
||||
| **L3 权威** | 关系型数据库 (PostgreSQL / SQLite) | 唯一权威数据源 |
|
||||
|
||||
### 读路径模板
|
||||
### 标准接口契约 (`contracts.CacheService`)
|
||||
|
||||
```go
|
||||
func GetThingCached(ctx context.Context, key string) (Thing, error) {
|
||||
ensureThingCacheListener() // 订阅 pub/sub,仅 sync.Once
|
||||
type CacheService interface {
|
||||
// Get 从缓存获取并反序列化至 target,若不存在返回 ErrCacheMiss
|
||||
Get(ctx context.Context, key string, target any) error
|
||||
|
||||
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
|
||||
// Set 存储对象至缓存并设置 TTL
|
||||
Set(ctx context.Context, key string, value any, ttl time.Duration) error
|
||||
|
||||
// Delete 彻底移除缓存(清空本地 RAM、删除 Redis 并广播 Pub/Sub 通知全集群清空 RAM)
|
||||
Delete(ctx context.Context, key string) error
|
||||
|
||||
// GetOrSet 优先读缓存,若未命中则执行 loader 回源加载并自动回写
|
||||
GetOrSet(ctx context.Context, key string, target any, ttl time.Duration, loader func() (any, error)) error
|
||||
|
||||
// Invalidate 是 Delete 的语义别名
|
||||
Invalidate(ctx context.Context, key string) error
|
||||
}
|
||||
```
|
||||
|
||||
### 写穿(populate)
|
||||
---
|
||||
|
||||
DB miss 或业务创建成功后,**必须**回写上层:
|
||||
## 2. 业务使用标准范式
|
||||
|
||||
### 2.1 高性能读穿透 (`GetOrSet`)
|
||||
|
||||
业务 Service 推荐优先使用 `GetOrSet`,框架底层自动完成 L1/L2 穿透、回写及并发防击穿:
|
||||
|
||||
```go
|
||||
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)
|
||||
}
|
||||
func (s *OrderService) GetOrderWithCache(ctx context.Context, orderID string) (*Order, error) {
|
||||
var order Order
|
||||
cacheKey := "order:" + orderID
|
||||
|
||||
err := s.cache.GetOrSet(ctx, cacheKey, &order, 10*time.Minute, func() (any, error) {
|
||||
// Cache Miss: 执行 DB 回源查询
|
||||
var dbOrder Order
|
||||
if err := s.db.WithContext(ctx).First(&dbOrder, "id = ?", orderID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dbOrder, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &order, nil
|
||||
}
|
||||
```
|
||||
|
||||
### 失效(Invalidate)— 分布式必做三步
|
||||
### 2.2 数据变更与失效广播 (`Invalidate` / `Delete`)
|
||||
|
||||
数据变更(Admin 更新、软删除、状态迁移)时:
|
||||
|
||||
1. **本机 RAM** — `thingRAM.Invalidate(key)` 或 `InvalidateAll()`
|
||||
2. **Redis** — `Del` / `HDel` 对应 key
|
||||
3. **pub/sub 广播** — 通知**其他节点**清除 RAM(Redis 已由写节点清掉)
|
||||
凡涉及数据创建、修改、软删除、状态变更的入口(**包含 HTTP Handler、后台 Worker 任务、定时清理任务**),必须调用缓存失效:
|
||||
|
||||
```go
|
||||
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
|
||||
func (s *OrderService) UpdateOrderStatus(ctx context.Context, orderID string, newStatus string) error {
|
||||
// 1. 更新数据库权威数据
|
||||
if err := s.db.WithContext(ctx).Model(&Order{}).Where("id = ?", orderID).Update("status", newStatus).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 广播失效缓存(自动清除本机 L1、删除 Redis L2,并向集群广播 Pub/Sub 消息清空其他节点 L1)
|
||||
return s.cache.Invalidate(ctx, "order:"+orderID)
|
||||
}
|
||||
```
|
||||
|
||||
### pub/sub 监听模板
|
||||
---
|
||||
|
||||
```go
|
||||
const thingInvalidationChannel = "domain:thing_invalidation"
|
||||
## 3. 核心规则与禁止写法 (Guardrails)
|
||||
|
||||
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)
|
||||
}
|
||||
}()
|
||||
}
|
||||
```
|
||||
1. **严禁自研本地 map 缓存**:
|
||||
- 严禁在插件内编写 `sync.RWMutex + map[string]Xxx` 的裸内存缓存,无法感知多节点数据变更,必然引发多机脏读。
|
||||
2. **写路径必须全覆盖失效**:
|
||||
- 不仅在 API 修改时失效,后台 Worker、定时任务执行数据清理或变更时,必须同步触发 `cache.Invalidate`。
|
||||
3. **Key 命名空间规范**:
|
||||
- 缓存 Key 必须带插件命名空间前缀(如 `order:meta:{id}`、`auth:session:{token}`)。
|
||||
4. **不可在业务高频读接口中绕过缓存直查 DB**。
|
||||
|
||||
- 使用 `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` ✅ |
|
||||
|
||||
## 新增缓存工作流
|
||||
|
||||
1. **判定是否需要缓存**:高频读、低变更、可容忍短暂 TTL;写路径必须能统一失效。
|
||||
2. **选型 L1**:优先 `pkg/cache/ram.MustNew`;**禁止**自研 `map+mutex+TTL`,除非有充分理由并文档说明。
|
||||
3. **选型 L2**:小对象 `SetJSON`;配置类多条目用 Redis Hash(`HSetJSON`)。
|
||||
4. **定义 Redis key**:小写蛇形,带业务前缀(`upload:meta:{id}`);统一 `db.PrefixedKey`。
|
||||
5. **实现 Invalidate + pub/sub**:凡多实例部署可读的 RAM 缓存**必须**有失效广播。
|
||||
6. **挂载变更钩子**:在所有 DB 变更入口调用 Invalidate(含 Worker/定时任务,不只 HTTP Handler)。
|
||||
7. **测试**:
|
||||
- RAM hit / Redis hit / DB fallback
|
||||
- Invalidate 清 L1+L2
|
||||
- pub/sub 触发他机 RAM 失效(可用 miniredis Publish 模拟)
|
||||
- `Reset*RAMCacheForTest` 仅清本机 RAM
|
||||
8. 运行 `go test` 相关包 + `make code-check`。
|
||||
|
||||
## 变更钩子清单(上传元数据示例)
|
||||
|
||||
| 入口 | 动作 |
|
||||
| :--- | :--- |
|
||||
| `ingest.persistUploadRecord` 创建成功 | `SetUploadMetaCache` |
|
||||
| `ingest.Remove` / `RemoveOwned` | `InvalidateUploadMetaCache` |
|
||||
| `task/cleanup.go` 软删除 pending 文件 | `InvalidateUploadMetaCache` |
|
||||
| 直接 `repository.SoftDeleteUpload` | **禁止** — 必须走 `upload.Remove` |
|
||||
|
||||
## 禁止写法
|
||||
|
||||
```go
|
||||
// ❌ 自研 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`。
|
||||
|
||||
## 验证清单
|
||||
## 4. 质量与测试验证
|
||||
|
||||
```bash
|
||||
go test ./internal/repository/... ./internal/apps/upload/cache/...
|
||||
make format
|
||||
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`
|
||||
go test ./plugins/...
|
||||
```
|
||||
Reference in New Issue
Block a user