sync.Map不能作为L1缓存层,因其无TTL、无容量控制、无自动淘汰、无命中统计,仅适用于session ID映射等极轻量固定场景;真需本地缓存应选bigcache或go-cache。

为什么不能用 sync.Map 当 L1 缓存
它不是缓存,只是线程安全的哈希表。没 TTL、不淘汰、无容量限制、无命中统计,更不会自动同步到 Redis。你手动 Store 一个值,其他 goroutine 可能因内存模型看不到更新;用 time.Now().Unix() 算过期,在容器里时钟漂移后,L1 就永远不清理。
只适合极轻量固定场景:session ID 映射、预热后不变的配置快照。别包装成带 expireAt 字段的 struct + 定时 goroutine 清理——GC 会被卡住,且无法应对 key 突增。
真要本地缓存,选 bigcache(压测吞吐高 2–3 倍)或 github.com/patrickmn/go-cache(支持回调、统计、TTL)。
bigcache 和 go-cache 的关键参数差异
两者都比 sync.Map 更接近缓存语义,但行为差异直接影响线上稳定性:
立即学习“go语言免费学习笔记(深入)”;
-
bigcache.NewBigCache必须传maxEntries和OnRemove回调:前者控内存上限,后者用于手动释放对象(它不 GC) -
go-cache.New(5*time.Minute, 10*time.Minute)第一个参数是默认 TTL,第二个是清理间隔;不设清理间隔,过期 key 会一直占内存 -
bigcache底层分片 + 时间轮,读写无锁;go-cache用sync.RWMutex,高并发下写频繁会争抢
L1/L2 协同必须守住的读写契约
不是“写两份”,而是靠流程约束一致性。错位 100ms 就可能返回旧值——这不是 bug,是时间窗口博弈。
读流程:l1.Get(key) → miss → singleflight.Do(key, func() { l2.Get(key) }) → L2 miss → 查 DB → l2.SetEX(key, val, ttl) → l1.Set(key, val, shortTTL)
写流程:l1.Delete(key) → l2.Del(key) → 更新 DB;绝不能反向,否则中间窗口返回旧 L1 + 新 DB
L1 的 TTL 必须 ≤ redis.PTTL(key) 返回的剩余时间,否则必然出现“Redis 已更新,L1 还返旧值”
缓存穿透防护在多级下容易失效的点
单层加空值就完事,但多级里 L1 写空值没意义——下次请求可能直接命中 L2 绕过 L1,导致穿透复现。
L2 层必须对空结果也写入:redis.SetEX("user:123:empty", "1", 5*time.Minute)
L1 同步写一个带 60s TTL 的 nil sentinel(不是空字符串),防止重复回源
Redis 宕机时,错误要识别出来,走降级逻辑(比如返回上次成功缓存值 + 告警),而不是直接穿透 DB


















