go-zero 的 cachedConn 只允许“先更新 DB 再删缓存”,因其硬编码逻辑固定为 sqlx.NamedExec 成功后执行 delCache,该路径不可配置;其一致性保障依赖“可收敛”而非绝对一致,且 delCache 不在事务内、无重试,需通过消息队列或信号 key 等异步方式兜底补偿。

go-zero 的 cachedConn 为什么只允许“先更新 DB 再删缓存”
因为这是硬编码逻辑,不是配置项。你改不了,也不该改——cachedConn.Update 内部固定走 sqlx.NamedExec → 成功 → delCache 这条路径,代码在 core/stores/sqlx/sql.go 和 core/stores/cache/cache.go 里写死。
它安全的关键不在“绝对一致”,而在“可收敛”:哪怕并发下有请求在 delCache 执行前读到旧缓存,也只是短暂脏读;只要 delCache 成功,后续请求立刻命中新数据。而“先删缓存再更新 DB”一旦失败(比如 DB 更新超时),旧值就可能被其他读请求重新回填进缓存,这种不一致不会自动修复。
注意:delCache 不在事务内执行,也不重试、不抛异常。DB 更新成功后,它失败了,业务流程照常返回,但缓存已脏——这正是你需要兜底的地方。
删缓存失败后怎么可靠补偿
别在 handler 里加 for 循环重试 redis.Del,网络抖动会卡住整个接口,延迟飙升。
立即学习“go语言免费学习笔记(深入)”;
生产环境必须解耦失败处理:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 推荐方案:DB 更新成功后,向 Kafka/RocketMQ 投递一条
cache_delete消息,由独立的 job service 消费并指数退避重试,失败时记录日志 + 触发告警 - 轻量替代:用
redis.SetEX写一个带 TTL 的信号 key(如cache:order:123_signal),TTL 设为 300s;另起 goroutine 监听 Redis 的__keyevent@0__:expired事件(需提前开启notify-keyspace-events Ex),事件触发后执行真实缓存 key 删除 - 禁用方案:把
redis.Del塞进 DB 事务——事务回滚时缓存已删但 DB 未改,造成“缓存空、DB 有值”的不可恢复状态
为什么不能用 time.Sleep 做延时双删
所谓“更新 DB 后 time.Sleep(500 * time.Millisecond) 再删一次缓存”,是典型误用。它阻塞当前 handler goroutine,拖慢响应;更致命的是,如果进程在此期间重启,第二次删除永远不执行。
真正可用的延时双删必须同时满足两个条件:
- 解耦:第二次删除不能和主流程共用 goroutine
- 可持久化:删除任务要落盘或进队列,不能只存在内存里
比如:第一次删缓存后,往 Redis 写个 delay_delete:order:123 key,TTL 设为 500ms;再用一个常驻 goroutine 扫描过期 key 并触发真实删除——这样即使服务重启,只要 Redis 没丢数据,任务就不会丢。
本地缓存 + Redis 双层时一致性怎么保
sync.Map 或 Ristretto 只解决单机线程安全,跨实例时完全不管用。常见错误是:更新时只删 Redis,不通知其他实例清理本地缓存,结果各节点缓存长期不一致。
正确做法分两层:
- 写操作:先删 Redis,再通过 Pub/Sub 或消息队列广播失效事件;各实例收到后调用
sync.Map.Delete或 Ristretto 的Del方法 - 读操作:查本地缓存,命中且租约未过期(
LeaseInfo.ExpireAt > time.Now())则直接返回;否则回源查 Redis,并校验 version_vector,确认无冲突后再写入本地缓存
本地缓存的 TTL 必须设得比 Redis 短(比如 Redis 30 分钟,本地 10 分钟),靠租约+版本号兜底,而不是靠时间被动过期。

















