8.4 缓存设计模式
面试重要程度:⭐⭐⭐⭐⭐
常见提问方式: 缓存穿透击穿雪崩、分布式锁、热点数据处理
预计阅读时间:45分钟
🎯 缓存穿透、击穿、雪崩解决方案
缓存穿透
面试官: "什么是缓存穿透?如何解决?"
问题定义:
/**
* 缓存穿透:查询不存在的数据
* 现象:缓存和数据库都没有数据,但请求持续打到数据库
* 危害:数据库压力过大,可能导致系统崩溃
*/
public class CachePenetration {
public User getUserById(Long userId) {
// 1. 查询缓存
User user = redisTemplate.opsForValue().get("user:" + userId);
if (user != null) {
return user;
}
// 2. 缓存未命中,查询数据库
user = userMapper.selectById(userId);
if (user != null) {
// 3. 存入缓存
redisTemplate.opsForValue().set("user:" + userId, user, 30, TimeUnit.MINUTES);
}
// 4. 问题:恶意请求不存在的userId会一直打到数据库
return user;
}
}
解决方案1:缓存空值
@Service
public class CacheNullService {
public User getUserById(Long userId) {
String key = "user:" + userId;
// 1. 查询缓存
Object cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
// 如果是空值标记,返回null
if ("NULL".equals(cached)) {
return null;
}
return (User) cached;
}
// 2. 查询数据库
User user = userMapper.selectById(userId);
if (user != null) {
redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
} else {
// 缓存空值,设置较短过期时间
redisTemplate.opsForValue().set(key, "NULL", 5, TimeUnit.MINUTES);
}
return user;
}
}
解决方案2:布隆过滤器
@Configuration
public class BloomFilterConfig {
@Bean
public BloomFilter<Long> userBloomFilter() {
// 预期插入100万个元素,误判率0.01%
return BloomFilter.create(Funnels.longFunnel(), 1000000, 0.0001);
}
}
@Service
public class BloomFilterService {
@Autowired
private BloomFilter<Long> userBloomFilter;
public User getUserById(Long userId) {
// 1. 布隆过滤器判断
if (!userBloomFilter.mightContain(userId)) {
// 一定不存在,直接返回
return null;
}
// 2. 可能存在,继续查询缓存和数据库
String key = "user:" + userId;
Object cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
return "NULL".equals(cached) ? null : (User) cached;
}
User user = userMapper.selectById(userId);
if (user != null) {
redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
} else {
redisTemplate.opsForValue().set(key, "NULL", 5, TimeUnit.MINUTES);
}
return user;
}
}
缓存击穿
面试官: "缓存击穿和缓存穿透有什么区别?如何解决?"
解决方案1:互斥锁
@Service
public class MutexLockService {
public Product getHotProduct(Long productId) {
String key = "product:" + productId;
String lockKey = "lock:product:" + productId;
// 1. 查询缓存
Product product = (Product) redisTemplate.opsForValue().get(key);
if (product != null) {
return product;
}
// 2. 尝试获取锁
Boolean lockAcquired = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (lockAcquired) {
try {
// 3. 双重检查
product = (Product) redisTemplate.opsForValue().get(key);
if (product != null) {
return product;
}
// 4. 查询数据库
product = productMapper.selectById(productId);
if (product != null) {
redisTemplate.opsForValue().set(key, product, 30, TimeUnit.MINUTES);
}
return product;
} finally {
redisTemplate.delete(lockKey);
}
} else {
// 获取锁失败,等待后重试
try {
Thread.sleep(100);
return getHotProduct(productId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}
}
}
解决方案2:逻辑过期
@Data
public class CacheData<T> {
private T data;
private LocalDateTime expireTime;
}
@Service
public class LogicalExpireService {
private final ExecutorService executor = Executors.newFixedThreadPool(10);
public Product getHotProduct(Long productId) {
String key = "product:" + productId;
// 1. 查询缓存
CacheData<Product> cacheData = (CacheData<Product>) redisTemplate.opsForValue().get(key);
if (cacheData == null) {
return queryAndCache(productId);
}
// 2. 检查逻辑过期时间
if (cacheData.getExpireTime().isAfter(LocalDateTime.now())) {
// 未过期,直接返回
return cacheData.getData();
}
// 3. 已过期,尝试获取锁
String lockKey = "lock:product:" + productId;
Boolean lockAcquired = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (lockAcquired) {
// 4. 异步更新缓存
executor.submit(() -> {
try {
Product product = productMapper.selectById(productId);
if (product !=
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
Java面试圣经 文章被收录于专栏
Java面试圣经,带你练透java圣经

