必须配置连接池:minidleconns设5–10,maxconnage设30分钟,poolsize按qps设20–30;防穿透需缓存空值;读用原子lua或信任ttl;写优先删缓存而非更新;key须纯ascii,value依类型选序列化方式。

用 go-redis/v9 连 Redis 之前,先配好连接池
不设连接池的 redis.Client 在并发稍高时会迅速耗尽文件描述符,报错 dial tcp 127.0.0.1:6379: connect: cannot assign requested address。这不是 Redis 挂了,是 Go 进程自己把 socket 耗光了。
-
MinIdleConns建议设为 5–10,避免每次请求都新建连接 -
MaxConnAge必须设(比如30 * time.Minute),防止长连接因网络中间件(如 NAT、SLB)静默断连后还被复用 -
PoolSize别盲目调大;QPS 1k 左右的服务,20–30足够,再大反而增加锁竞争 - 别用
redis.Dial或老版redigo——v9 的 context-aware API 能正确中断阻塞操作,避免 goroutine 泄漏
缓存读逻辑:先查 Redis,未命中再查 DB,但空结果也得缓存
“缓存穿透”不是理论风险,是真实线上事故:攻击者用大量随机 user_id 请求,导致所有查询都击穿到 DB,数据库 CPU 直冲 100%。
- DB 查无结果时,仍要执行
redisClient.Set(ctx, key, "null", 2*time.Minute),值可为任意占位符(如"{}"),但需在反序列化前判断 - 不要用
EXISTS+GET两步走——竞态下可能刚查完就过期,应改用 Lua 脚本原子读写,或直接信任 TTL - 对用户专属数据(如
"user:profile:123"),TTL 设短些(5–30s),配合主动失效,别依赖长时间自动过期
缓存写逻辑:删缓存,别更新缓存
更新缓存(SET)看似直观,但极易引发数据不一致:DB 提交成功,Redis 写失败,或写入中途 panic,缓存就永远卡在旧值。
- 写 DB 成功后,只调
redisClient.Del(ctx, key),下次读自然重建,逻辑简单且幂等 - 删除动作建议加日志和错误重试(比如失败时发到本地 channel,由后台 goroutine 重试 3 次),但别阻塞主流程
- 批量更新时,避免用
DEL key1 key2 key3传一堆 key——key 太多会阻塞 Redis,改用SCAN+ 管道分批删,或设计带通配的命名空间(如"user:123:*")
Key 和 Value 序列化:别让 json.Marshal 出的字符串当 key
用 json.Marshal(map[string]interface{}{"id": 123, "type": "user"}) 生成 key,结果含换行和空格,Redis 直接拒绝,报错 ERR invalid UTF-8 string。
立即学习“go语言免费学习笔记(深入)”;
- key 必须是纯 ASCII 字符串;推荐用
fmt.Sprintf("cache:user:profile:%d", userID)构造 - value 用
json.Marshal没问题,但记得加json:"omitempty"减少体积,且结构体字段顺序固定(别混用map和 struct) - 如果 value 是简单类型(如 int、string),别套 JSON——直接用
strconv.Itoa或原生类型存,省序列化开销
真正难的不是连上 Redis 或写对 Set,而是想清楚哪条数据该缓存多久、谁有权删它、以及当 Redis 暂时不可用时你的 handler 是否还能降级响应。这些决策藏在业务逻辑里,没法靠库自动解决。










