Redis 缓存设计与常见问题处理指南
Redis 常被用来提升接口响应速度、降低数据库压力。但缓存不是简单地 get 一下、set 一下。过期时间、数据一致性、缓存穿透、缓存击穿、缓存雪崩都会影响线上稳定性。
本文从后端业务角度总结 Redis 缓存设计的常见模式和风险处理方法。
一、缓存适合什么数据
适合缓存的数据通常有几个特点:
- 读取频率高
- 写入频率低或可接受短暂延迟
- 计算成本高
- 数据体积可控
- 对强一致性要求不高
典型例子:
1 | 用户基础信息 |
不适合缓存的数据:
1 | 余额扣减 |
这些数据可以使用 Redis 辅助,但不能只靠普通缓存逻辑保证正确性。
二、Cache Aside 模式
最常见的模式是 Cache Aside:
1 | 先读缓存 |
伪代码:
1 | func GetUser(ctx context.Context, id int64) (*User, error) { |
这个模式简单可靠,适合大多数读多写少场景。
三、缓存 Key 设计
Key 要稳定、清晰、可定位:
1 | user:1001 |
建议:
- 使用业务前缀区分模块
- 包含必要版本或日期
- 避免过长 key
- 不要把用户输入原样拼进 key
如果接口返回结构升级,可以加版本:
1 | user:v2:1001 |
这样旧缓存不会污染新结构。
四、过期时间设置
不要所有 key 都设置同一个过期时间。可以按数据类型设计:
1 | 用户信息 5 到 30 分钟 |
为了避免大量 key 同时过期,可以增加随机抖动:
1 | ttl := 10*time.Minute + time.Duration(rand.Intn(60))*time.Second |
这能降低缓存雪崩风险。
五、缓存穿透
缓存穿透指查询一个不存在的数据,每次缓存都 miss,然后打到数据库。
常见处理方式:
1. 缓存空值
数据库查不到时,缓存一个短 TTL 的空值:
1 | user:999999 = "__NULL__" |
下次再查同一个不存在 ID,就不会直接打数据库。
2. 参数校验
明显非法的 ID、页码、类型,应该在进入数据库前拦截。
3. 布隆过滤器
当不存在查询非常多时,可以使用布隆过滤器提前判断 ID 是否可能存在。
六、缓存击穿
缓存击穿指某个热点 key 过期瞬间,大量请求同时打到数据库。
处理方式:
- 热点 key 设置较长 TTL
- 后台定时刷新热点缓存
- 缓存 miss 时加互斥锁
互斥思路:
1 | 第一个请求获得锁,负责查数据库并重建缓存 |
不要让所有请求同时重建同一个热点 key。
七、缓存雪崩
缓存雪崩指大量 key 同时失效,数据库瞬间承压。
常见原因:
- 批量导入时设置了相同 TTL
- Redis 实例故障
- 定时任务集中刷新失败
处理方式:
- TTL 增加随机抖动
- 热点数据分批刷新
- 关键接口增加限流
- Redis 做高可用部署
- 数据库侧保留保护策略
八、写入后如何更新缓存
常见策略是“先更新数据库,再删除缓存”:
1 | 更新数据库 |
为什么不是直接更新缓存?因为复杂对象可能有多个缓存视图,例如:
1 | user:1001 |
直接更新所有相关缓存容易漏。删除缓存让下一次读取重建,通常更简单。
如果对一致性要求更高,可以结合消息队列、binlog 订阅或延迟双删。
九、监控指标
Redis 缓存上线后要关注:
- 命中率
- QPS
- 慢查询
- 内存使用
- key 数量
- 过期 key 数量
- 连接数
- 淘汰策略触发次数
如果命中率很低,说明缓存设计可能没有减少数据库压力,反而增加了系统复杂度。
十、实践建议
缓存设计要明确回答三个问题:
1 | 缓存什么数据? |
Redis 很快,但它不是数据库正确性的替代品。缓存的核心价值是提升读取效率和削峰,业务正确性仍然要由数据库约束、事务和清晰的数据模型来保证。