不能靠l1淘汰自动触发l2删除,因l1淘汰是内存管理行为,l2无感知,强行同步会导致淘汰时机不可控、网络失败无回退、跨实例无效三类问题。

不能靠 L1 淘汰自动触发 L2 删除——这会放大不一致风险,且违背多级缓存“读时填充、写时失效”的契约。
为什么 L1 淘汰不等于 L2 失效
本地缓存(L1)淘汰是内存管理行为,比如 bigcache 触发 LRU 驱逐或 ristretto 到达容量上限;而 Redis(L2)是独立服务,没有监听机制感知本地内存变化。强行在 L1 淘汰回调里调用 rdb.Del 会引入三类问题:
- 淘汰时机不可控:定时清理、访问频次触发、内存压力触发,都不是业务写操作点,L2 删除可能早于新值写入,造成短暂空窗
- 网络失败无回退:L1 淘汰回调中调 Redis 失败,无法重试或记录,L2 实际未删,后续读到脏数据
- 跨实例无效:A 实例淘汰了 key,B 实例的 L1 还有该 key,但 L2 已被删,B 实例下次读会误判为 miss 并穿透 DB
L1 和 L2 的正确联动时机只有两个
所有同步动作必须收敛到明确的业务写入口,而非 L1 生命周期事件:
-
写操作发生时:调用
Set(key, val)→ 先rdb.SetEX写 L2,再l1.Delete(key)清 L1(不是写 L1) -
读操作 miss 时:L1 miss → 用
singleflight.Do查 L2 → L2 命中后才l1.Set(key, val, ttl)回填 L1
注意:L1 的 TTL 必须 ≤ L2 的 TTL,例如 L2 设 300 秒,L1 最多设 240 秒,避免 L1 过期延迟导致返回陈旧值。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
如果真要“L1 淘汰同步 L2”,只能加一层间接层
这不是推荐做法,但某些强一致性场景下可折中实现:
- 用带
OnEvicted回调的库(如github.com/patrickmn/go-cache),在回调里发异步消息(如 channel 或 worker queue) - worker 收到淘汰通知后,重查 L2 当前值是否与刚淘汰的 L1 值一致;若一致,再执行
rdb.Del(需加 CAS 或版本号校验) - 不依赖回调实时性:允许 L2 滞后几秒,但必须有最终一致性兜底(比如定时扫描 L1 淘汰日志 + L2 TTL 对齐)
这种方案成本高、链路长,只适用于金融类对“过期即不可见”有硬要求的子模块,普通业务请严格走写时失效路径。
真正容易被忽略的是:L1 淘汰本身不携带业务语义,它只是内存回收动作;而缓存失效必须绑定数据变更事实。把两者混同,等于把 GC 行为当成业务事件来处理——错位一旦发生,排查时连日志都找不到触发源头。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










