lfu的核心逻辑是先比较访问频次,频次相同时再比较最近一次访问时间。go标准库无现成实现,需自定义heap.interface并在less方法中嵌套判断频次与时间。

LFU 的核心逻辑不是只看访问次数
LFU(Least Frequently Used)容易被误解为“谁访问次数最少就淘汰谁”,但真实场景中必须处理「次数相同」时的次级排序。比如两个 key 都被访问过 1 次,一个刚写入,一个半小时前写入——按纯次数无法决定淘汰顺序。标准 LFU 要求:**先比访问频次;频次相同时,再比最近一次访问时间(LRU fallback)**。漏掉这层,缓存会随机丢数据,命中率波动剧烈。
Go 标准库没有现成 LFU 实现,container/heap 可以做频次堆,但无法直接支持“频次相同 → 比时间”这种复合排序。你需要自己实现 heap.Interface,在 Less 方法里嵌套判断:
func (h *lfuHeap) Less(i, j int) bool {
if h.items[i].freq != h.items[j].freq {
return h.items[i].freq
<h3>map + heap 组合必须配双向链表或时间戳字段</h3>
<p>只用 <code>map[key]*heapItem</code> 加 <code>heap</code> 会导致 Get 时无法 O(1) 更新频次和时间:heap 不支持按 key 查找节点,你得遍历整个 heap 找对应项,复杂度退化到 O(n)。实际必须加一层间接引用:</p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/xiazai/skill8661" title="Golang Uber Dig"><img
src="https://img.php.cn/upload/skill/000/000/081/179215391842073.jpg" alt="Golang Uber Dig" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/xiazai/skill8661" title="Golang Uber Dig" class="overflowclass">Golang Uber Dig</a>
<p class="overflowclass">使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等</p>
</div>
<a rel="nofollow" href="/xiazai/skill8661" title="Golang Uber Dig" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div>
- 每个
heapItem存key和指针*list.Element,让 map 直接映射到链表节点; - 或者在
heapItem里存完整value和lastAccess time.Time,Get 时直接更新该 struct 字段,再调用heap.Fix重排; - 别用
sync.Map—— 它不支持遍历,你连 heap 里所有元素都拿不到,更没法做Fix或批量清理。
Set/Get 时的频次更新必须原子且无锁竞争
并发下多个 goroutine 同时 Get("user_123"),会触发多次频次+1 和时间更新。如果只靠 map + sync.RWMutex,写操作会被串行化,热点 key 成为性能瓶颈。可行解法:
- 对每个 key 做分片锁(如
shard[uint64(key)%32]),降低锁争用; - 频次字段用
atomic.Int64,避免锁;但lastAccess仍需锁保护(time.Time 不是原子类型); - 更稳的做法:把频次更新和时间更新合并进一次写锁,不在
Get中做任何 heap 重排,只标记“需刷新”,由后台 goroutine 定期批量Fix—— 这牺牲一点实时性,换高吞吐。
生产环境别硬刚 LFU,优先考虑 ristretto 或 lru.Cache + 访问计数
ristretto 本身不直接暴露 LFU,但它支持基于 Cost 的驱逐,你可以把 freq 编码进 cost 值(如 cost = 1000000 / (freq + 1)),配合 TTL 实现近似 LFU 行为。而 github.com/hashicorp/golang-lru/v2 的 lru.Cache 虽无原生 LFU,但结构干净、支持并发读写,你可以在其上包一层:用独立 map[key]atomic.Int64 记录频次,淘汰时扫描 top-N 最低频次项 —— 这比从零造轮子快得多,也更容易压测和监控。
真正难的不是实现频次计数,而是当缓存满时,如何在毫秒级内选出“最该淘汰的那几个”,还不阻塞后续请求。这个决策延迟,往往比算法本身更影响服务 SLA。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










