Go Ristretto 缓存未满时条目为什么也可能不被接纳
Go Ristretto 的缓存成本还没达到 MaxCost,并不代表每一次 Set 都一定能在缓存里找到。Ristretto 为了吞吐量使用异步写入和 TinyLFU admission:新条目可能先在 Set 缓冲阶段被丢弃,也可能在准入阶段因为已有条目的频率估计更有价值而不被接纳。
官方仓库明确说明,新的 Set 可能在经过 Set buffer 或 admission policy 时被丢掉;更新已有条目则有不同保证。排查时要先等待异步路径完成,再看指标区分“还没可见”和“确实被丢弃”。
官方地址:https://github.com/dgraph-io/ristretto
MaxCost是成本边界,不是新键的无条件接纳开关。- 新条目可能在 Set 缓冲或 TinyLFU admission 阶段被丢弃。
Set是异步路径,立即Get不能直接证明准入失败。- 先调用
Wait,再结合SetsDropped与SetsRejected观察写入结果。
先分清缓存未满与条目一定接纳
很多缓存的直觉是“还有空间就放进去”,但 Ristretto 的设计目标不是把每个新键都保存下来,而是用 admission policy 把有限空间留给更可能被再次访问的键。MaxCost 只约束缓存总成本;它回答的是“最多可以占用多少成本”,不回答“当前这个新键是否值得接纳”。
因此,下面两件事可以同时成立:缓存的当前成本还远低于 MaxCost,某个新键却没有进入缓存;另一个已经存在的键再次更新时,写入行为又与新键不同。不要把前一个现象直接归因于容量不足。
| 观察到的现象 | 更接近的解释 | 第一步怎么做 |
|---|---|---|
| Set 后立刻 Get 不到 | 异步写入尚未完成,也可能最终被丢弃 | 先调用 Wait |
| Wait 后新键仍不存在 | 可能在缓冲或 admission 阶段被丢弃 | 读取丢弃/拒绝指标 |
| 缓存未满但新键反复进不去 | TinyLFU 判断其频率价值低,或写入路径持续被丢弃 | 检查访问模式与 cost |
拆开 Set 缓冲和 TinyLFU admission 两道关口
可以把新条目想成要经过两道静态边界。第一道是 Set buffer:Ristretto 用批量化减少并发写入的争用,条目在真正进入后续策略前可能已经被丢弃。第二道是 TinyLFU admission:它根据访问频率估计判断新条目是否值得进入有限缓存空间。

这里的关键是:MaxCost 主要参与成本和淘汰决策,不会把 TinyLFU admission 变成“只要没满就收”。当条目的访问频率估计较低,而缓存里已有条目的保留价值更高时,新条目可以被拒绝;这正是缓存未满时也可能看不到它的原因之一。
还要注意新键与更新的区别。官方 FAQ 说明,Ristretto 不保证新的 Set 一定进入缓存,但更新已有条目有不同保证。因此,对关键状态不能把一次针对新键的 Set 当成可靠持久化写入。
用 Wait 与 Metrics 确认观察时机和原因
最小排查代码应该先让写入路径走完,再读取结果。下面的示例使用 Ristretto v2 的泛型配置;注释只解释与本问题有关的异步边界和指标含义。
package main
import (
"fmt"
"github.com/dgraph-io/ristretto/v2"
)
func main() {
cache, err := ristretto.NewCache(&ristretto.Config[string, string]{
NumCounters: 1000, // 为 admission 和 eviction 保存访问频率计数。
MaxCost: 100, // 用统一 cost 表示缓存允许的总成本。
BufferItems: 64, // 批量处理 Set/Get,减少并发路径的争用。
})
if err != nil { panic(err) }
defer cache.Close() // 结束时关闭后台资源,避免示例提前退出。
cache.Set("cold-key", "value", 1) // 新键先进入异步写入路径。
cache.Wait() // 等待缓冲处理完成,再观察最终可见性。
value, ok := cache.Get("cold-key")
fmt.Printf("value=%q present=%v\\n", value, ok)
}
Wait 解决的是观察时机,不是强制 admission 接受。它返回后仍然读不到 cold-key,只能说明这次写入没有产生可见条目;要判断是缓冲丢弃还是准入拒绝,还需要看 Metrics。
Ristretto 的指标 API 提供了 SetsDropped、SetsRejected 等计数。下面的函数把它们放到同一组观察里;它不伪造运行结果,实际数值应由服务自己的负载产生。
func printAdmissionMetrics(cache *ristretto.Cache[string, string]) {
metrics := cache.Metrics
// 先打开 Metrics,再读取计数;生产环境应在创建 Config 时按需启用指标。
if metrics == nil {
fmt.Println("metrics disabled")
return
}
// dropped 更接近缓冲路径丢弃,rejected 更接近 admission 策略拒绝。
fmt.Printf("sets_dropped=%d sets_rejected=%d\\n",
metrics.SetsDropped(), metrics.SetsRejected())
}

如果要在排查中建立前后对照,可以在一段固定负载前后分别读取计数,再结合 Wait 后的 Get 结果判断。不要只看缓存当前成本,也不要把单次 Get miss 当成某一个阶段的唯一证据。
按条目热度和成本调整调用策略
理解 admission 之后,处理方式通常不是简单把 MaxCost 调大。更实用的调整顺序如下:
- 确认是新键还是更新。如果业务要求更新一个已经存在的缓存条目,先确认调用方使用的是稳定键;键每次变化,Ristretto 会把它当成新条目处理。
- 为异步路径留出等待点。在测试或一次性任务里调用
Wait后再读取;在线服务里不要为了每次请求同步等待,而应把缓存命中视为可选优化。 - 检查 cost 是否反映真实代价。如果不同条目的内存或业务成本差异很大,统一写成 1 会让 eviction 的判断失真;但 cost 也不能随意放大,否则少量大条目可能挤占更多空间。
- 让热点键有重复访问机会。Ristretto 的 admission 依赖访问频率估计,只有写入一次、之后从不读取的冷键被丢弃并不一定是故障,而可能是策略正在保护热点数据。
如果业务不能接受新条目偶尔不在缓存中,就不能把 Ristretto 当作数据的唯一存储。缓存 miss 必须能回源,回源后再写入;关键数据仍应落在数据库、文件或其他明确的持久化系统中。
常见问题
缓存未满,为什么还会拒绝新键?
因为 admission 关注的是条目的保留价值,不只看剩余容量。Ristretto 的 TinyLFU 会参与新条目是否进入缓存的判断。
调用 Wait 后还读不到,是不是 MaxCost 太小?
不一定。Wait 只能排除部分异步时序影响;还要结合 SetsDropped、SetsRejected、条目的 cost 和访问模式判断。
把 MaxCost 调大能彻底解决吗?
不能保证。容量增大可能减少淘汰,但不会把 admission 规则改成无条件接纳;如果问题是新键冷、写入只发生一次或缓冲路径丢弃,单纯扩容并不对症。
Ristretto 适合保存不能丢的数据吗?
不适合把缓存当唯一存储。Ristretto 的新条目 Set 本来就不保证一定进入缓存,关键数据应保存在有明确持久化语义的系统中。
总结一下:先用 Wait 排除观察过早,再用 Metrics 区分缓冲丢弃与 admission 拒绝,最后根据键的热度和 cost 调整策略。看到“缓存还没满但 Get 不到”时,优先检查 Ristretto 的写入语义,而不是只盯着 MaxCost。
Go Ristretto 怎么开启指标并计算缓存命中比例
- 上一篇
- Go Ristretto 怎么开启指标并计算缓存命中比例
- 下一篇
- Go Ristretto Set 返回后立刻 Get 为什么可能取不到
-
- Golang · Go问答 | 24分钟前 |
- Go Excelize Excel 日期数字与格式化日期有什么区别
- 186浏览 收藏
-
- Golang · Go问答 | 25分钟前 |
- Go Excelize 读取公式单元格时为什么拿到旧结果
- 326浏览 收藏
-
- Golang · Go问答 | 32分钟前 |
- Go go-toml 空值字段为什么仍被编码到 TOML
- 488浏览 收藏
-
- Golang · Go问答 | 34分钟前 | go · 版本迁移 · Go go-toml go-toml v1 go-toml v2 Tree 结构体解码 TOML迁移
- Go go-toml 从 v1 树模型迁移到 v2 结构体解码要改什么
- 286浏览 收藏
-
- Golang · Go问答 | 38分钟前 | go · 时区 · localdatetime time.Time Go go-toml 本地日期时间 带时区时间
- Go go-toml 本地日期时间和带时区时间有什么区别
- 419浏览 收藏
-
- Golang · Go问答 | 48分钟前 | 配置管理 · go · Go Koanf Koanf 配置合并 StrictMerge Go 配置覆盖
- Go Koanf 不同来源的同名键类型不同会怎样合并
- 366浏览 收藏
-
- Golang · Go问答 | 56分钟前 | 缓存 · go · Go Ristretto NumCounters MaxCost
- Go Ristretto NumCounters 和 MaxCost 分别约束什么
- 159浏览 收藏
-
- Golang · Go问答 | 58分钟前 |
- Go Ristretto Set 返回后立刻 Get 为什么可能取不到
- 395浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go Badger 迭代器不关闭为什么会影响资源释放
- 193浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go Badger ValueCopy 和 Value 回调的生命周期有什么区别
- 159浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 460次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 522次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 541次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 488次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 307次使用
-
- 用Nginx反向代理部署go写的网站。
- 2023-01-17 502浏览
-
- GoLand调式动态执行代码
- 2023-01-13 502浏览
-
- Go crypto/rand.Text 的长度为什么不是固定字符数
- 2026-10-04 501浏览
-
- Go strings.ToValidUTF8 清洗日志内容的边界
- 2026-10-03 501浏览
-
- Go tls.GetCertificate 为什么收不到空 ServerName 请求
- 2026-09-27 501浏览

