Cache-Aside 是一种缓存读写策略,核心为查缓存→未命中则查DB→写回缓存;Go不提供现成实现,需用sync.Map、redis.Client等自行组装,并重点规避穿透、击穿、双写不一致等陷阱。

Cache-Aside 是什么,为什么 Go 里不能直接“实现”它
Cache-Aside 不是 Go 标准库里的一个接口或类型,它是一种缓存读写策略,核心逻辑就三句话:查缓存 → 缓存未命中则查数据库 → 写回缓存。Go 本身不提供“Cache-Aside 框架”,你得用 sync.Map、redis.Client 或 groupcache 等工具自己组装逻辑。关键不是“怎么写个 Cache-Aside 包”,而是“怎么避免在组合时掉坑里”。
读操作:先查缓存再查 DB,但要注意缓存穿透和空值覆盖
典型错误是只做 if cache.Get(key) != nil,然后直接 db.Query,结果大量请求击穿到 DB。更稳妥的做法是统一处理空值:
- 缓存中查不到,立刻查 DB;若 DB 返回空结果,也往缓存里写一个带短 TTL 的空标记(比如
"nil"或struct{}{}),防止重复穿透 - 用
time.Now().Add(30 * time.Second)控制空值过期时间,别用永久缓存 - 如果用 Redis,推荐
GET + SETNX + EXPIRE原子组合,或直接用SET key value EX 30 NX
写操作:删缓存比更新缓存更安全,但得防删失败和双写不一致
更新数据时,99% 场景该用 cache.Delete(key) 而非 cache.Set(key, newVal)。原因很实在:DB 更新成功但缓存 Set 失败,会导致脏数据;而删缓存失败,最多是下次读多一次 DB。
- 删缓存必须放在 DB 提交之后 —— 如果先删后改 DB,中间有并发读会把旧值重新塞进缓存
- 用 Redis 时,
DEL命令本身是原子的,但要注意连接超时或网络中断导致命令没发出去;建议加简单重试(最多 2 次)+ 日志告警 - 如果业务允许小概率不一致,可异步删缓存(比如发消息到本地 channel 或 Kafka),但别异步删还加复杂重试逻辑——复杂度陡增,收益极小
并发读写下缓存击穿:单点查询 DB + 回填,别让 100 个 goroutine 同时查库
当某个热点 key 过期瞬间,大量请求同时发现缓存为空,全部涌向 DB,这就是击穿。Go 里最轻量解法是用 sync.Once 或 singleflight.Group:
立即学习“go语言免费学习笔记(深入)”;
-
singleflight.Group是标准库golang.org/x/sync/singleflight里的,适合包裹 DB 查询函数,相同 key 的并发调用只执行一次 DB 查询,其余等待返回后一起回填缓存 - 注意:回填缓存动作(
cache.Set)必须放在singleflight.Do的回调里,不能在外部做,否则可能多个 goroutine 同时 Set - 别用
sync.Mutex手动锁整个 key,开销大且易死锁;singleflight是无锁设计,更贴合 Go 的并发哲学
真正难的不是写对这几行代码,而是想清楚「这个 key 的一致性要求到底有多高」「空值要不要缓」「删缓存失败能不能接受」——这些决策藏在业务逻辑里,没法靠框架自动兜底。


















