package proxy import ( "sync" "time" "github.com/openteam/server/internal/store" ) // 鉴权缓存:每个代理请求都要用密钥哈希查 api_keys、再用 user_id 查 users, // 这两条随机读在 profile 里占请求路径 CPU 的 ~70%。 // // 缓存的是「密钥 + 用户」这两行,但**状态与过期每次请求都重新判定**: // 密钥停用、用户停用、密钥过期都按缓存值判断,最长延迟就是 TTL。 // 因此 TTL 要短(默认 2s),且管理端/用户的写操作会立即清空缓存。 // // 余额也随用户行一起缓存:它只用于「余额 ≤ 0 就 402」这道软闸, // 最长 2s 的滞后不会造成实际损失(请求照样计费,余额允许为负)。 const ( authCacheMax = 8192 authCacheDefault = 2 * time.Second ) type authEntry struct { key store.APIKey user store.User until time.Time } type authCache struct { mu sync.RWMutex m map[string]authEntry ttl time.Duration // touch 每个密钥最近一次写 last_used_at 的时间。行被缓存后它自己是不会变的, // 节流必须按这里的记录判断,否则每请求都会写一次库(反而比不缓存更糟)。 // // 用 sync.Map 而不是和 m 共用一把锁:这个表是「每请求读、每分钟写一次」的 // 读多写少结构。早先共用一个 Mutex 时,16 并发下每个请求都要抢排他锁, // 直接把吞吐从 31k 拖到 3.2k(Goroutine 互斥饥饿,p99 7ms)。 touch sync.Map // keyID(uint64) → time.Time } func newAuthCache(ttl time.Duration) *authCache { if ttl < 0 { ttl = 0 } return &authCache{m: map[string]authEntry{}, ttl: ttl} } // on 缓存是否启用。 func (a *authCache) on() bool { return a != nil && a.ttl > 0 } func (a *authCache) get(hash string) (store.APIKey, store.User, bool) { if !a.on() { return store.APIKey{}, store.User{}, false } a.mu.RLock() e, ok := a.m[hash] a.mu.RUnlock() if !ok || time.Now().After(e.until) { return store.APIKey{}, store.User{}, false } return e.key, e.user, true } func (a *authCache) put(hash string, k store.APIKey, u store.User) { if !a.on() { return } a.mu.Lock() defer a.mu.Unlock() if len(a.m) >= authCacheMax { now := time.Now() for h, e := range a.m { if now.After(e.until) { delete(a.m, h) } } if len(a.m) >= authCacheMax { return // 全是活的:放弃本次缓存,下次请求再查库 } } a.m[hash] = authEntry{key: k, user: u, until: time.Now().Add(a.ttl)} } // shouldTouch 该密钥现在是否应该写一次 last_used_at(每 every 最多一次)。 // 快路径是 sync.Map 的无锁读。 func (a *authCache) shouldTouch(keyID uint64, every time.Duration) bool { if a == nil { return true } now := time.Now() if v, ok := a.touch.Load(keyID); ok { if t, ok := v.(time.Time); ok && now.Sub(t) < every { return false } } a.touch.Store(keyID, now) return true } // clear 清空缓存(密钥/用户/渠道等发生写操作后调用)。 func (a *authCache) clear() { if a == nil { return } a.mu.Lock() a.m = map[string]authEntry{} a.mu.Unlock() }