直接用 github.com/hashicorp/golang-lru/v2 最稳妥,它已解决并发、生命周期同步、淘汰边界等所有易错点;手写 container/list + map 极易因指针不同步、并发修改、淘汰顺序错误导致 panic 或数据静默丢失。

直接用 github.com/hashicorp/golang-lru/v2,别手写
它已解决所有高并发下易崩的点:map 与 list.Element 生命周期不同步、MoveToFront 并发 panic、淘汰时 map 残留 key 导致静默丢数据。自己用 container/list + map 封装,哪怕加了 sync.Mutex,在压测中仍大概率触发 panic: assignment to entry in nil map 或返回零值却不报错。
lru.New[string, string](128) 必须显式指定泛型参数
省略泛型或写成 any 会导致 Get 返回 any,类型断言失败时静默返回空字符串,缓存“命中”但内容为空——这种错误线上极难定位。
-
lru.New[string, int](128)和lru.New[string, []byte](128)是完全不同的类型,不能混用 -
Add("k", "v")是覆盖写入:key 存在时自动更新 value 并重置访问序,不用先Remove再Add - 容量满时自动淘汰
list.Back()对应项,行为不可配置;若需淘汰前落盘或打点,必须传onEvicted回调
Resize(256) 不是扩容,是重建
调用后会新建链表、按当前访问序迁移存活 key-value,旧结构交由 GC 回收。这个过程全程持有写锁,期间所有 Get 和 Add 都会阻塞。
- 已有 10 万条目时,
Resize耗时可达毫秒级,HTTP handler 中慎用 - 迁移后所有 key 的访问热度清零,依赖历史频次做降级(如“访问 ≥3 次才预热”)的逻辑会失效
- 不建议在请求路径中动态
Resize;如需弹性容量,应提前预估并固定 size
onEvicted 回调里不能做任何阻塞操作
该函数在持有缓存锁的上下文中同步执行,HTTP 请求、DB 查询、time.Sleep 都会拖慢全部缓存操作,甚至引发 goroutine 积压。
立即学习“go语言免费学习笔记(深入)”;
- 正确做法:只做轻量记录(如
log.Printf)、发通知(select { case ch ) - 重逻辑(如写磁盘、发 MQ)必须异步投递,且要防背压——比如用带缓冲的 channel 或 worker pool
- 回调内禁止再调用该缓存实例的
Get/Add,否则死锁
真正容易被忽略的是:onEvicted 的触发时机和锁持有状态。它不是“淘汰完成后回调”,而是在锁内完成节点移除后、释放锁前调用——这个细节决定了你能不能在里面安全地读取其他共享状态,也决定了为什么阻塞操作会卡住整个缓存。
文章来自机圈观察员网,发布者:,转载请注明出处:https://www.jqgcy.com/xinjizixun/123781.html