缓存预热必须在main()中InitDB()和InitRedis()完成后、e.Start()前同步执行,禁用init()和goroutine;需超时控制、分页查必要字段、Pipeline写入、降级标记及key存在性校验。

缓存预热不能在 Echo 的 echo.Start() 之后做,也不能塞进 init() 或 go 启动的 goroutine;必须在 InitDB() 和 InitRedis() 完成后、e.Start() 前同步执行,且需超时控制与降级标记。
预热入口必须放在 main() 里,紧挨着 Echo 初始化之后
Echo 框架本身不提供预热钩子,echo.New() 只是构造器,e.Start() 才真正接管 HTTP 生命周期。一旦它开始监听,请求就可能涌进来——此时若缓存还是空的,所有请求都会穿透到 DB。
常见错误现象:panic: runtime error: invalid memory address or nil pointer dereference,堆栈指向预热函数里的 rdb.Get() 或 db.QueryRow(),本质是 Redis 客户端或 DB 连接池还没初始化完就被调用了。
- 正确顺序只能是:
InitDB()→InitRedis()→preloadCache(ctx)→e.Start() - 禁用任何包级
init()函数做预热逻辑;哪怕用了 Wire DI,也要确保preloadCache是显式调用,而非自动注入的服务 - 若预热耗时较长(如 >5s),必须用
context.WithTimeout(context.Background(), 10*time.Second)包裹,超时后记 warn 日志,并设置atomic.StoreBool(&cacheWarmedUp, false)供后续请求降级判断
从 MySQL 查热点数据要分页 + 字段裁剪 + 游标驱动
直接 SELECT * FROM product WHERE is_hot = 1 在生产环境等于埋雷:大表深分页会锁表、OOM,冗余字段让 Redis 内存暴涨 3 倍,且无法反映真实访问热度。
立即学习“go语言免费学习笔记(深入)”;
预热不是 dump 全量,而是按业务语义精筛。比如电商场景,应优先加载 status = 1 AND pv > 1000 的商品,且只取 id, name, price, updated_at 四个字段。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 用游标分页替代 OFFSET:
WHERE id > ? ORDER BY id LIMIT 100,每次取上一批最后的id继续查 - 加事务隔离:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED,避免预热中途被更新阻塞 - 若用 go-sqlx,查询时显式指定结构体字段标签:
type Product struct { ID int `db:"id"` Name string `db:"name"` },防止字段映射错位
写入 Redis 必须用 Pipeline,且每批 ≤100 条
go-redis/v8 的 Pipeline() 是惰性的:不调 pipe.Exec(ctx),命令就永远卡在内存里,Redis 根本收不到。线上日志查不到 key、GET 返回空,八成是漏了 Exec()。
集群环境下更要注意:*redis.ClusterClient 不支持直接 Pipeline(),强行调用会 panic;跨 slot 的命令也会被 Redis Cluster 拒绝(CROSSSLOT Keys in request don't hash to the same slot)。
- 每批最多 100 条:
pipe := rdb.Pipeline(); for i := range batch { pipe.Set(ctx, "product:"+strconv.Itoa(p.ID), data, 1*time.Hour) } - 必须且仅能调一次
pipe.Exec(ctx),调完立刻遍历所有*redis.Cmd检查.Err() != nil - 读操作(如
Get)用.Result(),写操作(如Set)用.Err(),别混用.Val()——*redis.StatusCmd没有Val()方法
Key 命名、TTL 和空值防穿透要统一收敛
多个服务共用一个 Redis 实例但 key 没前缀,或者同一结构体在不同模块混用 json.Marshal 和 gob.Encode,结果就是缓存读出来 invalid character 或直接 panic,日志里根本找不到源头。
预热时若跳过空值处理,上线后大量无效 key 查询会瞬间打穿 DB,尤其当爬虫扫出一堆 /api/product?id=9999999 这类不存在 ID 时。
- key 必须带服务前缀:
"product:detail:" + strconv.Itoa(p.ID),用户相关用"user:profile:",避免冲突 - 每个 key 必须设随机 TTL:
1*time.Hour + time.Duration(rand.Int63n(300))*time.Second,防雪崩 - 查 DB 返回空时,仍要写入
rdb.Set(ctx, key, "null", 60*time.Second),业务层解包前先判if val == "null" { return nil, nil }
最易被忽略的一点:预热前必须用 rdb.Exists(ctx, key).Val() > 0 检查 key 是否已存在且未过期,存在就跳过。否则并发启动多个实例时,会反复覆盖同一组 key,引发锁竞争和不必要的 pipeline 负载。

















