本地缓存与分布式缓存必须分层协作,不能混用或替代:sync.Map/bigcache解决毫秒级延迟,redis.Client保障跨实例一致性;漏掉任一层将导致高并发下穿透、击穿或内存爆炸。

本地缓存和分布式缓存必须分层协作,不能混用或互相替代——sync.Map 或 bigcache 解决的是毫秒级延迟问题,redis.Client 解决的是跨实例一致性问题;两者漏掉任何一层,都会在高并发下暴露穿透、击穿或内存爆炸风险。
本地缓存该选 sync.Map 还是 bigcache
不是“哪个更好”,而是“谁更匹配你的数据特征”:
-
sync.Map适合读多写少、key 数量可控( -
bigcache必须显式初始化bigcache.NewBigCache并传入Shards(建议 16–64)、LifeWindow(如10 * time.Minute),适合百万级 key、高频更新(如用户会话、API 路由缓存);它绕过 GC,但不支持自定义淘汰策略,所有 key 共享 TTL - 别把结构体指针直接塞进
bigcache——它只存[]byte,必须先json.Marshal;反序列化失败时不会报错,只会返回 nil,得自己加校验
go-redis/v9 初始化必须做的五件事
跳过任意一项,上线后大概率遇到 redis: connection pool timeout 或首次请求卡死:
- 全局复用单例
*redis.Client,绝不在 handler 里redis.NewClient - 显式设
PoolSize:预估 QPS × 2~4,但不超过 Redis 的maxclients(默认 10000) - 启动时用带超时的 ctx 调
rdb.Ping(ctx).Err(),别等第一次Get才暴露密码错/地址不通 - 所有连接配
MinIdleConns: 5和MaxConnAge: 30 * time.Minute,防中间设备静默断连 - 键名强制加服务前缀,如
user-service:user:1001,避免多服务写冲突
Cache-Aside 模式下如何避免穿透/击穿/雪崩
Redis 不会自动帮你兜底,这三类问题全靠 Go 层代码拦截:
立即学习“go语言免费学习笔记(深入)”;
- 穿透:DB 查无结果时,
rdb.Set(ctx, key, "null", 60*time.Second);业务层解包前必须判if val == "null" { return nil, nil } - 击穿:热点 key 过期瞬间并发查 DB,用
singleflight.Group包住 DB 查询逻辑,同一 key 只放行一个 goroutine - 雪崩:TTL 不能写死,必须加随机扰动:
baseTTL + time.Duration(rand.Int63n(int64(2*time.Minute)));每次生成前用time.Now().UnixNano()初始化 rand,别用全局rand.Seed
本地缓存与 Redis 如何协同更新
不是“先写 Redis 再写本地”,而是“按命中路径反向回填”:
- 本地缓存未命中 → 查 Redis → Redis 命中 → 把值写回本地缓存(注意控制本地 TTL 略短于 Redis TTL,比如 Redis 设 10min,本地设 8min)
- 本地缓存未命中 → Redis 也未命中 → 查 DB → 写 Redis(含空值缓存)→ 再写本地缓存(非强制,可异步)
- 更新操作(如用户资料修改)必须同步删 Redis key(
rdb.Del(ctx, key)),再删本地缓存(localCache.Delete(key));顺序不能反,否则删 Redis 前本地缓存还在,下次读会误命中脏数据 - 别用
SETNX做更新锁——多数业务不需要强互斥,反而拖慢写路径;真要锁,用 Redis 的SET key value EX 30 NX加超时,且必须配context.WithTimeout
最常被忽略的点是本地缓存的 TTL 管理:它不能和 Redis TTL 完全一致,否则在 Redis 刚过期、本地还没过期的窗口期,会返回陈旧数据;这个时间差必须设计成可观察、可配置的参数,而不是硬编码。还有,所有缓存操作的日志必须打上 key 和耗时,否则出问题时根本分不清是本地没命中的慢,还是 Redis 超时的慢。


















