缓存预热必须在main()中同步执行,严禁init()或goroutine;正确顺序为initdb()→initredis()→preloadcache()→http.listenandserve(),需超时控制、分页查询、pipeline写入及降级标记。

预热必须在 main() 里同步执行,不能放 init() 或 goroutine
缓存预热失败最常见的原因是时机错乱:用 init() 调用预热函数,或起 goroutine 异步执行,都会导致 panic 或数据写入不全。
根本原因在于:预热依赖 DB 连接、Redis 客户端、配置加载等前置资源,而这些资源只在 main() 中显式初始化后才可用。init() 执行时这些变量还是 nil;goroutine 可能被主 goroutine 退出时强制终止。
- 正确顺序必须是:
InitDB()→InitRedis()→preloadCache()→http.ListenAndServe() - 如果用了 wire 等 DI 框架,确保
preloadCache()是显式调用,不是自动 resolve 的 service - 超时控制必不可少:
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second),超时后记 warn 日志,并设降级标记(如atomic.StoreBool(&cacheWarmedUp, false))
从 MySQL 加热点数据到 Redis,要分页 + pipeline + 字段裁剪
直接 SELECT * FROM product WHERE is_hot = true 在大表上会锁表、OOM、拖垮数据库,且冗余字段让 Redis 内存暴涨。
实际生产中应组合使用三项关键约束:
- 只查必要字段:
SELECT id, name, price, updated_at FROM product WHERE status = 1 ORDER BY pv DESC LIMIT 10000 - 用游标分页替代 OFFSET:
WHERE id > ? ORDER BY id LIMIT 100,每次取上一批的last_id继续 - 批量写入走 pipeline:
pipe := redisClient.Pipeline(),每批 ≤100 条,最后统一pipe.Exec(ctx)
额外建议:预热前显式设置事务隔离级别 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ,防止中途数据变更导致缓存与 DB 不一致。
本地 LRU 缓存预热用 Add,分布式缓存预热用 Set + TTL
本地内存缓存(如 golang-lru 或 go-cache)和 Redis 预热逻辑差异明显,不能混用同一套代码。
- 本地缓存预热直接调用
cache.Add(key, value)即可,无需过期时间(除非业务明确需要) - Redis 预热必须带 TTL:
pipe.Set(ctx, key, value, 1*time.Hour),否则缓存永不过期,故障时无法自动恢复 - 若用
go-zero的cache.NewTtlCache(),预热时传入的ttl参数会自动应用,但需确认底层是否已启用自动刷新
注意:golang-lru/v2 支持泛型和过期时间(通过 expirable 模块),但默认不启用;若要用 TTL,得显式包装成 expirable.Cache 实例,而非直接用 lru.New()。
缓存重建不能只靠定时任务,得结合事件驱动 + 降级兜底
纯 cron 定时重建(比如每天凌晨 reload 全量热点)风险高:DB 压力集中、重建期间缓存空窗、失败无感知。
更稳妥的做法是混合触发:
- 核心数据变更时发消息(如 Kafka / Redis PubSub),监听后触发单 key 或小批量重建(
cache.Set("user:123", user, ttl)) - 对访问频次高的 key,加“懒加载重建”逻辑:Get 失败后异步触发重建,同时返回旧值或默认值(避免穿透)
- 必须有降级开关:当重建失败超过阈值,自动关闭重建流程,记录指标并告警,而不是持续重试拖垮下游
最容易被忽略的是重建过程中的并发冲突——多个请求同时触发同一 key 重建,导致重复写入或覆盖。应在重建入口加 sync.Once 或分布式锁(如 redis.SetNX),确保同一 key 同一时刻只重建一次。











