3、业务场景与缓存策略:读多写少场景、热点数据场景、时间窗口场景
大家好,我是老张。今天咱们聊聊缓存命中率偏低时,最常遇到的三个业务场景。说实话,我见过太多团队一上来就堆Redis,结果命中率不到30%,白白浪费内存。你想想看,问题出在哪?
其实很简单——缓存策略没选对。不同的业务场景,就像不同的锁,你得用对应的钥匙去开。下面我结合自己的踩坑经历,把这三个典型场景掰开揉碎了讲清楚。
3.1 读多写少场景:缓存最舒服的“舒适区”
读多写少,说白了就是数据不怎么变,但大家反复查。比如商品详情页、用户配置信息、系统字典表。这种场景下,缓存命中率天然就高,但前提是你别自己搞砸了。
核心策略:Cache-Aside + 较长的过期时间
我个人习惯用Cache-Aside模式。读的时候先查缓存,没命中就查数据库,然后回填缓存。写的时候直接更新数据库,再删除缓存。
这里有个坑:过期时间设多长? 我建议设成业务可接受的最大延迟时间。比如商品信息允许5分钟延迟,那就设300秒。别设成永久,否则数据更新后,用户看到的还是旧数据。
// 读多写少场景的伪代码示例
public Product getProduct(Long id) {
// 1. 先查缓存
Product product = cache.get("product:" + id);
if (product != null) {
return product;
}
// 2. 缓存没命中,查数据库
product = dao.getProductById(id);
if (product != null) {
// 3. 回填缓存,过期时间300秒
cache.set("product:" + id, product, 300);
}
return product;
}
我的经验: 读多写少场景下,缓存击穿是最大威胁。我曾经遇到一个商品详情页,某个爆款商品上线后,缓存刚过期,瞬间几千个请求打到数据库,直接挂了。解决方案很简单:加一个互斥锁,或者用“永不过期+后台异步更新”的策略。
3.2 热点数据场景:流量洪峰下的“保命符”
热点数据,就是那些被大量请求同时访问的数据。比如双十一的秒杀商品、微博热搜、直播间弹幕。这种场景下,缓存命中率可能很高,但缓存雪崩和缓存击穿的风险也极高。
为什么会这样?你想想看,一个热点Key,如果过期了,所有请求同时去查数据库,数据库瞬间就扛不住了。我记得有一次,某大促活动,一个热点商品Key过期,导致数据库连接池被打满,整个服务都瘫痪了。
核心策略:本地缓存 + 分布式缓存 + 限流降级
我建议的做法是:在应用层加一层本地缓存(比如Caffeine或Guava Cache),把热点数据缓存在JVM内存里。这样即使Redis挂了,本地缓存还能顶一阵。
具体怎么识别热点数据?可以用LFU(Least Frequently Used)算法,统计每个Key的访问频率。当某个Key的QPS超过阈值时,自动把它加入本地缓存。
// 热点数据识别与本地缓存示例
public class HotKeyDetector {
private static final int HOT_THRESHOLD = 1000; // QPS阈值
private Cache<String, Object> localCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.SECONDS)
.build();
public Object getData(String key) {
// 1. 先查本地缓存
Object value = localCache.getIfPresent(key);
if (value != null) {
return value;
}
// 2. 查Redis
value = redis.get(key);
if (value != null) {
// 3. 判断是否为热点数据
if (isHotKey(key)) {
localCache.put(key, value);
}
return value;
}
// 4. 查数据库
value = dao.getData(key);
if (value != null) {
redis.set(key, value, 300);
}
return value;
}
private boolean isHotKey(String key) {
// 统计QPS的逻辑,略
return true;
}
}
注意: 本地缓存虽然快,但会占用应用内存。我建议把本地缓存的大小控制在1000个条目以内,过期时间不超过10秒。否则,内存溢出可不是闹着玩的。
3.3 时间窗口场景:数据“过期不候”
时间窗口场景,指的是数据只在某个时间段内有效。比如:验证码5分钟有效、限时抢购活动、每日签到数据。这种场景下,缓存命中率往往不高,因为数据过期很快。
嗯,这里要注意:不要用固定的过期时间。比如所有验证码都设300秒过期,那如果用户在第299秒输入,缓存刚好过期,就会导致验证失败。用户体验极差。
核心策略:TTL(Time To Live)+ 滑动窗口
我建议的做法是:用Redis的EXPIRE命令设置过期时间,同时配合滑动窗口机制。比如验证码,每次验证时,如果验证码正确但缓存已过期,就返回“验证码已过期”,而不是直接报错。
另外,时间窗口场景下,缓存穿透是个常见问题。比如恶意用户不断请求一个不存在的验证码,每次都会穿透到数据库。解决方案是:用布隆过滤器,或者缓存空值(但过期时间要短,比如30秒)。
// 时间窗口场景的验证码校验示例
public boolean verifyCode(String phone, String code) {
String key = "sms:code:" + phone;
String cachedCode = redis.get(key);
if (cachedCode == null) {
// 缓存已过期,返回“验证码已过期”
return false; // 或者抛出特定异常
}
if (cachedCode.equals(code)) {
// 验证成功,删除缓存(防止重复使用)
redis.del(key);
return true;
}
return false;
}
我的经验: 时间窗口场景下,我建议把过期时间设置得比业务要求稍微长一点。比如验证码要求5分钟有效,我就设成310秒。为什么?因为网络延迟、时钟偏差等因素,可能会导致缓存提前几秒过期。多出来的10秒,就是容错空间。
3.4 缓存策略选择:一张表说清楚
好了,三个场景讲完了。最后我总结一张表,方便你对照选择。
| 业务场景 | 推荐策略 | 过期时间 | 注意事项 |
|---|---|---|---|
| 读多写少 | Cache-Aside | 较长(如300秒) | 防止缓存击穿,加互斥锁 |
| 热点数据 | 本地缓存+分布式缓存 | 较短(如10秒) | 识别热点Key,限流降级 |
| 时间窗口 | TTL+滑动窗口 | 略长于业务要求 | 防止缓存穿透,缓存空值 |
其实,缓存策略没有银弹。我见过很多团队,不管什么场景都用同一种策略,结果命中率惨不忍睹。你想想看,是不是这个道理?
一句话总结: 读多写少用Cache-Aside,热点数据加本地缓存,时间窗口用TTL+滑动窗口。别偷懒,针对场景选策略,缓存命中率自然就上去了。
好了,这一章就到这里。记住,缓存策略选对了,命中率自然就高了。下一章我们聊聊缓存穿透的实战排查方法,到时候见。