Redis缓存穿透指查询不存在的数据导致请求直击数据库,解决方案包括布隆过滤器拦截、缓存空值、参数校验;击穿是热点key过期引发并发查库,可用互斥锁或singleflight解决;雪崩是大量key同时失效,需错开过期时间。

直接用 redis.NewClient 初始化后就调 Set/Get,线上基本等于主动制造故障——连接池没调、context 没设超时、空值没兜底、结构体序列化失败,90% 的缓存问题都源于初始化阶段的疏漏。
redis.NewClient 必须显式配置连接池参数
默认 PoolSize=10 仅适合低流量场景;QPS 上千时会频繁阻塞在 pool.Get,报错 redis: connection pool timeout。不设 MinIdleConns 和 MaxConnAge,中间网络设备静默断连后连接无法回收,最终耗尽 Redis 的 maxclients(默认 10000)。
-
PoolSize设为预估并发请求数的 2–4 倍(例如 QPS 300,建议 60–120),但不超过 Redis 实例的maxclients - 必须配
MinIdleConns: 5和MaxConnAge: 30 * time.Minute,防止连接老化失效 - 地址不能硬编码
"localhost:6379",从环境变量或配置中心读取,微服务部署下这是强制项 - 初始化后立刻用带超时的
ctx调rdb.Ping(ctx).Err()验证连通性,别等第一次Get失败才暴露问题
所有操作必须带 context.WithTimeout
用 context.Background() 或不设超时的 context.TODO() 调 client.Get 或 client.Set,一旦 Redis 响应慢或网络抖动,goroutine 就卡死,日志里看不到 Redis 错误,只看到 redis: connection pool timeout 或 goroutine 数持续上涨。
- HTTP handler 中统一用
ctx, cancel := context.WithTimeout(r.Context(), 300*time.Millisecond),优先继承 request 生命周期 - 写操作(如
Set、Del)同样要超时,尤其涉及分布式锁逻辑时,超时缺失会导致锁假死 - 禁止在
init函数里声明全局ctx = context.Background()—— 它不是“默认上下文”,而是“无约束上下文”
缓存穿透/击穿/雪崩必须在 Go 层兜底
Redis 自身不解决这三类问题。靠布隆过滤器或 singleflight.Group 容易过度设计,多数生产场景用轻量级手段更稳。
- 穿透:查不到时存
client.Set(ctx, key, "null", 60*time.Second),后续读到"null"直接跳过 DB;业务层解包前必须判if val == "null" { return nil, nil } - 击穿:热点 key 过期瞬间大量请求打穿 DB,用
singleflight.Group包一层Do,同一 key 的并发请求只放行一个去查 DB - 雪崩:避免所有 key 同一时刻过期,TTL 加随机扰动:
baseTTL + time.Duration(rand.Int63n(int64(2*time.Minute)));每次生成前用rand.New(rand.NewSource(time.Now().UnixNano())),禁用全局rand.Seed()
结构体存 Redis 必须用 json 且字段导出+加 tag
直接 json.Marshal(user) 存进去,json.Unmarshal([]byte(val), &user) 读出来却是空对象?八成是 struct 字段没导出或缺 json tag。
- 字段名必须首字母大写(Go 可导出),且显式加 tag:
ID int `json:"id"`,否则json包跳过该字段 - 禁用
gob—— 它只认 Go 进程,跨语言(Python/Node.js)或未来服务拆分后直接反序列化失败 -
time.Time别直接塞:统一转成int64时间戳再存,避免 JSON 默认 RFC3339 格式带来的时区和解析歧义 - 存之前先
json.Marshal试一下,检查是否报错,能提前发现 struct 定义问题
最常被忽略的是:进程退出前必须调 rdb.Close(),否则 fd 持续增长,最终触发 too many open files;还有就是所有缓存键必须加业务前缀(如 "user:"、"order:"),否则不同服务混用 key 空间,后期排查成本极高。


















