概念:
缓存雪崩:是指在缓存层面发生的现象,当大量的缓存数据几乎在同一时间内失效过期,导致所有的请求都直接落到数据库上,从而可能引起数据库压力过大、甚至宕机的问题
缓存雪崩可能由以下几个原因:
-
缓存同一时间过期:如果大量缓存数据设置了相同的过期时间,这些缓存将在同一时刻失效。
-
缓存服务宕机:当缓存服务如 Redis 发生故障或重启,所有的数据都会突然不可用,导致所有请求都转向数据库。
-
系统错误:由于系统错误或者配置错误,导致缓存数据被意外清空或失效。
-
资源紧张:在高流量或DDoS攻击下,缓存服务可能因为资源紧张(如内存不足)而无法维持正常运作,导致缓存数据丢失。
缓存雪崩的原理可以通过以下步骤来解释:
-
当缓存中的数据大规模失效时,新的请求无法在缓存中得到响应,因此转向数据库请求数据。
-
数据库需要处理这些原本由缓存处理的请求,导致数据库的读负载急剧增加。
-
如果请求量过大,超出了数据库的处理能力,数据库可能会变得响应缓慢或者完全宕机。
-
数据库宕机后,所有的请求都会失败,整个系统可能会因此停止服务,造成更加严重的连锁反应。
原理:大量缓存数据同时失效,导致所有请求都直接访问数据库。
实际例子:由于缓存层服务重启或者大规模缓存过期,突然所有的请求都绕过缓存直接打到数据库上。
理论解决方案:
-
缓存数据的过期时间分散设置:给缓存数据设置不同的随机过期时间,避免大量缓存同时过期。
-
使用高可用的缓存架构:比如 Redis 集群,确保缓存服务的高可用性。
-
限流降级:在系统访问压力增大时,启动限流降级策略,确保核心服务可用。
解决方案1: 过期时间分散设置:为防止缓存雪崩,可以在缓存时给每个 key 的过期时间加上一个随机值
typescript
import org.springframework.cache.annotation.CachePut;
public class UserService {
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
User updatedUser = updateUserInDatabase(user);
// 设置带有随机偏移的过期时间
cacheUserWithRandomExpiration(updatedUser);
return updatedUser;
}
private void cacheUserWithRandomExpiration(User user) {
long expiration = getExpirationWithJitter();
// 缓存用户并设置过期时间
// ...
}
private long getExpirationWithJitter() {
// 获取一个随机的过期时间
// ...
}
}
解决方案2:限流降级是一种常用的策略,用于在系统压力过大时临时限制访问频率,保护系统稳定运行。在面临缓存雪崩时,限流降级可以防止大量请求同时打到数据库上,从而避免数据库过载
下面是一个简单的 Java 代码示例,展示如何使用 Sentinel 实现限流降级。Sentinel 是阿里巴巴开源的一款轻量级的流量控制、熔断降级的库
1.首先,添加 Sentinel 的依赖到你的项目中
xml
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.8.1</version>
</dependency>
2.然后,配置 Sentinel 规则
java
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.Tracer;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager;
import java.util.ArrayList;
import java.util.List;
public class SentinelConfig {
public static void initDegradeRules() {
List<DegradeRule> rules = new ArrayList<>();
DegradeRule rule = new DegradeRule();
rule.setResource("getHotspotData");
// 使用异常数作为熔断依据
rule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT);
// 设置熔断触发的最小异常数
rule.setCount(5);
// 设置熔断的时间窗口,单位为秒
rule.setTimeWindow(10);
rules.add(rule);
DegradeRuleManager.loadRules(rules);
}
}
3.业务代码中使用 Sentinel 进行保护
typescript
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.Tracer;
import com.alibaba.csp.sentinel.slots.block.BlockException;
public class HotspotDataService {
public Object getHotspotData(String key) {
Entry entry = null;
try {
// Sentinel 保护的资源名
entry = SphU.entry("getHotspotData");
// 正常的业务逻辑
return getDataFromCacheOrDB(key);
} catch (BlockException ex) {
// 如果被限流或降级了,则进入这个代码块
return handleBlockException(key, ex);
} catch (Exception ex) {
// 业务异常
Tracer.trace(ex);
throw ex;
} finally {
if (entry != null) {
entry.exit();
}
}
}
private Object getDataFromCacheOrDB(String key) {
// 获取数据的逻辑
// ...
return new Object();
}
private Object handleBlockException(String key, BlockException ex) {
// 处理被限流或降级的逻辑,比如返回一个默认值或错误提示
// ...
return "Request Blocked";
上面的代码中,getHotspotData
方法被 Sentinel 保护。如果请求达到阈值,Sentinel 会抛出 BlockException
,我们可以捕获这个异常并进行相应的降级处理,例如返回一个默认值或者错误提示.
解决方案3:本地缓存+分布式缓存
ini
// Java伪代码
Object value = localCache.get(key);
if (value == null) {
value = redisCache.get(key);
if (value != null) {
localCache.put(key, value);
}
}