“先更新DB再删缓存”是最优解,因其避免并发脏数据回填:A删缓存后B可能将旧值写入缓存并持久化,而该策略仅导致一次短暂脏读,且能快速收敛至最终一致。

go-zero 里为什么推荐“先更新 DB,再删缓存”
因为这是它默认生成模型时采用的策略,且在绝大多数读多写少场景下,比“先删缓存再更新 DB”更安全。后者容易在并发时让旧值回填缓存:A 删缓存 → B 查库并写入旧值到缓存 → A 更新 DB,结果缓存里还是错的。
而“先更新 DB 再删缓存”最多造成一次短暂脏读(B 在 A 删缓存前读到旧缓存值),只要删缓存操作成功,后续请求就都能拿到新数据。go-zero 的 cachedConn 就是按这个顺序执行的。
- 它不保证强一致,但能快速收敛到最终一致
- 删缓存失败时,go-zero 不会自动重试,得靠上层加补偿(比如发消息队列)
- 如果业务对一致性要求极高,不能接受任何脏读,就得放弃纯删缓存,改用带版本号或时间戳的缓存 key + 条件删除
延时双删在 go-zero 中怎么落地才不翻车
go-zero 本身不内置延时双删,所谓“休眠 500ms 后再删”是常见误用——time.Sleep 会阻塞 handler goroutine,放大延迟,且无法应对进程重启。
真正可用的方案是解耦 + 延迟信号:
- DB 更新成功后,用
redis.SetEX写一个带 TTL 的信号 key,比如cache:student:123_delay,TTL 设为 300s - 另起一个常驻 goroutine,监听 Redis 的
__keyevent@0__:expired事件(需开启notify-keyspace-events Ex) - 事件触发后,执行
redis.Del删除真实缓存 key;若监听失败,退化为定时轮询ZSET delay_queue - 别在事务里调用
redis.Del:SQL 回滚了,缓存却删了,会导致“缓存空、DB 有值”的不一致
本地缓存(LRU)和 Redis 双层失效怎么协同
go-zero 的多级缓存不是简单套娃,本地缓存走的是 core/collection/cache.go 的 LRU 实现,Redis 层走的是 core/stores/redis,两者生命周期完全独立。删缓存时必须两级都清。
常见错误是只删 Redis,忘了本地缓存还在返旧值。正确做法:
- 所有写操作路径,必须显式调用两次
Del:localCache.Del(key)和redisClient.Del(key) - 本地缓存没有过期通知机制,所以不能依赖“等它自然过期”,必须主动清除
- 如果用了
syncx.SingleFlight包裹查询,删缓存后要顺便清理 flight barrier,否则并发读可能拿到正在重建中的旧值 - 本地缓存 key 命名最好和 Redis 一致,避免漏删或错删
监控和兜底:删缓存失败了你怎么知道
go-zero 的 redis.Del 返回 redis.IntCmd,它的 Result() 方法返回值含义很关键:
- 返回
0是正常:key 本来就不在,不是错误 - 返回
-1或非 nilerr才真要告警,比如连接断开、ACL 拒绝、集群 MOVED 重定向失败 - 建议把所有
Del调用包一层日志埋点,记录 key、耗时、返回值,用 Prometheus 抓取cache_del_failure_total指标 - 生产环境务必配重试:失败时投递到 Kafka 或 Redis Stream,由独立 job 消费重试,超过 3 次进死信队列人工介入
最易被忽略的一点:延迟双删里的“第二次删”,不是为了补救第一次失败,而是为了覆盖“DB 已写但缓存未删”的窗口期。它和第一次删是不同语义,不能合并监控逻辑。


















