必须用redis.newclusterclient连接redis cluster,而非newclient;前者内置槽位路由、节点发现与故障转移,后者会导致90%请求报moved/ask错误并卡死或panic。

用 redis.NewClusterClient 连集群,不是 redis.NewClient
直接拿单节点客户端去连 Redis Cluster,90% 的请求会报 MOVED 或 ASK 错误,然后卡死或 panic。Redis Cluster 要求客户端自己解析重定向、维护槽位映射、自动发现节点——这些 redis.NewClient 压根不干。
redis.NewClusterClient 才是正确入口,它内置槽位路由、节点自动发现、故障转移感知。漏掉这一步,后面所有缓存逻辑都跑在错误的抽象上。
- 初始化时传入的
Addrs只需填 2–3 个可连通节点(比如[]string{"192.168.1.100:6379", "192.168.1.101:6379"}),client 启动后会自动拉取整个集群拓扑 - 必须设
Timeout: 3 * time.Second和MaxRetries: 2,否则网络抖动时请求卡在重试里,ctx 超时都救不回来 - 别复用单节点配置模板:
Password和DB字段在 Cluster 模式下无效,填了也不生效;认证密码要统一配在每个节点上,用Username+Password(Redis 6+)或只用Password(旧版)
连接池参数必须显式调,不能依赖默认值
Cluster 客户端的连接池不是全局一个,而是每个已知节点维护独立连接池。默认 PoolSize=10 看似够用,但实际是每个节点各开 10 条连接——5 个节点就是 50 条,QPS 上千时立刻排队阻塞,日志里刷满 redis: connection pool timeout。
真正该设的是 MinIdleConns 和 MaxConnAge:前者防冷启动慢,后者防中间设备静默断连导致后续请求卡在 write: broken pipe。
-
PoolSize建议设为预估 QPS × 2 ÷ 节点数(例如集群 5 节点、QPS 500 → 设 200),但单节点池上限别超 200,避免打爆 Redismaxclients - 强制加
MinIdleConns: 5,确保每个节点常驻空闲连接,首请求不等建连 - 必设
MaxConnAge: 30 * time.Minute,让连接定期重建,避开 NAT 超时、防火墙踢连接等问题 - 初始化后立刻用带超时的 ctx 调
rdb.Ping(ctx).Err(),别等第一次Get才暴露地址错或认证失败
结构体存 Redis,json 是底线,gob 是陷阱
Go 结构体往 Cluster 里存,第一反应是 json.Marshal,但字段没导出、缺 tag、time.Time 直接塞进去,结果读出来全是零值——这不是 Cluster 的锅,是序列化层没过检。
更危险的是用 gob:本地跑得飞快,一上生产就崩。下游 Python 服务查不到 key?Node.js 写进去的值 Go 读不出来?八成是用了 gob。
- 字段必须首字母大写(导出),且显式加
json:"field_name",别信omitempty默认行为,空字符串和零值污染缓存很常见 -
time.Time统一转UnixMilli()存int64,读的时候再转回,避免 JSON 的 RFC3339 字符串和 gob 的纳秒整数对不上 - 存之前先
json.Marshal(user)试一下,检查是否返回 error,能提前发现 struct 定义问题 - 跨语言或对外 API 场景,禁用 gob;纯 Go 微服务内部若真要用 gob,必须提前
gob.Register(&User{}),且字段增删需同步升级所有实例
缓存穿透/击穿/雪崩,Cluster 不兜底,得在 Go 层写死逻辑
Redis Cluster 自身不识别“空值”“热点”“批量过期”,它只管分片和转发。穿透时 1000 个请求同时打穿 DB,不是因为 Cluster 慢,是因为 Go 层没拦截 redis.Nil 就放行查库。
击穿和雪崩同理:Cluster 不知道哪个 key 是热点,也不会给 TTL 加随机数。这些策略必须硬编码在业务调用前。
- 穿透:查
Get返回redis.Nil时,立刻Set(ctx, key, "null", 60*time.Second),后续读到"null"就跳过 DB;业务层解包前必须判if val == "null" { return nil, nil } - 击穿:用
singleflight.Group包一层Do,同一 key 并发请求只放行一个查 DB,其余等待结果,避免 DB 被打垮 - 雪崩:TTL 别写死,每次 Set 都生成扰动:
baseTTL + time.Duration(rand.Int63n(int64(2*time.Minute)));种子必须每次新建:rand.New(rand.NewSource(time.Now().UnixNano())),禁用全局rand.Seed
最易忽略的一点:所有这些逻辑都得放在 Get 和 Set 的封装函数里,而不是散落在 handler 中——否则新增一个接口就可能漏掉穿透防护。











