缓存使用场景、一致性及常见问题解析
一、缓存的核心使用场景
1. 高频读、低频写场景
- 典型场景:商品详情页、新闻资讯、用户基本信息。
- 特点:数据更新频率低,但访问量极高。
- 策略 :
- Cache-Aside(旁路缓存):优先读缓存,未命中时查数据库并回填。
- TTL(过期时间):设置合理过期时间(如5分钟),平衡数据新鲜度与缓存命中率。
示例:
java
public Product getProduct(String id) {
Product product = cache.get(id);
if (product == null) {
product = db.query("SELECT * FROM product WHERE id = ?", id);
cache.set(id, product, 300); // 缓存5分钟
}
return product;
}
2. 热点数据加速
- 典型场景:微博热搜、秒杀活动倒计时、直播在线人数。
- 特点:短时间内流量突增,需快速响应。
- 策略 :
- 本地缓存 + 分布式缓存:如Guava Cache + Redis,减少网络开销。
- Key分片 :将热点Key分散到多个节点(如
hot:item:123
→hot:item:123_{shardId}
)。
3. 复杂计算缓存
- 典型场景:排行榜、聚合统计结果(如用户总积分)。
- 特点:计算成本高,结果可复用。
- 策略 :
- 预计算+定时更新:定时任务生成结果并缓存。
- Write-Through(直写):数据变更时同步更新缓存。
4. 会话与状态管理
- 典型场景:用户登录状态、购物车信息。
- 特点:临时性数据,读写频繁。
- 策略 :
- Redis Session存储:集中管理会话,支持分布式扩展。
- TTL自动清理:设置会话过期时间(如30分钟)。
二、缓存一致性问题及解决方案
1. 一致性问题的根源
- 数据源多副本:缓存与数据库存在两份数据。
- 并发操作:多线程/多节点同时修改数据。
- 网络延迟:缓存与数据库更新不同步。
2. 典型场景与解决方案
场景 | 问题描述 | 解决方案 |
---|---|---|
先更新数据库,后删缓存 | 删除缓存失败,后续请求读到旧数据。 | 延迟双删:更新数据库 → 删除缓存 → 延迟几百毫秒再删一次。 |
先删缓存,后更新数据库 | 删缓存后、更新数据库前,其他请求可能读到旧值并回填缓存。 | 异步重试:删除缓存后,通过消息队列确保数据库更新成功,否则重试删除操作。 |
并发写导致覆盖 | 多个线程同时更新同一数据,缓存与数据库不一致。 | 分布式锁:更新时加锁(如Redis SETNX),串行化操作。 |
示例:延迟双删伪代码
java
public void updateProduct(Product product) {
// 1. 更新数据库
db.update(product);
// 2. 删除缓存
cache.delete(product.getId());
// 3. 延迟再次删除(应对并发场景)
executor.schedule(() -> cache.delete(product.getId()), 500, TimeUnit.MILLISECONDS);
}
3. 最终一致性方案
- Binlog监听:通过Canal监听数据库变更,异步更新缓存。
- 消息队列:数据库变更后发送MQ消息,消费者更新缓存。
三、缓存常见问题与应对策略
1. 缓存穿透(Cache Penetration)
- 问题:大量请求查询不存在的数据(如无效ID),绕过缓存直接访问数据库。
- 解决 :
- 布隆过滤器(Bloom Filter):预存所有合法Key,拦截非法请求。
- 缓存空值 :对不存在的数据缓存
NULL
并设置短TTL(如30秒)。
示例:
java
public Product getProduct(String id) {
Product product = cache.get(id);
if (product != null) {
return product;
}
if (bloomFilter.mightContain(id)) { // 布隆过滤器检查
product = db.query("SELECT * FROM product WHERE id = ?", id);
if (product != null) {
cache.set(id, product, 300);
} else {
cache.set(id, NULL, 30); // 缓存空值
}
}
return product;
}
2. 缓存雪崩(Cache Avalanche)
- 问题:大量缓存同时失效,请求直接冲击数据库。
- 解决 :
- 随机过期时间 :在基础TTL上增加随机值(如
TTL + random(0, 300)
)。 - 热点数据永不过期:后台定时异步更新缓存。
- 随机过期时间 :在基础TTL上增加随机值(如
3. 缓存击穿(Cache Breakdown)
- 问题:热点Key过期瞬间,大量并发请求直达数据库。
- 解决 :
- 互斥锁(Mutex Lock):第一个请求重建缓存时加锁,其他请求等待。
- 逻辑过期:缓存Value包含逻辑过期时间,异步刷新。
示例:互斥锁实现
java
public Product getProduct(String id) {
Product product = cache.get(id);
if (product == null) {
Lock lock = redisLock.lock(id); // 获取分布式锁
try {
product = cache.get(id); // 双重检查
if (product == null) {
product = db.query("SELECT * FROM product WHERE id = ?", id);
cache.set(id, product, 300);
}
} finally {
redisLock.unlock(id);
}
}
return product;
}
4. 数据漂移(Hot Key 不均)
- 问题:某个节点缓存过多热点Key,导致负载不均。
- 解决 :
- 本地缓存:在应用层缓存热点数据,减少访问分布式缓存。
- Key分片 :将热点Key分散到多个节点(如
product:123
→product:123_{shardId}
)。
四、缓存策略选择
策略 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
Cache-Aside | 通用场景,需强一致性控制 | 灵活,缓存与数据库解耦 | 需处理一致性问题 |
Read-Through | 缓存作为主要数据源 | 简化代码,自动加载数据 | 缓存层需支持加载逻辑 |
Write-Through | 写操作频繁且需强一致性 | 数据更新实时同步 | 写延迟较高 |
Write-Behind | 高吞吐写场景(如日志记录) | 写性能高,批量更新数据库 | 数据可能丢失(未持久化前宕机) |
🐮🐎
- 使用场景:缓存适用于读多写少、热点数据、复杂计算等场景,显著提升系统性能。
- 一致性问题:通过延迟双删、异步消息、分布式锁等方案平衡性能与一致性。
- 常见问题 :穿透、雪崩、击穿需针对性设计防御策略(布隆过滤器、随机TTL、互斥锁)。
合理选择缓存策略,结合监控与动态调优,可最大化缓存收益并规避潜在风险。
