答案是:不能用sync.Map当L1缓存,因其无TTL、无容量控制、无自动淘汰、无命中统计,仅适用于session映射等极轻量固定场景;应选bigcache或go-cache。

别用 sync.Map 当 L1 缓存
它根本不是缓存,只是线程安全的哈希表。没有 TTL、不控容量、不自动淘汰、无命中统计——硬塞进去等于把一致性逻辑全甩给业务代码。比如你写 l1.Store(key, val) 后又手动加个过期字段,再起 goroutine 定时清理:容器里时钟漂移会让 key 永远不删;并发 Delete 只是标记,dirty map 里的内存实际不释放,RSS 持续上涨;更别说高并发下写多卡顿、GC 频繁的问题。
真正适合的场景只有:几十个固定 key 的 session 映射、连接池元数据这类生命周期短、无时效要求的数据。
选 bigcache 还是 go-cache?看你的压测结果
两者都原生支持 TTL、分片锁、容量限制和淘汰回调,比手撸 sync.Map + 定时器靠谱得多。
-
bigcache:适合大量小对象(如 JSON 字节流),内存友好、GC 压力低;但只支持[]byte类型,序列化/反序列化得自己包一层 -
github.com/patrickmn/go-cache:接口简洁,支持自定义过期回调,类型自由;但非分片设计,高并发下锁竞争明显,QPS 上千就容易成为瓶颈
实操建议:
- 如果 key 数量稳定在万级以内、value 大多是 struct 或 map,优先用
go-cache,初始化别传0:cache.New(cache.NoExpiration, 30*time.Second) - 如果 key 达十万+、value 是短字符串或序列化后 bigcache,
shards参数设为 CPU 核数 × 2~4,别盲目设 64
本地缓存必须带 singleflight 和时间轮
没 singleflight.Do,热点 key 在高并发下会触发 N 次穿透查询,DB 直接雪崩;没时间轮或定时清理,过期 key 积压导致内存失控。
常见错误:
- 用
time.AfterFunc给每个 key 单独启 timer → goroutine 泄漏爆炸 - 只靠
cache.Get时判断时间戳 → 并发读可能同时发现过期,然后一起写入新值,击穿 DB - 设置
cleanupInterval为 0 或负数 →go-cachepanic:“invalid memory address”
正确做法:
- 所有读路径统一走
barrier.Do(key, func() { ... }) - 用时间轮(如 go-zero 的
TimingWheel)或周期性扫描(go-cache的cleanupInterval必须 > 0)集中清理
L1 过期时间必须显著短于 L2
这是防止脏读最廉价也最关键的防线。L1 存的是“快照”,不是权威;L2(Redis)才是跨实例共享的权威源;DB 是最终事实。
实操细节:
- L1 TTL 设为 L2 TTL 的 1/3~1/5,比如 Redis 设 30 分钟,L1 就设 5~10 分钟
- 写操作严格按顺序:
l1.Delete(key)→l2.Del(key)→ 更新 DB;绝不能反过来 - 读 miss 后必须先查 L2,再查 DB;绕过 L2 直接查 DB,L2 就彻底失效
最容易被忽略的是写顺序和 singleflight —— 漏掉任意一环,线上就可能出现旧 L1 + 新 DB 的中间态,且问题复现难、定位慢。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











