Redis 缓存最佳实践 Skill
描述
这个 skill 帮助开发者在后端服务中正确、高效、安全地使用 Redis。涵盖了缓存键命名规范、缓存更新策略、高并发下的三大缓存问题(穿透、击穿、雪崩)的解决方案,以及分布式锁的正确实现。
何时使用
在以下场景中使用这个 skill:
- AI 协助编写涉及 Redis 读写操作的代码时
- 用户需要优化高并发查询接口时
- 用户询问如何解决缓存穿透、击穿、雪崩时
- 需要实现分布式锁或限流功能时
核心设计原则
1. Key 命名规范
规范的 Key 命名有助于排查问题和管理内存。
- 格式:
业务线:子模块:实体类型:实体ID - 分隔符:统一使用冒号
:分隔。 - 示例:
- ✅
user:profile:info:12345 - ✅
order:payment:status:ORD999
- ✅
- 长度控制:Key 不宜过长,尽量控制在 64 字节以内,避免占用过多内存。
2. 缓存更新策略 (Cache Update Patterns)
推荐使用 Cache Aside Pattern (旁路缓存模式):
读操作:
- 先读缓存,命中则直接返回。
- 未命中,则读数据库。
- 将数据库读到的数据写入缓存,并设置过期时间。
写操作:
- 先更新数据库。
- 再删除缓存(注意是删除,不是更新)。
为什么是删除而不是更新? 并发写时,更新缓存容易导致脏数据;删除缓存操作简单,且下次读取时会自动加载最新数据(延迟计算)。
3. 高并发缓存三大问题及解决方案
缓存穿透 (Cache Penetration)
定义:查询一个根本不存在的数据,缓存层和数据库层都不会命中,导致请求全部打到数据库。 解决方案:
- 缓存空对象:即使数据库返回空,也将其缓存起来(如缓存一个特殊的标识
"{}"),并设置较短的过期时间(如 30 秒)。 - 布隆过滤器 (Bloom Filter):在缓存层前加一层布隆过滤器,快速判断数据是否存在。
缓存击穿 (Cache Breakdown)
定义:一个热点 Key 在失效的瞬间,海量并发请求同时打到数据库,压垮数据库。 解决方案:
- 互斥锁 (Mutex Lock):缓存失效时,只有一个线程能去数据库查询并重建缓存,其他线程等待或重试。(go-zero 中的
core/syncx/SingleFlight完美解决此问题)。 - 逻辑过期:物理上不设置过期时间,在 Value 中存入逻辑过期时间。发现逻辑过期时,异步开启线程去更新缓存,当前请求返回旧数据。
缓存雪崩 (Cache Avalanche)
定义:大量 Key 在同一时间集中失效,或者 Redis 宕机,导致大量请求打到数据库。 解决方案:
- 过期时间随机化:在基础过期时间上增加一个随机值(如 1~5 分钟的随机抖动),避免集中失效。
- 多级缓存:本地缓存(如 Go 的
bigcache或freecache) + Redis 分布式缓存。 - 服务降级与限流:在网关层或服务层进行限流。
4. 分布式锁规范 (基于 go-zero)
在 go-zero 框架中,严禁手动使用 SET NX EX 和 Lua 脚本去造轮子,必须使用 go-zero 官方提供的 core/stores/redis.RedisLock 组件。该组件已经内置了原子加锁、唯一标识(随机字符串)防误删、Lua 脚本安全解锁以及锁重入(续期)等特性。
标准使用范例:
import "github.com/zeromicro/go-zero/core/stores/redis"
// 1. 创建锁实例 (rdb 为 *redis.Redis 实例)
lock := redis.NewRedisLock(rdb, "lock:order:process:12345")
// 2. 设置过期时间(必须设置,防止死锁。单位为秒)
lock.SetExpire(5)
// 3. 尝试获取锁
// AcquireCtx 支持传入 context,内部包含 500ms 的容忍重试机制
acquired, err := lock.AcquireCtx(ctx)
if err != nil {
return err
}
if !acquired {
return fmt.Errorf("操作太频繁,请稍后再试") // 或返回特定错误码
}
// 4. 确保释放锁
defer lock.ReleaseCtx(ctx)
// 5. 执行业务临界区代码
// ...
关键注意事项:
- 必须设置超时时间:调用
SetExpire(seconds),否则一旦进程崩溃会导致死锁。 - 无自动看门狗 (Watchdog):go-zero 的
RedisLock默认没有后台自动续期线程。如果业务执行时间可能超过锁的 TTL,需要开发者自行评估 TTL 长度,或者在业务中手动再次调用lock.Acquire()来刷新 TTL(go-zero 的锁支持同一实例重入续期)。 - 必须校验 acquired 结果:
err == nil仅代表 Redis 请求成功,不代表拿到了锁,必须判断acquired == true。
AI 交互指导
当 AI 协助生成涉及 Redis 的 Go 代码时,必须:
- 检查 Key 的命名是否符合规范。
- 强制要求所有的缓存 Key 必须设置过期时间 (TTL),且针对批量生成的 Key 建议加上随机抖动。
- 在处理高并发查询逻辑时,主动建议使用
SingleFlight来防止缓存击穿。 - 在编写更新逻辑时,确保遵循“先更新 DB,后删除 Cache”的策略。
- 当用户需要实现分布式锁时,必须使用 go-zero 的
redis.NewRedisLock,并强制加上SetExpire和defer lock.ReleaseCtx()逻辑。