FreeCache需预分配内存并调低GC频率,键值必须为[]byte且序列化,分片锁支持高并发读写,过期与淘汰为惰性近似LRU,监控应侧重HitCount/MissCount而非EntryCount。

初始化时必须预分配内存并调低GC频率
FreeCache 的内存是 preallocated 的,一旦创建就立即占用指定大小——比如 freecache.NewCache(100 * 1024 * 1024) 会立刻申请 100MB 连续内存。若不调整 GC 行为,Go 运行时可能因“看似空闲但实际被 FreeCache 占用”的内存而误判,导致 GC 频率异常升高、停顿变长。
实操建议:
- 务必在
cache := freecache.NewCache(...)后紧接着调用debug.SetGCPercent(20)(甚至更低,如5),避免 GC 扫描整个缓存区域 - 缓存大小要预留余量:实际业务数据 + 索引结构开销 ≈ 1.1–1.2 倍配置值;别刚好卡死 100MB,否则 LRU 淘汰会更激进
- 不要在测试环境用小内存(如 1MB)验证逻辑——分片数固定为 256,过小内存会导致段内环形缓冲区(
RingBuf)频繁覆盖,命中率骤降
键和值必须是 []byte,序列化不能省略
FreeCache 底层只接受 []byte 类型的 key 和 value,不支持结构体、map 或 interface{}。任何非字节切片数据都得先序列化,否则编译或运行时直接 panic。
常见错误现象:
立即学习“go语言免费学习笔记(深入)”;
-
cache.Set("user:123", userStruct, 300)→ 编译失败:类型不匹配 -
cache.Set([]byte("user:123"), fmt.Sprintf("%v", userStruct), 300)→ 运行时可能因字符串逃逸触发额外 GC,抵消零GC优势
实操建议:
- 统一用
encoding/json或更高效的gogoproto/msgpack序列化,避免fmt.Sprintf或strconv临时分配 - key 尽量短且确定性哈希稳定,例如用
sha256.Sum256([]byte("user:" + uid)).[32]byte转[]byte,避免中文或特殊字符导致 hash 分布倾斜 - 对高频小对象(如 token 字符串),可复用
sync.Pool缓存序列化后的[]byte,减少堆分配
高并发下 Get/Set 不需要额外加锁,但 Touch 和 Del 要注意返回值
FreeCache 内部按 key 的 hash 分成 256 个 segment,每个 segment 独占一把 sync.Mutex。这意味着 99% 场景下,不同 key 的读写天然隔离,你不用包一层 sync.RWMutex。
但容易踩的坑在于:
-
cache.Touch(key, newExpire)在 key 不存在时返回ErrKeyNotFound,不是静默失败;若没检查 err,后续逻辑可能误认为“已续期成功” -
cache.Del(key)返回int:1 表示删除成功,0 表示 key 不存在——别当成 bool 用 -
cache.GetOrSet是原子操作,但若 value 序列化耗时长,会阻塞该 key 对应 segment 的所有其他操作(因为同一 segment 共享锁)
实操建议:
- 对关键路径上的
Touch和Del,必须显式判断 error 或返回值,例如:if affected := cache.Del(key); affected == 0 { log.Warn("key not found") } - 避免在
GetOrSet的 value 构造逻辑里做 DB 查询或 HTTP 调用——应先在外层准备好[]byte,再传入
过期和淘汰是近似LRU,不能依赖精确时间或顺序
FreeCache 的过期不是定时扫描,而是“访问时惰性检查”:每次 Get 都会比对 entry 的 timestamp 和当前时间;淘汰也不是严格 LRU,而是基于环形缓冲区写入位置的近似策略——老数据被新数据覆盖时才真正释放空间。
这意味着:
- 一个已过期但从未被访问的 key,会一直留在内存里,直到对应 segment 的 ring buffer 被写满并覆盖
- EntryCount() 返回的是逻辑条目数,不是实际占用内存的条目数;
cache.EntryCount()可能远大于真实存活数 - 没有后台 goroutine 清理,所以不会出现意外协程泄漏,但也意味着无法主动触发“清理过期项”
实操建议:
- 对时效性极强的数据(如秒级订单状态),别只靠
expireSeconds,应用层需配合业务逻辑二次校验 - 监控
cache.Metrics()中的HitCount/MissCount和TotalCount,比单纯看EntryCount更能反映真实水位 - 如果发现内存长期不释放,优先检查是否大量 key 永远不被访问(比如冷数据写入后就没再读),而非怀疑 FreeCache 泄漏
FreeCache 的零GC和高并发优势,只在「预分配内存 + byte 切片操作 + 分片锁」三者齐备时才真正生效;任意一环绕开(比如用 map 包一层、或动态扩容 cache 大小),都会让性能回落到普通 sync.Map 水平。


















