Set后Get返回false是因默认异步写入,需调用cache.Wait()确保刷盘;HTTP handler中禁用Wait(),应靠MaxCost与SetWithTTL控制;NumCounters建议为活跃key数10倍,MaxCost须按value实际字节估算;过期需手动封装ExpireAt逻辑,OnEvict需显式配置才触发。

为什么 Set 后 Get 总是返回 false?
Ristretto 的 Set() 默认走异步路径:键值先入无锁缓冲区 setBuf,再由后台 goroutine 批量刷到分片 map。没等刷完就调 Get(),必然查不到。
- 单元测试或 CLI 工具类短生命周期服务中,必须紧跟
cache.Wait()—— 否则Get()返回false是正常行为,不是 bug - HTTP handler 中绝不能每个请求都
Wait(),会串行化写入、拖垮吞吐;应靠MaxCost控制水位 +SetWithTTL()封装过期逻辑 - 高并发写入时若
Metrics.Dropped > 0,说明BufferItems太小或写入节奏远超消费能力,需调大(默认 64,建议 256–1024)
MaxCost 和 NumCounters 怎么设才不翻车?
MaxCost 是内存上限的真正约束,NumCounters 只影响 TinyLFU 准入精度——两者错配是生产环境最常见翻车点。
-
NumCounters建议设为预估活跃 key 数的 10 倍(如预计 50 万活跃 key,设5e6);设太小会让 TinyLFU 因哈希冲突误拒高频 key -
MaxCost必须按 value 实际字节估算:len(body)(HTML)、gob.EncodedLen()或unsafe.Sizeof()(结构体);别写死MaxCost: 1(1GB),否则平均 1KB/value 最多存 100 万条,而NumCounters: 1e7却在为 1000 万 key 预备,浪费且干扰淘汰 - 若缓存对象大小差异极大(如 1KB 字符串 vs 10MB protobuf),
ItemCost必须按比例映射(例如每 1KB → cost += 1),否则 ARC 淘汰会抖动、GC 压力激增
如何让缓存自动过期?Ristretto 本身不内置 TTL
Ristretto 的过期逻辑完全由用户控制,它只提供 SetWithTTL() 辅助函数(底层仍是把 expireAt 打包进 value),不依赖 GC 或定时扫描。
- 推荐封装
SetWithTTL(key, value, ttl):内部构造struct{ Data any; ExpireAt int64 }存入,Get()时先解包再比对time.Now().UnixNano() - 别依赖
OnEvict做过期清理——它只在驱逐时触发,不保证及时性;也别用 goroutine 定时遍历清理——sync.Map不支持安全迭代,ristretto.Cache也不暴露全量 key 列表 - 如果硬需求是“精确 TTL”且不想手写逻辑,应换
github.com/patrickmn/go-cache或github.com/bluele/gcache;Ristretto 的定位是“极致吞吐”,不是“功能完备”
OnEvict 回调为什么从不触发?
OnEvict 默认是 nil,Ristretto 不会主动调用它——哪怕 key 被淘汰了,你也收不到通知。
立即学习“go语言免费学习笔记(深入)”;
- 必须在
ristretto.Config中显式传入函数:OnEvict: func(key string, value any, cost int64) { log.Printf("evicted %s", key) } - 回调里做的任何操作(如释放资源、发监控)都需自行保证线程安全;Ristretto 不做额外同步
- 注意:该回调仅在驱逐发生时触发,不替代业务层的过期检查;
Get()仍需手动校验时间戳
NumCounters 影响准入,MaxCost 控制水位,ItemCost 决定淘汰权重,三者缺一不可。漏掉任意一个,缓存就退化成“看似在跑,实则无效”的黑盒。


















