不能直接用闭包包裹 db.QueryRow + cache.Set,因缓存 key 缺失查询参数、结构体序列化类型丢失、未防缓存穿透、跨库 JOIN 表名前缀不一致,易导致数据错乱或数据库压垮。

为什么不能直接用闭包包裹 db.QueryRow + cache.Set
常见错误是写一个函数,里面先查缓存,没命中就 db.QueryRow 执行 JOIN SQL,再把结果塞进 cache.Set —— 这样看似“缓存了”,但问题极多:
• 缓存 key 没包含所有查询参数(比如 WHERE 条件值、分页 offset),导致不同请求打到同一个 key,返回错数据
• JOIN 查询结果是结构体切片或嵌套 map,直接 cache.Set 存原始 Go 值,反序列化时类型丢失(尤其含 sql.NullString 等)
• 没处理缓存穿透:当某个 user_id 根本不存在时,每次请求都穿透到 DB,压垮数据库
• 忽略了 MySQL 跨库 JOIN 的表名前缀(如 db1.users 和 db2.orders),缓存逻辑和 SQL 不一致,本地测试通过、上线后查不到数据
用 func(*gorm.DB) *gorm.DB 作为缓存键的生成依据
闭包真正起作用的地方,不是“把查询包起来”,而是把「查询意图」抽象成可哈希、可复用的函数值。GORM 的 func(*gorm.DB) *gorm.DB 类型天然适合作为缓存 key 的基础:
• 它本身是函数值,可用 fmt.Sprintf("%p", fn) 取地址做轻量 key(仅限单机场景)
• 更稳妥的做法是让每个条件函数自带 Key() string 方法,例如:
func ByUserID(id int64) func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Where("users.id = ?", id)
}
}
func (ByUserID) Key() string { return fmt.Sprintf("user:%d", id) } // 注意:这里 id 需闭包捕获并暴露• 多个条件组合时(如
ByUserID(123).And(ByStatus(1))),key 拼接顺序必须与实际 SQL 构造顺序严格一致,否则缓存会错乱• 别在闭包里直接拼 SQL 字符串做 key,容易忽略空格、换行、参数绑定顺序等细节
go-cache 闭包封装必须隔离数据序列化逻辑
直接缓存 struct{} 或 *[]UserOrder 是危险的。正确做法是闭包内部只负责「取、存、转」三件事,且明确边界:
• 取缓存时,用 c.Get(key) 得到 interface{},立刻断言为 []byte(不是原始结构体)
• 没命中则执行 SQL,用 json.Marshal 序列化结果(而非 gob,gob 对 struct 字段变更敏感)
• 存缓存时,c.Set(key, dataBytes, ttl),dataBytes 是字节流,不带 Go 类型信息
• 回源函数(fn)必须返回 interface{},由闭包统一做 json.Unmarshal,避免调用方误传未序列化数据
• 注意:MySQL 的 TIME、JSON 字段在 json.Marshal 后可能格式变化,需提前用自定义 MarshalJSON 方法处理
跨数据库 JOIN 缓存要额外校验表名前缀一致性
当你的 JOIN 涉及 db1.users 和 db2.orders,缓存层必须感知这个拓扑:
• 缓存 key 中必须显式包含数据库名,例如 "join:db1_users-db2_orders:user_id=123",不能只写 "user:123"
• 如果服务同时连多个 MySQL 实例(非同一 server),跨库 JOIN 实际不可行,此时闭包必须能 fallback 到「两次单库查询 + Go 层 join」,并在 key 中标记 mode=app_join
• 使用 go-cache 分片模式(sharded.Cache)时,确保相同数据库组合的 key 落在同一 shard,否则跨 shard 查不到数据
• 最容易被忽略的一点:MySQL 8.0+ 默认开启 sql_mode=ONLY_FULL_GROUP_BY,若缓存回源 SQL 中 SELECT 字段没全在 GROUP BY 里,即使缓存命中的数据也可能是旧版 MySQL 下跑出来的脏数据


















