Cache-Aside 模式在 Go 中需显式控制缓存读写,读取时用 singleflight 避免击穿,DB 失败时设短时空值防穿透;更新时先 DB 后删缓存,删缓存失败须日志记录而非返回错误;禁用 go-cache 等本地缓存,应抽象接口以支持 Redis 等分布式存储。

Cache-Aside 模式在 Go 里怎么写才不踩坑
Go 本身没有内置 Cache-Aside 框架,得靠自己组织 Get / Set / Delete 的调用顺序和错误分支。核心就一条:**缓存只在业务逻辑里显式读写,数据库永远是最权威源**。别指望中间件或装饰器自动帮你兜底。
读取时先查缓存、未命中再查 DB 并回填(Get 流程)
这是最容易写出竞态的地方——两个并发请求同时发现缓存 miss,都去查 DB,又都往缓存写,浪费资源还可能写入不一致数据。
- 用
sync.Once或更稳妥的singleflight.Group消除重复 DB 查询(尤其对热点 key) - 缓存 key 要带业务上下文,比如
"user:123:profile",别用裸 ID 或结构体指针做 key - DB 查询失败时,别把空结果或错误塞进缓存(避免缓存穿透),可以设个短过期时间的空对象(如
nil+time.Minute) - 示例片段:
val, err := cache.Get(ctx, key) if err == nil { return val, nil } if !errors.Is(err, cache.ErrKeyNotFound) { return nil, err } // 缓存未命中,查 DB dbVal, err := db.GetUserByID(ctx, id) if err != nil { // 可选:缓存空值防穿透 cache.Set(ctx, key, nil, time.Minute) return nil, err } cache.Set(ctx, key, dbVal, time.Hour) return dbVal, nil
更新时先改 DB、再删缓存(Update 和 Delete 流程)
“先删缓存再改 DB” 看似简单,但 DB 写失败时缓存已空,下次读就会误加载旧值;“先改 DB 再删缓存” 是更安全的选择,即便删缓存失败,最多是短暂脏读,不会永久错。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 删缓存失败不能直接返回错误——DB 已改成功,缓存只是尽力而为,应记录日志但继续走完流程
- 如果用 Redis,
DEL命令本身幂等,但注意管道或事务中出错可能导致部分 key 没删干净 - 涉及关联数据(如用户资料变更后要清掉其所有订单缓存),别漏删,建议用前缀扫描 + 批量删除,或用布隆过滤器辅助识别
- 不要用
Set覆盖新值代替Delete——万一 DB 写成功、缓存 Set 失败,就会卡在旧值上
为什么不用 github.com/patrickmn/go-cache 这类内存缓存做 Cache-Aside
它没提供原子性的 “get-or-create” 接口,也没支持 context 取消,更不兼容分布式场景。一旦服务多实例,各进程缓存不同步,go-cache 就退化成一个不可控的本地变量。
立即学习“go语言免费学习笔记(深入)”;
- 单机开发调试可以用,上线必须换
redis.Client或memcache.Client - 哪怕用本地缓存,也要加一层适配器抽象,确保
Get/Set接口能无缝切到远程存储 - Redis 的
GET+SET不是原子的,别手写if nil { set }——要用GETEX或 Lua 脚本保一致性 - 注意
context.WithTimeout必须传给缓存操作,否则 DB 超时了,缓存还在傻等

















