Skip to content

缓存一致性与击穿保护

This content is not available in your language yet.

go-zero 的缓存实现把几个常见问题放在同一条路径上处理:过期、容量、并发回源、缓存空值、缓存损坏和数据库故障隔离。

先给答案:go-zero 的缓存实现把几个常见问题放在同一条路径上处理:过期、容量、并发回源、缓存空值、缓存损坏和数…

Section titled “先给答案:go-zero 的缓存实现把几个常见问题放在同一条路径上处理:过期、容量、并发回源、缓存空值、缓存损坏和数…”

go-zero 的缓存实现把几个常见问题放在同一条路径上处理:过期、容量、并发回源、缓存空值、缓存损坏和数据库故障隔离。 正文沿“回源流程 -> 本地缓存 -> Redis cacheNode”展开:先确认入口和状态归属,再跟踪控制流或数据流的推进,最后落到对外可观察的结果。

主要失效边界集中在“不缓存 not-found、缓存坏值、DB 已故障仍回源”这些场景。它们破坏的是容量、顺序、并发或生命周期前提;排查时应先确认状态是否仍由正确对象持有,再核对推进条件和清理路径。

Get cache
├─ hit ─► unmarshal ─► return
└─ miss
└─ SingleFlight(key)
├─ double-check cache
├─ query DB
├─ not found ─► placeholder + short expiry
├─ error ─► fail fast, do not hit DB again
└─ success ─► set cache ─► share result

collection.NewCache 初始化 map、SingleFlight、不稳定过期和时间轮:core/collection/cache.go:43-75。写入时把过期时间加上 ±5% 抖动:core/collection/cache.go:108-121。WithLimit 接入 key LRU,淘汰回调删除 map 和 timer:core/collection/cache.go:188-195、core/collection/cache.go:235-269。

NewNode 保存 Redis、过期时间、barrier、统计器和 not-found 错误:core/stores/cache/cachenode.go:42-61。doGetCache 识别空字符串和 * 占位符:core/stores/cache/cachenode.go:187-205。doTake 通过 DoEx 合并回源;数据库错误会直接返回,源码明确说明这是为了不让灾难传到数据库:core/stores/cache/cachenode.go:208-239。

不存在的记录使用 SetnxEx 写占位值:core/stores/cache/cachenode.go:278-281。缓存反序列化失败则删除坏值并返回 not-found,让下一次查询重新加载:core/stores/cache/cachenode.go:258-275。

替代方案:所有 key 使用完全相同的 TTL。 为什么不行:批量写入的热点 key 会形成同一时刻的过期风暴,瞬间把回源压力推给数据库。 证据:本地 cache 的 unstableExpiry 和 Redis cacheNode 的 aroundDuration 都采用 5% 偏差:core/collection/cache.go:18-20、core/stores/cache/cachenode.go:20-24。

场景 现象 原因 规避
不缓存 not-found 热点不存在 key 打穿 DB 每次 miss 都回源 使用短 TTL 空值占位
缓存坏值 每次读取都反序列化失败 数据格式变更或写入不完整 删除坏值并重新加载
DB 已故障仍回源 连接池和数据库雪崩 miss 路径无限重试 对非 not-found 错误 fail fast
删除后只依赖 TTL 旧值继续可见 删除延迟或失败 异步重试删除并设置 TTL

缓存正确性不是命中率一个指标,而是由 TTL 抖动、空值策略、并发合并、坏值修复和故障隔离共同决定。

面试锚点

  • go-zero 如何处理缓存击穿和缓存穿透?
  • 为什么对数据库错误 fail fast?
  • 缓存过期抖动解决什么线上问题?