优先用ristretto;若自研,仅lru.cache加懒检查过期可控,其余方案易oom。因sync.map无访问顺序、遍历无序且不支持原子淘汰操作,无法满足淘汰/过期/频次需求,导致内存只增不减。

别自己封装淘汰逻辑,优先用 ristretto;若必须自研,lru.Cache + 懒检查过期是唯一可控路径,其余方案大概率线上 OOM。
为什么 sync.Map 不能当缓存底座
sync.Map 不记录访问顺序,Range 遍历无序且非原子,没法配合 list.List 做 LRU 排序。它适合存开关、白名单这类几乎不变的只读数据。但凡带「淘汰」「过期」「访问频次」任一需求,就会出现缓存项看似存在、实际淘汰完全随机、内存只增不减的情况。压测几小时就触发 OOM killer,日志里看不到明显错误,只有 runtime: memory allocated 持续上涨。
lru.Cache 怎么加过期而不踩坑
第三方库 github.com/hashicorp/golang-lru/v2 的 lru.Cache 本身不带 TTL,但结构干净、无全局锁、支持并发读写。加过期只需两步,且必须严格遵循:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
-
Set时 value 类型定义为结构体,含expireAt time.Time字段(不用int64时间戳,避免时区和纳秒精度问题) -
Get时先取值,再立刻调expireAt.Before(time.Now())判断;过期则Delete并返回未命中 - 绝不自动在
Get里删 key 后返回nil, false——并发Get可能同时触发删除,导致日志刷屏、监控误报 - 淘汰回调里不执行任何阻塞操作(如 HTTP 调用、文件 IO),只做
map删除和简单字段清理
ristretto 初始化和使用关键点
它是目前 Go 生态唯一把 TTL + cost 控制 + 无锁读 + 异步清理做对的库。但用错参数照样翻车:
- 初始化别设
NumCounters: 1e7,小对象(如 token string)会撑爆内存;改用MaxCost: 100 * 1024 * 1024(100MB)更靠谱 -
Set时传ristretto.TTL,底层自动处理懒删和驱逐,不用手写time.Now().Add() - 默认禁用后台清理(
Policy: false),如需主动回收,显式调Close()或定期ResetMetrics() - 它不暴露淘汰 key 给回调函数,所以不适合需要拿到 key 做额外清理(如删文件、发通知)的场景
系统内存压力感知不是可选项
runtime.ReadMemStats 只反映 Go 堆内存,不包含 OS 进程、栈、mmap、CGO 及内核缓冲区。即使它显示 HeapInuse 才 200MB,/proc/meminfo 中 MemAvailable 也可能已跌破 5%,OOM killer 随时触发。正确做法是:
- Linux 下用
unix.Statfs或直接读/proc/meminfo解析MemAvailable - macOS 下用
host_statisticssyscall 获取可用内存 - 采样周期设为 1–3 秒;太短加重系统负担,太长响应滞后
- 在
Put和后台 goroutine 中注入判断:若当前MemAvailable低于阈值,强制调evictByLRU批量淘汰(不是只删一个Back()节点)
真正难的不是实现算法,而是控制淘汰副作用:GC 压力、锁争用、过期时机不可控、并发删除冲突——这些细节藏在每行 map 操作和每次 time.Now() 调用里。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










