// Package cache 提供进程内 TTL 缓存与 singleflight 合并,服务 OCI 读放大 // 与审计原始事件暂存等场景;单进程部署,无跨进程共享需求(调研①方案 A)。 package cache import ( "math/rand/v2" "strings" "sync" "time" ) type entry struct { val any exp time.Time } // flight 是一次进行中的回源;后到的同 key 调用等待它完成。 type flight struct { wg sync.WaitGroup val any err error } // Cache 是带键数上限的进程内 TTL 缓存;并发 miss 同 key 只回源一次。 type Cache struct { mu sync.Mutex items map[string]entry flights map[string]*flight max int } // New 创建缓存;max 为键数上限,到限时先清过期项、仍满则任意驱逐。 func New(max int) *Cache { return &Cache{items: map[string]entry{}, flights: map[string]*flight{}, max: max} } // Get 返回未过期的缓存值。 func (c *Cache) Get(key string) (any, bool) { c.mu.Lock() defer c.mu.Unlock() e, ok := c.items[key] if !ok || time.Now().After(e.exp) { return nil, false } return e.val, true } // Set 写入缓存;TTL 附加至多 1/10 的随机抖动,避免整批同时过期。 func (c *Cache) Set(key string, val any, ttl time.Duration) { c.mu.Lock() defer c.mu.Unlock() if len(c.items) >= c.max { c.evictLocked() } jitter := time.Duration(rand.Int64N(int64(ttl)/10 + 1)) c.items[key] = entry{val: val, exp: time.Now().Add(ttl + jitter)} } // evictLocked 先清全部过期项,仍到上限则任意删除腾出空位。 func (c *Cache) evictLocked() { now := time.Now() for k, e := range c.items { if now.After(e.exp) { delete(c.items, k) } } for k := range c.items { if len(c.items) < c.max { return } delete(c.items, k) } } // DeletePrefix 删除全部以 prefix 开头的键(写操作后按租户前缀失效)。 func (c *Cache) DeletePrefix(prefix string) { c.mu.Lock() defer c.mu.Unlock() for k := range c.items { if strings.HasPrefix(k, prefix) { delete(c.items, k) } } } // join 登记或加入 key 的进行中回源;返回 true 表示已有同 key 回源在跑。 func (c *Cache) join(key string) (*flight, bool) { c.mu.Lock() defer c.mu.Unlock() if f, ok := c.flights[key]; ok { return f, true } f := &flight{} f.wg.Add(1) c.flights[key] = f return f, false } // settle 结束回源登记并唤醒等待者。 func (c *Cache) settle(key string, f *flight) { c.mu.Lock() delete(c.flights, key) c.mu.Unlock() f.wg.Done() } // Do 返回缓存值;miss 时经 singleflight 回源,成功写缓存,错误不缓存 // (等待者共享同一错误,下次调用重新回源)。 func Do[T any](c *Cache, key string, ttl time.Duration, fn func() (T, error)) (T, error) { var zero T if v, ok := c.Get(key); ok { return v.(T), nil } f, joined := c.join(key) if joined { f.wg.Wait() if f.err != nil { return zero, f.err } return f.val.(T), nil } f.val, f.err = fn() c.settle(key, f) if f.err != nil { return zero, f.err } c.Set(key, f.val, ttl) return f.val.(T), nil }