预热是在http服务启动前将高频数据加载到sync.map或bigcache中以避免缓存穿透;需控制超时、支持降级、确保线程安全,并分层处理数据更新与一致性验证。

启动时加载缓存数据到 sync.Map 或 bigcache
Go 服务冷启动后首次请求慢,本质是缓存为空导致穿透 DB。预热就是在 main() 启动流程中、HTTP server 监听前,把高频/稳定数据灌进内存缓存。
别用 map 做并发缓存——它不是线程安全的;也别在 handler 里边查边塞,那不是预热,是懒加载。推荐两种实际可用方式:
-
sync.Map:适合中小规模(万级以内 key)、读多写少、不需要过期策略的场景;预热就是遍历数据源,调用Store(key, value) -
bigcache:适合十万级以上 key、需控制内存上限、能接受少量 GC 开销的场景;用bigcache.NewBigCache()创建实例后,通过Set()批量写入
示例片段(sync.Map 预热):
func preloadCache() {
data := fetchHotDataFromDB() // 自定义函数,返回 []struct{ID string; Name string}
for _, item := range data {
cache.Store(item.ID, item.Name) // cache 是全局 sync.Map
}
}
func main() {
preloadCache() // 必须在 http.ListenAndServe 前
http.ListenAndServe(":8080", nil)
}
避免预热阶段阻塞 HTTP server 启动
预热耗时长(比如查几千条数据 + 解析 JSON),直接卡住 ListenAndServe,会导致健康检查失败、K8s 重启 Pod。
立即学习“go语言免费学习笔记(深入)”;
常见错误是把预热逻辑写成同步串行,尤其用了 ORM 的 FindAll() + 循环 Save(),没加 context 控制或超时。
- 给 DB 查询加
context.WithTimeout(ctx, 5*time.Second),超时直接 panic 或降级空缓存启动 - 预热失败不 panic 全局,而是记录 error 日志 + 允许服务带部分缓存上线(比如只加载了 70% 数据)
- 真要异步做,用
go func() { ... }(),但必须确保所有Set()完成后再允许第一个请求进来——可借助sync.WaitGroup或atomic.Bool标记就绪状态
预热数据更新后如何刷新内存缓存
预热不是一劳永逸。DB 数据变了,内存里还是旧的,就会产生一致性问题。但又不能每改一次 DB 就全量重刷缓存——太重。
实际做法是分层处理:
- 对「几乎不变」的数据(如国家列表、协议版本号),预热一次,进程生命周期内不动
- 对「低频变更」的数据(如配置表、运营活动开关),监听 DB binlog 或用 Redis pub/sub 触发单 key 更新,不用全量重载
- 对「无法容忍脏数据」的场景(如用户余额),干脆放弃内存缓存,走 Redis + Lua 原子读写,或用
freshcache这类带 TTL 和主动 refresh 的库
注意:bigcache 默认不支持 TTL 自动淘汰,得自己封装一层定时清理,或者换用 freecache(支持 LRU + TTL)。
验证预热是否生效的三个检查点
上线后光看日志不够,容易误判。真正要盯的是这三个地方:
- 看
http.Server.Handler第一个请求的 p95 延迟:如果从 300ms 降到 15ms,说明缓存命中了 - 查 DB 慢查询日志:预热后首分钟内,对应预热表的 SELECT 应该归零或极低
- 用
runtime.ReadMemStats()对比预热前后Alloc和HeapInuse,确认内存确实被缓存占用了(比如涨了 20MB)
最容易被忽略的是:预热代码写了,但忘了在 main() 里调用,或者 import 了包却没触发 init 函数。上线前务必用 go run -gcflags="-m" main.go 看下关键结构体有没有逃逸到堆上——否则缓存数据可能被频繁 GC 回收。










