应封装带缓存逻辑的FindUserByID函数:查缓存优先,命中则json.Unmarshal返回;未命中则GORM查询后json.Marshal存入Redis,键含业务前缀与结构体版本号;更新DB后采用延迟双删策略,并为Redis操作设超时。

怎么让GORM查完自动写进Redis缓存
直接在GORM查询后手动调用rdb.Set()不是不行,但容易漏、难维护。真正可落地的做法是封装一个带缓存逻辑的FindUserByID函数,而不是改GORM源码或加全局钩子。
关键点在于:缓存键要带业务前缀和结构体版本号(比如user:v2:123),避免字段变更后旧缓存反序列化失败;值用json.Marshal存,别用gob——跨服务或未来换语言时会踩坑。
- 查缓存优先:先
rdb.Get(ctx, "user:v2:123").Result(),命中就json.Unmarshal返回,跳过DB - 未命中再查GORM:
db.First(&user, id),成功后立刻rdb.Set(ctx, key, data, 10*time.Minute).Err() - 注意
ctx超时:Redis操作必须带context.WithTimeout,否则慢查询会拖垮整个HTTP请求
更新数据库后,Redis缓存怎么删才不丢数据
“先删缓存再更新DB”看似简单,但并发下极易出现脏读:A删缓存→B查库(没缓存,从DB读到旧值)→B写缓存→A更新DB完成。结果缓存里存的是旧数据。
生产环境推荐“延迟双删”:DB更新成功后,起一个time.AfterFunc(500*time.Millisecond, func(){ rdb.Del(ctx, key) })。500ms足够让主从同步完成,又不会让延迟太长。
- 别用
time.Sleep阻塞协程,必须用AfterFunc或独立goroutine - 删除动作本身也要加错误重试,比如失败时记录日志并推入异步任务队列
- 如果用Canal+Kafka监听Binlog,这一步可以交给消费者做,应用层只负责第一删
go-redis v9连接池参数怎么设才不爆内存
默认的MaxIdleConns=10和MaxOpenConns=100在高并发场景下往往不够。实际值要看你的QPS和平均响应时间,不是拍脑袋定的。
公式参考:MaxOpenConns ≈ QPS × 平均耗时(秒)× 2。比如QPS=200、DB平均耗时100ms,则建议设为40;Redis更快,可设为20~30。但MaxIdleConns别超过MaxOpenConns的1/3,否则空闲连接占内存不释放。
- 务必调
rdb.SetConnMaxLifetime(30*time.Minute),防止连接被中间件(如RDS代理)静默断开 - 用
redis.NewFailoverClient代替NewClient,当Redis主节点宕机时自动切到从节点 - 每次
rdb.Get()后检查err == redis.Nil,这是正常未命中,不是错误
GORM事务里混用Redis操作会出什么问题
在db.Transaction()回调里调rdb.Set(),表面看没问题,但一旦Redis网络抖动超时,事务会卡住甚至回滚失败——GORM事务和Redis操作根本不共享事务上下文。
正确做法是把Redis操作移出事务:先完成DB事务,再单独处理缓存。如果要求强一致,就用消息队列兜底,比如Kafka发一条user_updated事件,由独立消费者删缓存。
- 绝对不要在
tx.Create()后立刻rdb.Set()并期望原子性 - 如果必须同步删缓存,至少给Redis操作加
context.WithTimeout(ctx, 100*time.Millisecond),超时直接放弃,靠后续延迟双删补救 - PostgreSQL的
LISTEN/NOTIFY机制可替代Kafka做轻量级通知,但go-redis不原生支持,需自己封装
最常被忽略的是缓存键的设计粒度。用user:123没问题,但查列表时用user:list:page1这种键,分页参数一变就失效,不如直接缓存接口级结果(如api:/v1/users?sort=name),配合ETag或Last-Modified头做客户端缓存协同。


















