sync.Map 不适合文件缓存淘汰,因其不支持按访问顺序遍历、Range 随机、无法原子完成查路径→移链表头→更新 mtime,导致淘汰随机、内存只增不减;应改用 lru.Cache 加文件元信息懒检查,并通过 /proc/meminfo 的 MemAvailable 感知真实系统内存压力。

为什么不能用 sync.Map 做文件缓存淘汰
它根本不支持按访问顺序遍历,Range 返回 key 的顺序是随机的,没法做 LRU/LFU 淘汰;也没法原子完成「查文件路径 → 移到链表头 → 更新 mtime」这个最小操作单元。实测中缓存项看似存在,但淘汰完全随机,小文件反复加载后内存只增不减,压测几小时就 OOM。它适合存配置开关这类几乎不变的数据,但凡涉及「最近访问」「过期时间」「文件大小权重」,就必须换掉。
lru.Cache + 文件元信息懒检查是最稳的手动组合
第三方库 github.com/hashicorp/golang-lru/v2 的 lru.Cache 本身不带 TTL,但结构干净、无全局锁、支持并发读写。加文件缓存逻辑只需两步:
-
Set时存value(比如[]byte或*os.File)和stat结构体(含Size()、ModTime()、IsDir()),别只存路径字符串——否则每次Get都要os.Stat,IO 开销翻倍 -
Get时先取值,再立刻检查stat.ModTime().Before(time.Now().Add(-24 * time.Hour))(按需调整过期窗口),过期就Delete并返回未命中
绝不自动在 Get 里删 key 后返回 nil, false——并发 Get 可能同时触发删除,导致日志刷屏、监控误报;应先读、再判、再删,靠 map + RWMutex 保证原子性。
如何让淘汰感知真实系统内存压力而非仅看缓存项数
runtime.ReadMemStats 只反映 Go 堆内存,漏掉了 mmap 分配的文件映射、内核 page cache、buffers 等关键内存,线上服务常因此被 OOM killer 杀掉。正确做法是读取 /proc/meminfo 中的 MemAvailable(Linux)或调 host_statistics(macOS):
立即学习“go语言免费学习笔记(深入)”;
- 在缓存结构体中加字段:
memCheckFn func() uint64和memThreshold uint64(如512 * 1024 * 1024) -
Put开头加检查:if c.memCheckFn() (淘汰 30% 最久未用项) - 启独立 goroutine,每 2 秒调
memCheckFn并动态缩放memThreshold(高负载时阈值上浮 20%) -
evictByLRU必须支持批量淘汰,避免循环Back()+Remove导致锁持有过久
文件缓存的淘汰不能只比访问时间,还得加 size 成本权重
纯 LRU 会让一个 100MB 的旧日志文件长期霸占缓存,挤掉几十个高频小配置文件。ristretto 支持基于 cost 的驱逐(比如设 MaxCost: 100 * 1024 * 1024),底层按文件 Size() 计费,天然适配文件场景。若手写,必须把 size 作为淘汰因子:
- 每个缓存项额外存
size int64字段,不是靠len([]byte)临时算——避免重复 allocation - 淘汰逻辑从「删最久未用」升级为「删最久未用 × size 最大」,即按
atime × size加权排序 - 用
container/heap构建最小堆,元素为{key string, weight int64},插入时更新权重,淘汰时 Pop 有效项(注意懒删除)
真正难的不是实现加权逻辑,而是 os.Stat 调用时机和 mmap 内存归属——文件缓存项若包含 *os.File 或 mmap 地址,其内存不计入 runtime.ReadMemStats,但会真实消耗物理内存。这点极易被忽略,直到 OOM 才发现。


















