1. 缓存穿透
定义:缓存穿透是指查询一个不存在的数据,缓存中没有,数据库中也没有,导致每次请求都会直接打到数据库上。
解决方案:
- 布隆过滤器:使用布隆过滤器来判断数据是否存在,如果布隆过滤器返回不存在,则直接返回,不再查询数据库。
- 缓存空值:将不存在的数据也缓存一段时间,避免频繁查询数据库。
使用布隆过滤器和缓存空值的结合方案:
import com.google.common.hash.Funnel;
import com.google.common.hash.Hashing;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
@Service
public class CachePenetrationService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedissonClient redissonClient;
private final RBloomFilter<String> bloomFilter;
public CachePenetrationService() {
// 初始化布隆过滤器
this.bloomFilter = redissonClient.getBloomFilter("bloomFilter");
this.bloomFilter.tryInit(1000000, 0.03); // 预期插入 100 万个元素,误判率为 3%
}
/**
* 获取数据,处理缓存穿透
*
* @param key 键
* @return 数据
*/
public String getData(String key) {
// 使用布隆过滤器检查数据是否存在
if (!bloomFilter.contains(key)) {
return "数据不存在";
}
// 存在则从缓存中获取数据
String data = stringRedisTemplate.opsForValue().get(key);
if (data != null) {
return data;
}
// 加锁,确保只有一个请求去查询数据库
String lockKey = "lock:" + key;
while (stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS)) {
try {
// 从数据库中获取数据
data = fetchDataFromDatabase(key);
// 如果数据不存在,缓存空值
if (data == null) {
stringRedisTemplate.opsForValue().set(key, "", 60, TimeUnit.SECONDS);
return "Data does not exist";
}
// 将数据写回缓存
stringRedisTemplate.opsForValue().set(key, data, 60 * 60, TimeUnit.SECONDS);
return data;
} finally {
stringRedisTemplate.delete(lockKey);
}
}
// 等待其他请求完成并获取结果
return stringRedisTemplate.opsForValue().get(key);
}
/**
* 从数据库中获取数据
*
* @param key 键
* @return 数据
*/
private String fetchDataFromDatabase(String key) {
// 模拟从数据库中获取数据
// 实际应用中应替换为真实的数据库查询逻辑
return "Data from database";
}
}
2. 缓存击穿
定义:缓存击穿是指某个热点数据在缓存中过期的一瞬间,大量请求同时打到数据库上,造成数据库压力骤增。
解决方案:
- 加锁:在缓存失效后,通过加锁机制确保只有一个请求去查询数据库,并将结果写回缓存。
- 设置随机过期时间:为缓存设置一个随机的过期时间,避免大量缓存在同一时间点过期。
使用加锁和随机过期时间的结合方案:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class CacheBreakdownService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 获取数据,处理缓存击穿
*
* @param key 键
* @return 数据
*/
public String getData(String key) {
// 从缓存中获取数据
String data = stringRedisTemplate.opsForValue().get(key);
if (data != null) {
return data;
}
// 加锁,确保只有一个请求去查询数据库
String lockKey = "lock:" + key;
while (stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS)) {
try {
// 从数据库中获取数据
data = fetchDataFromDatabase(key);
// 将数据写回缓存,并设置随机过期时间
int randomExpireTime = 60 * 60 + (int) (Math.random() * 3600); // 1 小时到 2 小时之间
stringRedisTemplate.opsForValue().set(key, data, randomExpireTime, TimeUnit.SECONDS);
return data;
} finally {
stringRedisTemplate.delete(lockKey);
}
}
// 等待其他请求完成并获取结果
return stringRedisTemplate.opsForValue().get(key);
}
/**
* 从数据库中获取数据
*
* @param key 键
* @return 数据
*/
private String fetchDataFromDatabase(String key) {
// 模拟从数据库中获取数据
// 实际应用中应替换为真实的数据库查询逻辑
return "Data from database";
}
}
3. 缓存雪崩
定义:缓存雪崩是指大量缓存在同一时间点集体失效,导致大量请求直接打到数据库上,造成数据库压力剧增。
解决方案:
- 设置随机过期时间:为缓存设置一个随机的过期时间,避免大量缓存在同一时间点过期。
- 限流:在请求到达时进行限流,避免过多请求同时打到数据库上。
- 降级:在缓存和数据库都不可用时,返回默认值或降级处理。
使用随机过期时间和限流的结合方案:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class CacheAvalancheService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 获取数据,处理缓存雪崩
*
* @param key 键
* @return 数据
*/
public String getData(String key) {
// 从缓存中获取数据
String data = stringRedisTemplate.opsForValue().get(key);
if (data != null) {
return data;
}
// 限流,确保每秒不超过 100 个请求
if (isRateLimited()) {
return "请求过多,请稍后再试";
}
// 从数据库中获取数据
data = fetchDataFromDatabase(key);
// 将数据写回缓存,并设置随机过期时间
int randomExpireTime = 60 * 60 + (int) (Math.random() * 3600); // 1 小时到 2 小时之间
stringRedisTemplate.opsForValue().set(key, data, randomExpireTime, TimeUnit.SECONDS);
return data;
}
/**
* 从数据库中获取数据
*
* @param key 键
* @return 数据
*/
private String fetchDataFromDatabase(String key) {
// 模拟从数据库中获取数据
// 实际应用中应替换为真实的数据库查询逻辑
return "Data from database";
}
/**
* 限流逻辑
*
* @return 是否超过限制
*/
private boolean isRateLimited() {
// 模拟限流逻辑,实际应用中可以使用 Redis 的限流功能或其他限流组件
// 例如:使用 Redis 的 INCR 和 EXPIRE 命令实现每秒 100 个请求的限流
return false;
}
}