
在高并发系统中,缓存是提升性能的关键组件。然而,缓存的使用也伴随着一些潜在问题,其中最常见的就是缓存穿透、缓存击穿和缓存雪崩。本文将详细解析这三个问题,并结合 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();
}
}