读多写少、key稳定且无需TTL时选sync.Map,轻量零依赖;高频热key、需TTL或内存控制时必选bigcache,其分片+时间轮设计吞吐高2–3倍,但须注册OnRemove回调防内存泄漏。

本地缓存该用 sync.Map 还是 bigcache
读多写少、key 数量稳定且不需要 TTL 的场景(比如用户权限白名单、配置项),sync.Map 是最轻量的选择——零依赖、原生并发安全、无 GC 压力。但它不支持过期、无容量限制、不自动驱逐,你得自己加定时 goroutine 清理;一旦开始这么干,说明你其实需要的是缓存库,不是 map 结构体。
高频热 key(QPS > 5k)、需要 TTL 或内存上限控制的场景,优先选 bigcache。它通过分片 + 时间轮实现高吞吐,压测下比 sync.Map 快 2–3 倍,但要注意:bigcache.NewBigCache 的 OnRemove 回调必须手动注册清理逻辑,否则内存不释放给 GC。
别用 map + sync.RWMutex:锁粒度太粗,实测比 sync.Map 慢 40%+;也别在 sync.Map.LoadOrStore 里直接调 DB 或 Redis——函数体非原子,多个 goroutine 同时触发会导致重复加载,必须外层套 singleflight.Group 防穿透。
查缓存顺序为什么必须是 “先本地再 Redis”
顺序反了就失去多级意义:如果先查 Redis 再回填本地,那本地缓存永远只在 Redis 命中后才写入,冷启动或 Redis 故障期间本地始终为空,L1 彻底失效。
正确流程是:
- 请求进来 → 调
localCache.Load("user:profile:123");命中则直接返回 - 未命中 → 查 Redis:
rdb.Get(ctx, "user:profile:123");Redis 命中 → 反序列化 +localCache.Store()→ 返回 - Redis 也未命中 → 查 MySQL → 序列化 →
rdb.Set(ctx, key, val, 60*time.Second)→localCache.Store(key, val, 5*time.Second)→ 返回
注意:MySQL 查询成功后必须写本地缓存,不能只等 Redis 命中才写;否则 Redis 故障时,所有请求都会穿透到 DB。
redis.Client 初始化和调用最容易踩的坑
90% 的线上缓存故障都源于初始化阶段漏掉关键配置:
- 全局复用单例
*redis.Client,绝不能每次请求都redis.NewClient;否则连接风暴直接打满 Redis 的maxclients - 显式设
PoolSize(建议 20–50)、MinIdleConns(至少 5)、MaxConnAge(30 * time.Minute) - 地址必须从环境变量或配置中心读取,硬编码
"localhost:6379"在容器/微服务下必挂 - 初始化后立即调
rdb.Ping(ctx).Err()验证连通性,别等第一个业务请求才暴露问题 - 所有操作必须带
context.WithTimeout:HTTP 场景推荐ctx, cancel := context.WithTimeout(r.Context(), 300*time.Millisecond);超时值要略大于 Redis P99 RT(如 P99=80ms,设 200ms 较稳妥)
别在 init() 函数里提前声明全局 ctx = context.Background()——它是无约束上下文,不是“默认上下文”。
Key 设计、空值处理与一致性窗口怎么控
裸 ID 当 key(如 "123")极危险:不同业务表 ID 可能重叠,删一条就误清其他业务缓存。推荐格式:"user:profile:123"、"order:summary:20260424:uid_789",前缀明确语义,中间段可嵌时间、租户、版本等维度。
空值防穿透不能只写 Redis:rdb.Set(ctx, key, "null", 60*time.Second),L1 也得同步写短 TTL 空值(如 60s),否则下次请求可能命中 L2 绕过 L1,空值没意义。
真正难控的是时间窗口里的一致性博弈:localCache 的 TTL、rdb.Set 的 TTL、DB 更新耗时,三者只要差 100ms 就可能暴露脏数据。别迷信“自动刷新”,得按业务容忍度手工对齐这几个值——比如 DB 更新耗时 P99 是 120ms,那 L1 TTL 至少设为 200ms,L2 TTL 设为 600ms,才能留出安全缓冲。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











