★ Redis 在 Gin 服务中的典型用法(缓存/锁/会话)
一句话结论:Redis 在 Gin 服务里按“缓存、锁、会话与计数”三类使用。缓存走 cache-aside:读 Redis 未命中→回源 DB→回填并设 TTL,再叠加防穿透(空值缓存/布隆)、防击穿(单飞/互斥重建)、防雪崩(TTL 加随机抖动);分布式锁用 SET key token NX PX 原子加锁、Lua 比对 token 再删除来释放、带续期看门狗,锁只是并发前置手段,最终一致性仍要 DB 唯一约束兜底;会话/登录态可存 Redis(配合 JWT 黑名单与刷新),此外 INCR+EXPIRE 可做计数与限流、ZSET 做榜单与延迟队列。记住:Redis 是缓存与加速层,DB 才是事实源。
三类用法
| 用法 | 典型命令/结构 | 关键点 |
|---|---|---|
| 缓存 | GET/SETEX(go-redis: Set(ctx, k, v, ttl)) | 防穿透/击穿/雪崩;先更 DB 再删缓存 |
| 分布式锁 | SET k token NX PX ttl;Lua DEL 比对 | 必须设过期;释放要验 token;续期看门狗 |
| 会话/计数 | SETEX session;INCR + EXPIRE;ZSET | JWT 黑名单、限流计数、榜单、队列 |
cache-aside 示例
func getUser(id int64) (*User, error) {
key := fmt.Sprintf("user:%d", id)
val, err := rdb.Get(ctx, key).Bytes()
if err == nil { return unmarshalUser(val), nil }
u, err := dao.FindUser(id) // 回源 DB
if err != nil { return nil, err }
// 防穿透:空结果也短缓存;TTL 加随机抖动防雪崩
ttl := baseTTL + time.Duration(rand.Int63n(300))*time.Second
_ = rdb.Set(ctx, key, marshalUser(u), ttl).Err()
return u, nil
}
分布式锁示例
// 加锁:原子 SET NX PX,value 放随机 token
ok, err := rdb.SetNX(ctx, lockKey, token, 10*time.Second).Result()
// 释放:Lua 保证“比对 token 再删”原子性,防止误删别人的锁
delScript := redis.NewScript(`if redis.call("get", KEYS[1]) == ARGV[1]
then return redis.call("del", KEYS[1]) else return 0 end`)
delScript.Run(ctx, rdb, []string{lockKey}, token)
注意:业务时间要远小于锁过期时间,超时兜底由锁过期完成;长任务需看门狗续期。Redis 主从切换瞬间可能丢锁,强一致要求高时再讨论 Redlock 或多副本,一般业务“锁防并发 + DB 唯一约束兜底”就够了。
追问记忆点
- 追问:为什么不直接 DEL 释放锁?——可能删掉别人刚续期/新获取的锁;必须比对持有者 token。
- 追问:缓存与 DB 一致性怎么做?——先更新 DB 成功再删缓存(Cache Aside),删失败用延迟重试/消息兜底,而不是先写缓存。
- 追问:热点 key 击穿怎么办?——单机 singleflight 合并回源,或用互斥锁只让一个请求重建缓存。
- 记忆点:缓存三防(穿透/击穿/雪崩);锁三要素(NX+PX、token 校验删除、续期);会话与计数靠 TTL 纪律;DB 永远兜底。