【Redis】【缓存】理解缓存三大问题:缓存穿透、缓存击穿与缓存雪崩及解决方案

在高并发系统中,缓存是提升性能的关键组件。然而,缓存的使用也伴随着一些潜在问题,其中最常见的就是缓存穿透、缓存击穿和缓存雪崩。本文将详细解析这三个问题,并结合 Java 与 Redisson 框架提供具体解决方案。

1. 缓存穿透

什么是缓存穿透?

缓存穿透指的是查询一个根本不存在的数据,导致请求绕过缓存直接打到数据库,而数据库也查不到,于是不会写入缓存,下次再有相同请求,依然会穿透缓存直达数据库。。每次请求都会如此,给数据库带来巨大压力。

解决方案

1)空值缓存:对于查询结果为空的数据,也将其缓存起来(设置较短的过期时间) 2)布隆过滤器:在缓存之前设置布隆过滤器,快速判断数据是否存在

空值缓存实现

java 复制代码
@Autowired
private RedissonClient redissonClient;

/**
 * 使用空值缓存解决缓存穿透
 */
public Object solveCachePenetration(String key) {
    // 1. 先查询缓存
    Object value = redissonClient.getBucket(key).get();
    if (value != null) {
        // 2. 缓存存在,直接返回(可能是空值)
        return value.equals("NULL") ? null : value;
    }
    
    try {
        // 3. 缓存不存在,查询数据库
        value = database.query(key);
        
        // 4. 无论查询结果是否存在,都存入缓存
        RBucket<Object> bucket = redissonClient.getBucket(key);
        if (value == null) {
            // 空值设置较短的过期时间,如5分钟
            bucket.set("NULL", 5, TimeUnit.MINUTES);
        } else {
            // 正常值设置较长的过期时间,如1小时
            bucket.set(value, 1, TimeUnit.HOURS);
        }
        return value;
    } catch (Exception e) {
        log.error("查询数据库异常", e);
        return null;
    }
}

布隆过滤器实现

java 复制代码
/**
 * 使用布隆过滤器解决缓存穿透
 */
public class BloomFilterSolution {
    private RBloomFilter<String> bloomFilter;
    
    @PostConstruct
    public void initBloomFilter() {
        // 初始化布隆过滤器,预计元素数量100万,误判率0.01
        bloomFilter = redissonClient.getBloomFilter("idBloomFilter");
        bloomFilter.tryInit(1000000, 0.01);
        
        // 从数据库加载所有存在的ID到布隆过滤器
        List<String> existingIds = database.loadAllExistingIds();
        existingIds.forEach(id -> bloomFilter.add(id));
    }
    
    public Object getData(String id) {
        // 1. 先通过布隆过滤器判断ID是否存在
        if (!bloomFilter.contains(id)) {
            log.info("ID:{} 不存在,直接返回空", id);
            return null;
        }
        
        // 2. 布隆过滤器认为存在,再查询缓存和数据库
        Object value = redissonClient.getBucket(id).get();
        if (value != null) {
            return value;
        }
        
        // 3. 缓存未命中,查询数据库
        value = database.query(id);
        if (value != null) {
            redissonClient.getBucket(id).set(value, 1, TimeUnit.HOURS);
        }
        return value;
    }
}

2. 缓存击穿

什么是缓存击穿?

缓存击穿指的是一个热点 key 在缓存中过期的瞬间,有大量并发请求同时访问这个 key,导致所有请求都打到数据库,造成数据库瞬间压力过大。

解决方案

1)互斥锁:只允许一个线程去数据库查询并更新缓存,其他线程等待 2)热点数据永不过期:对于特别热点的数据,设置永不过期 3)定时更新:提前通过后台线程更新即将过期的热点数据

互斥锁实现

java 复制代码
/**
 * 使用Redisson分布式锁解决缓存击穿
 */
public class CacheBreakdownSolution {
    
    @Autowired
    private RedissonClient redissonClient;

    public Object solveCacheBreakdown(String key) {
        // 1. 先查询缓存
        Object value = redissonClient.getBucket(key).get();
        if (value != null) {
            return value;
        }
        
        RLock lock = null;
        try {
            // 2. 获取分布式锁,锁的key可以是原key加后缀
            lock = redissonClient.getLock(key + ":lock");
            
            // 3. 尝试获取锁,最多等待100ms,10秒后自动释放
            boolean isLocked = lock.tryLock(100, 10, TimeUnit.MILLISECONDS);
            if (isLocked) {
                // 4. 成功获取锁后,再次检查缓存,防止重复查询数据库
                value = redissonClient.getBucket(key).get();
                if (value != null) {
                    return value;
                }
                
                // 5. 缓存仍未命中,查询数据库
                value = database.query(key);
                if (value != null) {
                    // 6. 将查询结果存入缓存
                    redissonClient.getBucket(key).set(value, 1, TimeUnit.HOURS);
                }
                return value;
            } else {
                // 7. 未获取到锁,等待一段时间后重试
                Thread.sleep(50);
                return solveCacheBreakdown(key); // 递归重试
            }
        } catch (InterruptedException e) {
            log.error("获取锁异常", e);
            Thread.currentThread().interrupt();
            return null;
        } finally {
            // 8. 释放锁(只有持有锁的线程才能释放)
            if (lock != null && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

3. 缓存雪崩

什么是缓存雪崩?

缓存雪崩指的是在某一时间段内,缓存中大量的 key 同时过期,或者缓存Redis宕机,导致所有请求都打到数据库,造成数据库压力骤增甚至宕机,引发系统雪崩。

解决方案

1)过期时间随机化:为不同 key 设置随机的过期时间,避免集中过期 2)服务熔断与降级:当缓存失效时,限制部分请求访问数据库 3)缓存集群:使用多个缓存节点,避免单点故障 4)多级缓存:结合本地缓存和分布式缓存,降低分布式缓存压力

过期时间随机化实现

java 复制代码
/**
 * 为缓存key设置随机过期时间,避免缓存雪崩
 */
public void setWithRandomExpire(String key, Object value, int baseExpireSeconds) {
    // 生成随机过期时间,在基础过期时间上增加0-300秒的随机值
    int randomSeconds = new Random().nextInt(300);
    int expireSeconds = baseExpireSeconds + randomSeconds;
    
    redissonClient.getBucket(key).set(value, expireSeconds, TimeUnit.SECONDS);
}

服务熔断与降级实现

java 复制代码
/**
 * 使用熔断降级机制解决缓存雪崩
 */
@Service
public class CacheAvalancheSolution {
    
    // 配置熔断器,当失败率超过50%时开启熔断,熔断时间为1分钟
    @CircuitBreaker(name = "databaseService", fallbackMethod = "databaseFallback")
    public Object getDataWithFallback(String key) {
        // 1. 先查询缓存
        Object value = redissonClient.getBucket(key).get();
        if (value != null) {
            return value;
        }
        
        // 2. 缓存未命中,查询数据库
        value = database.query(key);
        if (value != null) {
            // 3. 设置带随机过期时间的缓存
            setWithRandomExpire(key, value, 3600); // 基础过期时间1小时
        }
        return value;
    }
    
    // 降级方法,当熔断开启或发生异常时调用
    public Object databaseFallback(String key, Exception e) {
        log.warn("服务降级,key:{}", key, e);
        // 返回默认值或空,避免请求打到数据库
        return getDefaultValue(key);
    }
    
    // 设置带随机过期时间的缓存
    private void setWithRandomExpire(String key, Object value, int baseExpireSeconds) {
        int randomSeconds = new Random().nextInt(300);
        int expireSeconds = baseExpireSeconds + randomSeconds;
        redissonClient.getBucket(key).set(value, expireSeconds, TimeUnit.SECONDS);
    }
    
    // 获取默认值
    private Object getDefaultValue(String key) {
        // 根据不同的key返回不同的默认值
        return null;
    }
}
// 熔断器配置类
@Configuration
public class CircuitBreakerConfig {
    @Bean
    public io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry circuitBreakerRegistry() {
        CircuitBreakerConfig config = CircuitBreakerConfig.custom()
            .failureRateThreshold(50) // 失败率阈值50%
            .waitDurationInOpenState(Duration.ofMinutes(1)) // 熔断时间1分钟
            .permittedNumberOfCallsInHalfOpenState(10) // 半开状态允许的调用次数
            .slidingWindowSize(100) // 滑动窗口大小
            .build();
            
        return CircuitBreakerRegistry.of(config);
    }
}

总结

缓存穿透、击穿和雪崩是高并发系统中常见的缓存问题,它们都会对数据库造成压力,但产生原因和解决思路各有不同:

缓存穿透:针对不存在的数据,解决方案是空值缓存和布隆过滤器

缓存击穿:针对热点 key 过期,解决方案是互斥锁和热点数据永不过期

缓存雪崩:针对大量 key 同时过期或缓存宕机,解决方案是过期时间随机化、服务熔断降级和缓存集群

在实际项目中,往往需要结合多种方案来保障系统的稳定性和性能。同时,也要根据业务场景选择合适的方案,避免过度设计带来的复杂性。 在生产环境中,还需要结合监控系统,及时发现和解决缓存相关的问题。

附录------自测代码工程

项目结构

css 复制代码
src/
├── main/
│   └── java/
│       └── org/
│           └── example/
│               └── cache/
│                   ├── config/
│                   │   └── RedisConfig.java
│                   ├── db/
│                   │   └── MockDatabase.java
│                   ├── penetration/
│                   │   └── CachePenetrationSolution.java
│                   ├── breakdown/
│                   │   └── CacheBreakdownSolution.java
│                   └── avalanche/
│                       └── CacheAvalancheSolution.java
└── test/
    └── java/
        └── org/
            └── example/
                └── cache/
                    ├── penetration/
                    │   └── CachePenetrationTest.java
                    ├── breakdown/
                    │   └── CacheBreakdownTest.java
                    └── avalanche/
                        └── CacheAvalancheTest.java

配置

windows版本redis:

启动时需要运行redis-server.exe

pom.xml

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>Redisson</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <redisson.version>3.17.0</redisson.version>
        <resilience4j.version>1.7.1</resilience4j.version>
        <slf4j.version>1.7.36</slf4j.version>
        <logback.version>1.2.11</logback.version>
    </properties>

    <dependencies>
        <!-- Redisson -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>${redisson.version}</version>
        </dependency>

        <!-- Resilience4j (熔断降级) -->
        <dependency>
            <groupId>io.github.resilience4j</groupId>
            <artifactId>resilience4j-circuitbreaker</artifactId>
            <version>${resilience4j.version}</version>
        </dependency>

        <!-- 日志 -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>${logback.version}</version>
        </dependency>

        <!-- 测试 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

自测项目代码

RedisConfig.java

java 复制代码
package org.example.cache.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

public class RedisConfig {

    private static RedissonClient redissonClient;

    // 初始化Redisson客户端
    public static RedissonClient getRedissonClient() {
        if (redissonClient == null) {
            Config config = new Config();
            // 默认本地Redis配置,实际生产环境需要修改
            config.useSingleServer()
                    .setAddress("redis://127.0.0.1:6379")
                    .setDatabase(0);

            // 如果Redis有密码,添加下面这行
            // .setPassword("yourPassword");

            redissonClient = Redisson.create(config);
        }
        return redissonClient;
    }

    // 关闭Redisson客户端
    public static void shutdown() {
        if (redissonClient != null) {
            redissonClient.shutdown();
        }
    }
}

MockDatabase.java

java 复制代码
package org.example.cache.db;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 模拟数据库,包含一些预设数据
 */
public class MockDatabase {
    private static final Logger log = LoggerFactory.getLogger(MockDatabase.class);
    private static final Map<String, String> data = new HashMap<>();

    // 初始化一些测试数据
    static {
        data.put("user:1", "User 1 - John Doe");
        data.put("user:2", "User 2 - Jane Smith");
        data.put("user:3", "User 3 - Bob Johnson");
        data.put("product:100", "Product 100 - Laptop");
        data.put("product:101", "Product 101 - Smartphone");
    }

    /**
     * 模拟查询数据库,添加延迟来模拟真实数据库操作
     */
    public String query(String key) {
        // 模拟数据库查询延迟
        try {
            TimeUnit.MILLISECONDS.sleep(50); // 50ms延迟
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return null;
        }

        log.info("查询数据库,key: {}", key);
        return data.get(key);
    }

    /**
     * 加载所有存在的ID,用于布隆过滤器初始化
     */
    public Iterable<String> loadAllExistingIds() {
        return data.keySet();
    }

    /**
     * 模拟数据库压力测试,增加响应时间
     */
    public String stressQuery(String key) {
        // 模拟高负载下的数据库延迟
        try {
            TimeUnit.MILLISECONDS.sleep(200); // 更长的延迟
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return null;
        }

        log.warn("高负载下查询数据库,key: {}", key);
        return data.get(key);
    }
}

CachePenetrationSolution.java

java 复制代码
package org.example.cache.penetration;

import org.example.cache.config.RedisConfig;
import org.example.cache.db.MockDatabase;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.TimeUnit;

/**
 * 缓存穿透解决方案
 */
public class CachePenetrationSolution {
    private static final Logger log = LoggerFactory.getLogger(CachePenetrationSolution.class);
    private final RedissonClient redissonClient;
    private final MockDatabase database;
    private RBloomFilter<String> bloomFilter;
    
    // 空值标记
    private static final String NULL_VALUE = "NULL";
    
    public CachePenetrationSolution() {
        this.redissonClient = RedisConfig.getRedissonClient();
        this.database = new MockDatabase();
        initBloomFilter();
    }
    
    /**
     * 初始化布隆过滤器
     */
    private void initBloomFilter() {
        // 初始化布隆过滤器,预计元素1000个,误判率0.01
        bloomFilter = redissonClient.getBloomFilter("idBloomFilter");
        bloomFilter.tryInit(1000, 0.01);
        
        // 从数据库加载所有存在的ID到布隆过滤器
        database.loadAllExistingIds().forEach(id -> bloomFilter.add(id));
        log.info("布隆过滤器初始化完成,已添加 {} 个ID", database.loadAllExistingIds().spliterator().estimateSize());
    }
    
    /**
     * 使用空值缓存解决缓存穿透
     */
    public String solveWithNullValue(String key) {
        // 1. 先查询缓存
        RBucket<String> bucket = redissonClient.getBucket(key);
        String value = bucket.get();
        
        if (value != null) {
            // 2. 缓存存在,判断是否是空值
            if (NULL_VALUE.equals(value)) {
                log.info("缓存命中空值,key: {}", key);
                return null;
            }
            log.info("缓存命中,key: {}", key);
            return value;
        }
        
        // 3. 缓存不存在,查询数据库
        log.info("缓存未命中,查询数据库,key: {}", key);
        value = database.query(key);
        
        // 4. 无论结果是否存在,都存入缓存
        if (value == null) {
            // 空值设置较短的过期时间,如5分钟
            bucket.set(NULL_VALUE, 5, TimeUnit.MINUTES);
            log.info("查询结果为空,缓存空值,key: {}", key);
        } else {
            // 正常值设置较长的过期时间,如1小时
            bucket.set(value, 1, TimeUnit.HOURS);
            log.info("查询结果不为空,缓存正常值,key: {}", key);
        }
        
        return value;
    }
    
    /**
     * 使用布隆过滤器解决缓存穿透
     */
    public String solveWithBloomFilter(String key) {
        // 1. 先通过布隆过滤器判断ID是否存在
        if (!bloomFilter.contains(key)) {
            log.info("布隆过滤器判断ID不存在,直接返回空,key: {}", key);
            return null;
        }
        
        // 2. 布隆过滤器认为存在,再查询缓存
        RBucket<String> bucket = redissonClient.getBucket(key);
        String value = bucket.get();
        
        if (value != null) {
            log.info("缓存命中,key: {}", key);
            return value;
        }
        
        // 3. 缓存未命中,查询数据库
        log.info("缓存未命中,查询数据库,key: {}", key);
        value = database.query(key);
        
        if (value != null) {
            bucket.set(value, 1, TimeUnit.HOURS);
            log.info("查询结果不为空,缓存数据,key: {}", key);
        } else {
            // 对于布隆过滤器误判的情况,也缓存空值
            bucket.set(NULL_VALUE, 5, TimeUnit.MINUTES);
            log.info("布隆过滤器误判,实际数据不存在,缓存空值,key: {}", key);
        }
        
        return value;
    }
}
    

CachePenetrationTest.java

java 复制代码
package org.example.cache.penetration;

import org.example.cache.config.RedisConfig;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

/**
 * 缓存穿透测试
 */
public class CachePenetrationTest {
    private CachePenetrationSolution solution;

    @Before
    public void setUp() {
        solution = new CachePenetrationSolution();
        // 清理测试数据
        RedisConfig.getRedissonClient().getKeys().deleteByPattern("user:*");
        RedisConfig.getRedissonClient().getKeys().deleteByPattern("product:*");
    }

    @After
    public void tearDown() {
        RedisConfig.shutdown();
    }

    /**
     * 测试正常存在的key
     */
    @Test
    public void testExistingKey() {
        String key = "user:1";

        System.out.println("第一次查询: " + solution.solveWithNullValue(key));
        System.out.println("第二次查询: " + solution.solveWithNullValue(key));
    }

    /**
     * 测试空值缓存解决方案
     */
    @Test
    public void testNullValueSolution() {
        // 不存在的key
        String key = "user:9999";

        System.out.println("第一次查询: " + solution.solveWithNullValue(key));
        System.out.println("第二次查询: " + solution.solveWithNullValue(key));
        System.out.println("第三次查询: " + solution.solveWithNullValue(key));
    }

    /**
     * 测试布隆过滤器解决方案
     */
    @Test
    public void testBloomFilterSolution() {
        // 不存在的key
        String key = "user:9999";

        System.out.println("第一次查询: " + solution.solveWithBloomFilter(key));
        System.out.println("第二次查询: " + solution.solveWithBloomFilter(key));

        // 存在的key
        String existingKey = "product:100";
        System.out.println("查询存在的key: " + solution.solveWithBloomFilter(existingKey));
    }

    /**
     * 高并发测试缓存穿透
     */
    @Test
    public void testConcurrentPenetration() throws InterruptedException {
        String key = "user:9999";
        int threadCount = 50;

        Runnable task = () -> solution.solveWithNullValue(key);

        // 创建多个线程同时查询不存在的key
        Thread[] threads = new Thread[threadCount];
        for (int i = 0; i < threadCount; i++) {
            threads[i] = new Thread(task);
        }

        // 启动所有线程
        long startTime = System.currentTimeMillis();
        for (Thread thread : threads) {
            thread.start();
        }

        // 等待所有线程完成
        for (Thread thread : threads) {
            thread.join();
        }

        long endTime = System.currentTimeMillis();
        System.out.println("高并发测试完成,耗时: " + (endTime - startTime) + "ms");
    }
}

CacheBreakdownSolution.java

java 复制代码
package org.example.cache.breakdown;

import org.example.cache.config.RedisConfig;
import org.example.cache.db.MockDatabase;
import org.redisson.api.RBucket;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.TimeUnit;

/**
 * 缓存击穿解决方案
 */
public class CacheBreakdownSolution {
    private static final Logger log = LoggerFactory.getLogger(CacheBreakdownSolution.class);
    private final RedissonClient redissonClient;
    private final MockDatabase database;
    
    public CacheBreakdownSolution() {
        this.redissonClient = RedisConfig.getRedissonClient();
        this.database = new MockDatabase();
    }
    
    /**
     * 不使用任何防护措施的查询方法
     */
    public String queryWithoutProtection(String key) {
        // 1. 先查询缓存
        RBucket<String> bucket = redissonClient.getBucket(key);
        String value = bucket.get();
        
        if (value != null) {
            log.info("缓存命中,key: {}", key);
            return value;
        }
        
        // 2. 缓存未命中,查询数据库
        log.info("缓存未命中,查询数据库,key: {}", key);
        value = database.query(key);
        
        if (value != null) {
            // 设置较短的过期时间,方便测试
            bucket.set(value, 5, TimeUnit.SECONDS);
            log.info("缓存数据,key: {}", key);
        }
        
        return value;
    }
    
    /**
     * 使用分布式锁解决缓存击穿
     */
    public String solveWithLock(String key) {
        // 1. 先查询缓存
        RBucket<String> bucket = redissonClient.getBucket(key);
        String value = bucket.get();
        
        if (value != null) {
            log.info("缓存命中,key: {}", key);
            return value;
        }
        
        RLock lock = null;
        try {
            // 2. 获取分布式锁
            lock = redissonClient.getLock(key + ":lock");
            
            // 3. 尝试获取锁,最多等待100ms,10秒后自动释放
            boolean isLocked = lock.tryLock(100, 10, TimeUnit.MILLISECONDS);
            
            if (isLocked) {
                log.info("获取锁成功,key: {}", key);
                
                // 4. 再次检查缓存,防止重复查询数据库
                value = bucket.get();
                if (value != null) {
                    log.info("二次检查缓存命中,key: {}", key);
                    return value;
                }
                
                // 5. 查询数据库
                log.info("查询数据库,key: {}", key);
                value = database.query(key);
                
                if (value != null) {
                    // 设置较短的过期时间,方便测试
                    bucket.set(value, 5, TimeUnit.SECONDS);
                    log.info("缓存数据,key: {}", key);
                }
                
                return value;
            } else {
                // 6. 未获取到锁,等待一段时间后重试
                log.info("获取锁失败,等待后重试,key: {}", key);
                TimeUnit.MILLISECONDS.sleep(50);
                return solveWithLock(key); // 递归重试
            }
        } catch (InterruptedException e) {
            log.error("获取锁异常", e);
            Thread.currentThread().interrupt();
            return null;
        } finally {
            // 7. 释放锁(只有持有锁的线程才能释放)
            if (lock != null && lock.isHeldByCurrentThread()) {
                lock.unlock();
                log.info("释放锁,key: {}", key);
            }
        }
    }
    
    /**
     * 清除指定key的缓存,用于测试
     */
    public void clearCache(String key) {
        redissonClient.getBucket(key).delete();
        log.info("清除缓存,key: {}", key);
    }
}
    

CacheBreakdownTest.java

java 复制代码
package org.example.cache.breakdown;

import org.example.cache.config.RedisConfig;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

/**
 * 缓存击穿测试
 */
public class CacheBreakdownTest {
    private CacheBreakdownSolution solution;
    private final String hotKey = "product:100"; // 热点key
    
    @Before
    public void setUp() {
        solution = new CacheBreakdownSolution();
        // 清除测试数据
        RedisConfig.getRedissonClient().getKeys().delete(hotKey);
        RedisConfig.getRedissonClient().getKeys().delete(hotKey + ":lock");
    }
    
    @After
    public void tearDown() {
        RedisConfig.shutdown();
    }
    
    /**
     * 测试没有防护措施时的缓存击穿
     */
    @Test
    public void testWithoutProtection() throws InterruptedException {
        // 先将热点数据加载到缓存
        solution.queryWithoutProtection(hotKey);
        
        // 清除缓存,模拟缓存过期
        solution.clearCache(hotKey);
        
        // 模拟大量并发请求
        int threadCount = 30;
        Runnable task = () -> solution.queryWithoutProtection(hotKey);
        
        Thread[] threads = new Thread[threadCount];
        for (int i = 0; i < threadCount; i++) {
            threads[i] = new Thread(task);
        }
        
        long startTime = System.currentTimeMillis();
        for (Thread thread : threads) {
            thread.start();
        }
        
        for (Thread thread : threads) {
            thread.join();
        }
        
        long endTime = System.currentTimeMillis();
        System.out.println("无防护措施测试完成,耗时: " + (endTime - startTime) + "ms");
    }
    
    /**
     * 测试使用分布式锁解决缓存击穿
     */
    @Test
    public void testWithLockSolution() throws InterruptedException {
        // 先将热点数据加载到缓存
        solution.solveWithLock(hotKey);
        
        // 清除缓存,模拟缓存过期
        solution.clearCache(hotKey);
        
        // 模拟大量并发请求
        int threadCount = 30;
        Runnable task = () -> solution.solveWithLock(hotKey);
        
        Thread[] threads = new Thread[threadCount];
        for (int i = 0; i < threadCount; i++) {
            threads[i] = new Thread(task);
        }
        
        long startTime = System.currentTimeMillis();
        for (Thread thread : threads) {
            thread.start();
        }
        
        for (Thread thread : threads) {
            thread.join();
        }
        
        long endTime = System.currentTimeMillis();
        System.out.println("分布式锁防护测试完成,耗时: " + (endTime - startTime) + "ms");
    }
    
    /**
     * 对比测试
     */
    @Test
    public void testComparison() throws InterruptedException {
        System.out.println("===== 无防护措施测试 =====");
        testWithoutProtection();
        
        // 等待缓存过期
        Thread.sleep(1000);
        System.out.println("\n===== 分布式锁防护测试 =====");
        testWithLockSolution();
    }
}
    

CacheAvalancheSolution.java

java 复制代码
package org.example.cache.avalanche;

import org.example.cache.config.RedisConfig;
import org.example.cache.db.MockDatabase;
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.Duration;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
 * 缓存雪崩解决方案
 */
public class CacheAvalancheSolution {
    private static final Logger log = LoggerFactory.getLogger(CacheAvalancheSolution.class);
    private final RedissonClient redissonClient;
    private final MockDatabase database;
    private final CircuitBreaker circuitBreaker;
    private final Random random = new Random();
    
    public CacheAvalancheSolution() {
        this.redissonClient = RedisConfig.getRedissonClient();
        this.database = new MockDatabase();
        
        // 初始化熔断器
        CircuitBreakerConfig config = CircuitBreakerConfig.custom()
                .failureRateThreshold(50) // 失败率阈值50%
                .waitDurationInOpenState(Duration.ofSeconds(30)) // 熔断时间30秒
                .permittedNumberOfCallsInHalfOpenState(5) // 半开状态允许的调用次数
                .slidingWindowSize(10) // 滑动窗口大小
                .build();
        
        CircuitBreakerRegistry registry = CircuitBreakerRegistry.of(config);
        this.circuitBreaker = registry.circuitBreaker("databaseService");
    }
    
    /**
     * 不使用任何防护措施的查询方法,所有key设置相同过期时间
     */
    public String queryWithoutProtection(String key) {
        // 1. 先查询缓存
        RBucket<String> bucket = redissonClient.getBucket(key);
        String value = bucket.get();
        
        if (value != null) {
            log.info("缓存命中,key: {}", key);
            return value;
        }
        
        // 2. 缓存未命中,查询数据库
        log.info("缓存未命中,查询数据库,key: {}", key);
        value = database.stressQuery(key);
        
        if (value != null) {
            // 所有key设置相同的过期时间,容易造成缓存雪崩
            bucket.set(value, 10, TimeUnit.SECONDS);
            log.info("缓存数据,key: {}", key);
        }
        
        return value;
    }
    
    /**
     * 使用随机过期时间防止缓存雪崩
     */
    public String queryWithRandomExpire(String key) {
        // 1. 先查询缓存
        RBucket<String> bucket = redissonClient.getBucket(key);
        String value = bucket.get();
        
        if (value != null) {
            log.info("缓存命中,key: {}", key);
            return value;
        }
        
        // 2. 缓存未命中,查询数据库
        log.info("缓存未命中,查询数据库,key: {}", key);
        value = database.stressQuery(key);
        
        if (value != null) {
            // 设置带随机值的过期时间,基础时间10秒,随机增加0-5秒
            int randomSeconds = random.nextInt(5);
            int expireSeconds = 10 + randomSeconds;
            bucket.set(value, expireSeconds, TimeUnit.SECONDS);
            log.info("缓存数据,key: {}, 过期时间: {}秒", key, expireSeconds);
        }
        
        return value;
    }
    
    /**
     * 使用熔断器防止缓存雪崩
     */
    public String queryWithCircuitBreaker(String key) {
        // 使用熔断器包装数据库查询操作
        return circuitBreaker.decorateSupplier(() -> {
            // 1. 先查询缓存
            RBucket<String> bucket = redissonClient.getBucket(key);
            String value = bucket.get();
            
            if (value != null) {
                log.info("缓存命中,key: {}", key);
                return value;
            }
            
            // 2. 缓存未命中,查询数据库
            log.info("缓存未命中,查询数据库,key: {}", key);
            value = database.stressQuery(key);
            
            if (value != null) {
                // 设置带随机值的过期时间
                int randomSeconds = random.nextInt(5);
                int expireSeconds = 10 + randomSeconds;
                bucket.set(value, expireSeconds, TimeUnit.SECONDS);
                log.info("缓存数据,key: {}, 过期时间: {}秒", key, expireSeconds);
            }
            
            return value;
        }).get();
    }
    
    /**
     * 熔断器降级方法
     */
    public String fallback(String key, Exception e) {
        log.warn("服务降级,key: {}", key, e);
        // 返回默认值
        return "降级数据: " + key;
    }
    
    /**
     * 清除所有缓存,模拟缓存雪崩场景
     */
    public void clearAllCache() {
        redissonClient.getKeys().deleteByPattern("user:*");
        redissonClient.getKeys().deleteByPattern("product:*");
        log.info("清除所有缓存");
    }
}
    

CacheAvalancheTest.java

java 复制代码
package org.example.cache.avalanche;

import org.example.cache.config.RedisConfig;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

/**
 * 缓存雪崩测试
 */
public class CacheAvalancheTest {
    private CacheAvalancheSolution solution;
    private final String[] keys = {
        "user:1", "user:2", "user:3",
        "product:100", "product:101"
    };
    
    @Before
    public void setUp() {
        solution = new CacheAvalancheSolution();
        // 清除所有缓存
        solution.clearAllCache();
    }
    
    @After
    public void tearDown() {
        RedisConfig.shutdown();
    }
    
    /**
     * 测试没有防护措施时的缓存雪崩
     */
    @Test
    public void testWithoutProtection() throws InterruptedException {
        // 先将数据加载到缓存
        for (String key : keys) {
            solution.queryWithoutProtection(key);
        }
        
        // 清除所有缓存,模拟缓存同时失效
        solution.clearAllCache();
        
        // 模拟大量并发请求
        int threadCount = 50;
        Runnable task = () -> {
            // 随机访问不同的key
            String key = keys[(int)(Math.random() * keys.length)];
            solution.queryWithoutProtection(key);
        };
        
        Thread[] threads = new Thread[threadCount];
        for (int i = 0; i < threadCount; i++) {
            threads[i] = new Thread(task);
        }
        
        long startTime = System.currentTimeMillis();
        for (Thread thread : threads) {
            thread.start();
        }
        
        for (Thread thread : threads) {
            thread.join();
        }
        
        long endTime = System.currentTimeMillis();
        System.out.println("无防护措施测试完成,耗时: " + (endTime - startTime) + "ms");
    }
    
    /**
     * 测试使用随机过期时间防止缓存雪崩
     */
    @Test
    public void testWithRandomExpire() throws InterruptedException {
        // 先将数据加载到缓存
        for (String key : keys) {
            solution.queryWithRandomExpire(key);
        }
        
        // 清除所有缓存,模拟缓存同时失效
        solution.clearAllCache();
        
        // 模拟大量并发请求
        int threadCount = 50;
        Runnable task = () -> {
            String key = keys[(int)(Math.random() * keys.length)];
            solution.queryWithRandomExpire(key);
        };
        
        Thread[] threads = new Thread[threadCount];
        for (int i = 0; i < threadCount; i++) {
            threads[i] = new Thread(task);
        }
        
        long startTime = System.currentTimeMillis();
        for (Thread thread : threads) {
            thread.start();
        }
        
        for (Thread thread : threads) {
            thread.join();
        }
        
        long endTime = System.currentTimeMillis();
        System.out.println("随机过期时间测试完成,耗时: " + (endTime - startTime) + "ms");
    }
    
    /**
     * 测试使用熔断器防止缓存雪崩
     */
    @Test
    public void testWithCircuitBreaker() throws InterruptedException {
        // 先将数据加载到缓存
        for (String key : keys) {
            solution.queryWithCircuitBreaker(key);
        }
        
        // 清除所有缓存,模拟缓存同时失效
        solution.clearAllCache();
        
        // 模拟大量并发请求
        int threadCount = 50;
        Runnable task = () -> {
            String key = keys[(int)(Math.random() * keys.length)];
            solution.queryWithCircuitBreaker(key);
        };
        
        Thread[] threads = new Thread[threadCount];
        for (int i = 0; i < threadCount; i++) {
            threads[i] = new Thread(task);
        }
        
        long startTime = System.currentTimeMillis();
        for (Thread thread : threads) {
            thread.start();
        }
        
        for (Thread thread : threads) {
            thread.join();
        }
        
        long endTime = System.currentTimeMillis();
        System.out.println("熔断器测试完成,耗时: " + (endTime - startTime) + "ms");
    }
    
    /**
     * 对比测试
     */
    @Test
    public void testComparison() throws InterruptedException {
        System.out.println("===== 无防护措施测试 =====");
        testWithoutProtection();
        
        // 等待系统恢复
        Thread.sleep(2000);
        System.out.println("\n===== 随机过期时间测试 =====");
        testWithRandomExpire();
        
        // 等待系统恢复
        Thread.sleep(2000);
        System.out.println("\n===== 熔断器测试 =====");
        testWithCircuitBreaker();
    }
}
    
相关推荐
9号达人3 小时前
泛型+函数式:让策略模式不再是复制粘贴地狱
java·后端·面试
captain3763 小时前
Java线性表
java·开发语言
tuokuac3 小时前
Java String类中的lastIndexOf方法的应用场景
java·开发语言
柑木3 小时前
开发必备-使用DevContainer技术消除 “在我这能运行”
后端
怪兽20143 小时前
Looper、MessageQueue、Message及Handler的关系是什么?如何保证MessageQueue的并发访问安全?
android·面试
武子康3 小时前
大数据-122 - Flink Watermark 全面解析:事件时间窗口、乱序处理与迟到数据完整指南
大数据·后端·flink
weixin_379880923 小时前
.Net Core WebApi集成Swagger
java·服务器·.netcore
@逆风微笑代码狗3 小时前
147.《手写实现 Promise.all 与 Promise.race》
java
她说彩礼65万3 小时前
Asp.net core Kestrel服务器详解
服务器·后端·asp.net