本文目录导读:
这是一个非常经典且重要的后端架构问题,热点数据缓存的核心目标是抗住高并发、降低延迟、保护后端数据库。
下面从识别热点、缓存策略、常见问题与解决三个层面,为你梳理一套完整的方案。
如何识别热点数据?
不是所有数据都适合缓存,首先要判断哪些是热点:
-
业务层面:
- 高频访问: 如秒杀商品详情、首页banner、微博热搜榜、明星大V的粉丝列表。
- 读多写少: 如商品规格(一经发布很少修改)、配置信息。
- 计算复杂: 如经过复杂SQL或逻辑聚合后的报表数据。
-
技术层面:
- 监控与分析: 在业务代码中埋点,或在数据库层面(如慢查询日志、读写分离的从库流量)、网关层面(统计URL访问频率)分析出QPS(每秒查询率)最高的数据Key。
- LUR淘汰观测: 如果使用Redis的allkeys-lru策略,可以观测哪些Key被频繁访问且很少被淘汰。
核心缓存策略与架构
缓存更新模式:避免脏数据
这是最基础也最容易出错的地方,主流有三种模式:
-
Cache Aside(旁路缓存):最常用
- 读: 先查缓存,命中则返回;未命中则查DB,写入缓存,返回。
- 写: 先更新DB,再删除缓存(或更新缓存,但删除更简单安全)。
- 注意: 为什么是“删除”而不是“更新”?因为删除操作是幂等的,能有效避免并发写导致的缓存与DB不一致。
-
Read/Write Through(穿透读写):
缓存层(如Redis)自身负责数据同步,应用层只需跟缓存通信,实现复杂,一般不推荐在业务系统自己实现。
-
Write Behind Caching(异步回写):
- 数据直接写入缓存,隔一段时间异步批量写入DB。性能极高,但存在丢数据风险(如Redis宕机),常用于日志、计数等非关键场景。
解决缓存穿透、击穿、雪崩:三大经典问题
这是热点数据缓存最容易踩的坑,必须有预案:
-
缓存穿透: 查询一个肯定不存在的数据(如恶意id=-1)。
- 解决:
- 缓存空对象:将null也缓存起来,并设置较短过期时间(如5分钟)。
- 布隆过滤器(Bloom Filter): 在缓存前加一层过滤器,判断数据是否存在,不存在则直接返回,这是对抗恶意攻击的最有效手段。
- 解决:
-
缓存击穿: 一个热点数据(如秒杀商品)的缓存恰好过期,瞬间大量请求打到DB。
- 解决:
- 互斥锁(Mutex Key): 第一个请求发现缓存过期,获取分布式锁去查DB;其他请求等待,等锁释放后直接从缓存拿数据,Redis可用
SETNX实现。 - 逻辑过期 + 异步刷新: 缓存永不过期,存储在Value里加一个逻辑过期时间,发现过期时,异步开启一个线程去更新缓存,当前线程直接返回旧数据(牺牲一点一致性换取高可用),这是极致性能方案。
- 互斥锁(Mutex Key): 第一个请求发现缓存过期,获取分布式锁去查DB;其他请求等待,等锁释放后直接从缓存拿数据,Redis可用
- 解决:
-
缓存雪崩: 大量缓存数据在同一时间过期,或Redis宕机。
- 解决:
- 过期时间加随机值: 例如基础过期时间1小时,再加0-300秒的随机数,打散过期时间。
- 多级缓存: Redis作为一级缓存(L1),本地缓存(Caffeine/Guava Cache)作为二级缓存(L2),即使Redis挂掉,本地缓存还能扛一下。
- 缓存预热: 系统启动时,主动加载热点数据到缓存中,避免瞬间涌入。
- Redis高可用: 使用Redis Sentinel或Redis Cluster,避免单点故障。
- 解决:
进阶:针对“高热点”的专项优化
当单个Key的QPS达到几十万甚至上百万时,常规Redis可能成为瓶颈。
热点Key发散(将单Key拆分为多Key)
- 问题: Redis是单线程处理单个Key的,如果
hot这个Key的请求量过大,会把Redis CPU打满。 - 方案:
- 将热点Key拆分为多个子Key,如
hot_001、hot_002…hot_010,请求该Key时,对请求参数(如用户ID)进行Hash(hash(user_id) % 10),随机访问一个子Key。 - 查询时,如果子Key里没有数据,从DB查完后写入所有子Key(或采用一致性哈希)。
- 代价: 增加了数据一致性维护的复杂度(写操作需要更新所有子Key或批量删除)。
- 将热点Key拆分为多个子Key,如
本地缓存 + 旁路架构
- 场景: 热点极高的商品详情页、热搜词。
- 方案:
- 应用服务启动时,从Redis或DB中拉取热点数据,存储到JVM内部的Caffeine或Guava Cache。
- 请求优先走本地缓存,本地未命中再请求Redis。
- 通过一个后台线程(或消息队列)定期推送数据变更到所有应用节点,更新本地缓存。
- 优点: 几乎消除了网络I/O,QPS可以轻松过百万。
- 缺点: 存在多节点数据不一致窗口期,且占用JVM内存。
分层缓存(CDN + Redis + DB)
- 适用于静态或半静态热点数据: 如图片、PDF、视频、CSS/JS。
- 方案:
- CDN(内容分发网络): 缓存静态资源,扛下90%以上的流量。
- CDN回源到Redis: 如果CDN未命中,回源服务器先查Redis。
- Redis再回源到DB: 提供最终一致性。
- 关键配置: 热数据通常设置CDN回源TTL(生存时间)较长(如7天),通过API或缓存Tag主动刷新。
简单总结:一张图流程图 (伪代码/Pseudocode)
// 1. 布隆过滤器(可选,防穿透)
if (!bloomFilter.mightContain(key)) {
return null;
}
// 2. 查本地缓存(可选,JVM级)
value = localCache.get(key);
if (value != null) {
return value;
}
// 3. 查Redis缓存
value = redis.get(key);
if (value != null) {
// 如果是空对象,返回null
if (value == EMPTY_VALUE) return null;
return value;
}
// 4. 缓存未命中,加互斥锁(防击穿)
lockKey = "lock_" + key;
if (redis.setnx(lockKey, "1", 3秒)) {
try {
// 双重检查(非常关键,防止锁等待期间已被其他线程写入)
value = redis.get(key);
if (value != null) {
return value;
}
// 查DB
value = db.query(key);
if (value != null) {
// 设置过期时间+随机值(防雪崩)
redis.setex(key, 3600 + random(300), value);
} else {
// 缓存空对象(防穿透)
redis.setex(key, 300, EMPTY_VALUE);
}
return value;
} finally {
redis.del(lockKey);
}
} else {
// 没拿到锁,等待100ms后重试或降级
Thread.sleep(100);
return redis.get(key); // 或者直接返回本地缓存的老数据
}
给你的建议:
- 不要过度设计: 99%的业务用Cache Aside(旁路缓存) + 设置随机过期时间 + 对空值缓存 就够了。
- 先监控,再优化: 先用Redis的
INFO commandstats或redis-cli --bigkeys找到真正的热点,再考虑拆分或本地缓存。 - 保证最终一致性: 缓存系统允许短暂的(秒级)不一致,但一定要有兜底策略(如定时任务、MQ延迟消息补偿)。
- 做好降级: 缓存挂了或DB压力过大时,系统要有能力优雅降级(如返回旧数据、限流),而不是直接崩溃返回500。
如果你能提供一个具体的业务场景(比如秒杀商品的详情页,或者一个千万粉丝博主的Feed流),我可以针对性地给你更细的架构建议。
标签: Redis缓存策略