不能用 sync.map 做文件缓存底座,因其无法获取 key 列表、range 无序非原子,不支持按访问顺序遍历,无法构建 lru 链表;实测导致缓存无限增长、磁盘空间只增不减,几小时即触发写入失败或 oom killer。

为什么不能用 sync.Map 做文件缓存底座
它根本拿不到 key 列表,也没法按访问顺序遍历 —— Range 无序、非原子,更没法配合 list.List 构建 LRU 链表。你存的是文件路径和 os.FileInfo,但淘汰时连“哪个最久没被访问”都算不出来。实测结果:缓存项越积越多,os.Remove 无从触发,磁盘空间只增不减,几小时后直接写入失败或被 OOM killer 干掉。
lru.Cache + 懒检查过期 + 文件元信息封装才是可控组合
推荐用 github.com/hashicorp/golang-lru/v2 的 lru.Cache,轻量、无全局锁、并发安全。关键不是直接塞字符串,而是定义结构体:
type fileEntry {
path string
size int64
modTime time.Time
expireAt time.Time
}
这样避免类型断言开销,也把真正影响淘汰的成本(磁盘占用)显式带上。实操要点:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
-
Set()只存fileEntry,不写磁盘;Get()先查expireAt.Before(time.Now()),过期就Delete()并返回未命中,不自动删文件(删不删留给业务层决定) - 淘汰回调
OnEvicted里必须调os.Stat()获取真实Size,不能信 struct 字段 —— 文件可能已被外部修改或删除 - 别在
OnEvicted里直接os.Remove,先发到一个带 buffer 的 channel,由独立 goroutine 批量执行,防阻塞
磁盘水位信号必须走系统调用,不能靠内存指标
runtime.ReadMemStats 对磁盘空间完全无感。真实瓶颈是挂载点可用字节,必须用系统调用:
- Linux/macOS:用
unix.Statfs("/path/to/cache/dir", &stat),算stat.Avail * uint64(stat.Bsize) - 采样周期设为 3 秒起一个 goroutine 单独轮询;失败时 fallback 到上次成功值,避免启动卡住
-
evictByLRU必须支持批量删除 —— 淘汰时不能只删Back()一个节点,得一口气清掉若干个,否则水位下降太慢 - Put 前检查:
if diskAvail ,n 至少为 5~10,别贪小
归档与清理要分两路走,别混在一起
文件缓存的“自动清理”常被误当成“定时删旧文件”,其实二者目标不同:缓存淘汰是保容量,归档是保合规性。错用 filepath.Walk 或硬解析文件名,容易静默失败。
- 归档任务用
robfig/cron/v3调度 +os.ReadDir遍历,取entry.Info().ModTime(),别抠文件名 - 跨设备移动前,先
os.SameFile检查源/目标是否同挂载点;不是就改用io.Copy+os.RemoveAll组合 - 清理操作(如删冷文件)绝不能塞进 HTTP handler 或主 goroutine;必须扔进独立 goroutine,并用
runtime.Gosched()分批让出调度权
真正难的不是写逻辑,是让磁盘水位、访问热度、文件生命周期三者在淘汰时对齐 —— 任何一环掉链子,缓存就会从加速器变成定时炸弹。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










