掘金 后端 ( ) • 2024-04-12 13:52

概念:

缓存雪崩:是指在缓存层面发生的现象,当大量的缓存数据几乎在同一时间内失效过期,导致所有的请求都直接落到数据库上,从而可能引起数据库压力过大、甚至宕机的问题

缓存雪崩可能由以下几个原因:

  1. 缓存同一时间过期:如果大量缓存数据设置了相同的过期时间,这些缓存将在同一时刻失效。

  2. 缓存服务宕机:当缓存服务如 Redis 发生故障或重启,所有的数据都会突然不可用,导致所有请求都转向数据库。

  3. 系统错误:由于系统错误或者配置错误,导致缓存数据被意外清空或失效。

  4. 资源紧张:在高流量或DDoS攻击下,缓存服务可能因为资源紧张(如内存不足)而无法维持正常运作,导致缓存数据丢失。

缓存雪崩的原理可以通过以下步骤来解释:

  1. 当缓存中的数据大规模失效时,新的请求无法在缓存中得到响应,因此转向数据库请求数据。

  2. 数据库需要处理这些原本由缓存处理的请求,导致数据库的读负载急剧增加。

  3. 如果请求量过大,超出了数据库的处理能力,数据库可能会变得响应缓慢或者完全宕机。

  4. 数据库宕机后,所有的请求都会失败,整个系统可能会因此停止服务,造成更加严重的连锁反应。

原理:大量缓存数据同时失效,导致所有请求都直接访问数据库。

实际例子:由于缓存层服务重启或者大规模缓存过期,突然所有的请求都绕过缓存直接打到数据库上。

理论解决方案:

  • 缓存数据的过期时间分散设置:给缓存数据设置不同的随机过期时间,避免大量缓存同时过期。

  • 使用高可用的缓存架构:比如 Redis 集群,确保缓存服务的高可用性。

  • 限流降级:在系统访问压力增大时,启动限流降级策略,确保核心服务可用。

解决方案1: 过期时间分散设置:为防止缓存雪崩,可以在缓存时给每个 key 的过期时间加上一个随机值

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 的依赖到你的项目中

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-core</artifactId>
    <version>1.8.1</version>
</dependency>

2.然后,配置 Sentinel 规则

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 进行保护

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:本地缓存+分布式缓存

// Java伪代码  
Object value = localCache.get(key);  
if (value == null) {  
    value = redisCache.get(key);  
    if (value != null) {  
        localCache.put(key, value);  
    }  
}