// Copyright 2026 tamakyi. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. // Package cache 提供进程内、并发安全的短 TTL 缓存。 // // 适用场景:结果聚合统计等"非强一致"数据,用短 TTL 换取数据库实时重算压力的显著降低 // (投票结果页、探索页热门投票等)。需注意:进程内存缓存在多实例横向扩容时不会共享, // 因此仅用于可接受短暂陈旧的数据;强一致数据请勿使用本缓存。 package cache import ( "sync" "time" ) type ttlEntry struct { value interface{} expiresAt time.Time } // TTLCache 并发安全的带 TTL 进程内缓存。 type TTLCache struct { mu sync.RWMutex items map[string]ttlEntry maxItems int } // NewTTLCache 创建缓存实例。maxItems<=0 时使用默认容量 256;达到容量上限时, // 先淘汰过期项,若仍满则整表清空(避免内存无限增长,短 TTL 下过期项通常已占多数)。 func NewTTLCache(maxItems int) *TTLCache { if maxItems <= 0 { maxItems = 256 } return &TTLCache{ items: make(map[string]ttlEntry, maxItems), maxItems: maxItems, } } // Get 读取缓存;命中且未过期返回值,否则返回 (nil, false)。 func (c *TTLCache) Get(key string) (interface{}, bool) { c.mu.RLock() e, ok := c.items[key] c.mu.RUnlock() if !ok { return nil, false } if time.Now().After(e.expiresAt) { c.mu.Lock() // 双重检查,避免并发重复删除(无影响,仅兜底) if cur, still := c.items[key]; still && time.Now().After(cur.expiresAt) { delete(c.items, key) } c.mu.Unlock() return nil, false } return e.value, true } // GetWithExpiry 读取缓存值与其过期时间,**不判断、不删除**过期条目。 // // 供 stale-while-revalidate 使用:调用方拿到过期的旧值后可以先返回给用户, // 再在后台重算。Get 做不到这一点——它遇到过期项就直接删掉并返回 false, // 于是「过期」和「不存在」变成同一个结果,调用方只能同步等待重算。 func (c *TTLCache) GetWithExpiry(key string) (value interface{}, expiresAt time.Time, ok bool) { c.mu.RLock() defer c.mu.RUnlock() e, ok := c.items[key] if !ok { return nil, time.Time{}, false } return e.value, e.expiresAt, true } // Expire 把条目标记为「已过期」但**保留其值**。 // // 与 Delete 的区别很关键:Delete 之后下次读是彻底 miss,调用方只能同步重算; // Expire 之后下次读仍然拿得到旧值,于是可以「先返回旧值 + 后台刷新」。 // 写多读多的场景(例如投票:每来一票都要让结果失效)只有用 Expire 才不会 // 把每次写入都变成下一次读的同步 O(N) 重算。 // key 不存在或已过期时什么也不做。 func (c *TTLCache) Expire(key string) { c.mu.Lock() defer c.mu.Unlock() e, ok := c.items[key] if !ok { return } e.expiresAt = time.Now().Add(-time.Second) c.items[key] = e } // Set 写入缓存并设定过期时间。 func (c *TTLCache) Set(key string, value interface{}, ttl time.Duration) { c.mu.Lock() defer c.mu.Unlock() if len(c.items) >= c.maxItems { // 先淘汰过期项 now := time.Now() for k, v := range c.items { if now.After(v.expiresAt) { delete(c.items, k) } } // 仍满(多为短 TTL 下瞬时高频写入):整表重建,保证写入成功且不 OOM if len(c.items) >= c.maxItems { c.items = make(map[string]ttlEntry, c.maxItems) } } c.items[key] = ttlEntry{value: value, expiresAt: time.Now().Add(ttl)} } // Delete 删除指定键的缓存项;键不存在时静默返回。 // 用于写穿式失效:数据变更(如新增投票提交)后立即清除对应缓存,避免结果页短暂陈旧。 func (c *TTLCache) Delete(key string) { c.mu.Lock() defer c.mu.Unlock() delete(c.items, key) }