引言
在高并发系统中,缓存是提升系统性能、降低数据库压力的关键组件。然而,如果使用不当,缓存也会带来一系列问题:缓存穿透 、缓存击穿 、缓存雪崩。今天我们来深度解析一个专门为解决这些问题而生的 Redis 工具类。
一、工具类概述
专门为高并发场景设计的缓存工具类,提供了多种缓存问题的解决方案:
-
🔒 缓存击穿:通过分布式锁机制防止热点 key 失效时大量请求直达数据库
-
🕳️ 缓存穿透:通过空值缓存防止恶意查询不存在的 key
-
❄️ 缓存雪崩:通过随机过期时间避免大量 key 同时失效
-
🔄 多级缓存:支持本地缓存+Redis 缓存的多级缓存架构
-
🔥 缓存预热:支持缓存预热机制
工具完整代码:
java
package com.xxx.frame.common.redis.utils;
import com.xxx.frame.common.core.exception.ServiceException;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.time.Duration;
import java.util.Random;
import java.util.function.Supplier;
/**
* <p>
* 高并发场景下的缓存工具类
* 提供缓存穿透、缓存击穿、缓存雪崩等问题的解决方案
* </p>
*
* @author MC.Yang
* @version V1.0
**/
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ConcurrentCacheUtils {
/**
* 默认最大重试次数
*/
private static final int DEFAULT_MAX_RETRIES = 3;
/**
* 默认锁过期时间(秒)
*/
private static final int DEFAULT_LOCK_EXPIRE = 10;
/**
* 默认重试间隔(毫秒)
*/
private static final int DEFAULT_RETRY_INTERVAL = 100;
/**
* 通用缓存处理方法,用于处理高并发场景下的缓存操作
*
* @param cacheKey 缓存键
* @param loader 数据加载器函数
* @param expireTime 过期时间(秒)
* @param <T> 返回值类型
* @return 缓存或加载的数据
*/
public static <T> T getFromCacheWithLock(String cacheKey, Supplier<T> loader, int expireTime) {
return getFromCacheWithLock(cacheKey, loader, expireTime, DEFAULT_MAX_RETRIES);
}
/**
* 通用缓存处理方法,用于处理高并发场景下的缓存操作
*
* @param cacheKey 缓存键
* @param loader 数据加载器函数
* @param expireTime 过期时间(秒)
* @param maxRetries 最大重试次数
* @param <T> 返回值类型
* @return 缓存或加载的数据
*/
public static <T> T getFromCacheWithLock(String cacheKey, Supplier<T> loader, int expireTime, int maxRetries) {
// 先尝试从缓存获取
T result = RedisUtils.getCacheObject(cacheKey);
if (result != null) {
return result;
}
// 使用循环替代递归,避免死循环和栈溢出
int retryCount = 0;
String lockKey = cacheKey + ":lock";
while (retryCount < maxRetries) {
try {
// 获取锁
if (RedisUtils.setObjectIfAbsent(lockKey, "1", Duration.ofSeconds(DEFAULT_LOCK_EXPIRE))) {
try {
// 双重检查,防止重复查询
result = RedisUtils.getCacheObject(cacheKey);
if (result != null) {
return result;
}
// 执行数据加载
result = loader.get();
// 设置随机过期时间,防止缓存雪崩
int randomExpire = expireTime + new Random().nextInt(expireTime / 2);
RedisUtils.setCacheObject(cacheKey, result);
RedisUtils.expire(cacheKey, Duration.ofSeconds(randomExpire));
return result;
} finally {
RedisUtils.deleteObject(lockKey);
}
} else {
// 获取锁失败,短暂等待后重试
Thread.sleep(DEFAULT_RETRY_INTERVAL);
retryCount++;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ServiceException("获取缓存数据被中断");
} catch (Exception e) {
log.error("缓存加载失败", e);
throw new ServiceException("缓存加载失败: " + e.getMessage());
}
}
// 重试次数用完仍然没有获取到锁
throw new ServiceException("获取缓存数据超时");
}
/**
* 带有空值缓存防止缓存穿透的缓存处理方法
*
* @param cacheKey 缓存键
* @param loader 数据加载器函数
* @param expireTime 过期时间(秒)
* @param emptyExpireTime 空值过期时间(秒)
* @param <T> 返回值类型
* @return 缓存或加载的数据
*/
public static <T> T getFromCacheWithPenetrationProtection(String cacheKey, Supplier<T> loader,
int expireTime, int emptyExpireTime) {
// 先尝试从缓存获取
Object result = RedisUtils.getCacheObject(cacheKey);
if (result != null) {
// 判断是否是空值标记
if ("<NULL>".equals(result)) {
return null;
}
@SuppressWarnings("unchecked")
T typedResult = (T) result;
return typedResult;
}
return getFromCacheWithLock(cacheKey, () -> {
T data = loader.get();
if (data == null) {
// 缓存空值,防止缓存穿透
RedisUtils.setCacheObject(cacheKey, "<NULL>");
RedisUtils.expire(cacheKey, Duration.ofSeconds(emptyExpireTime));
}
return data;
}, expireTime);
}
/**
* 多级缓存处理方法(本地缓存+Redis缓存)
*
* @param cacheKey 缓存键
* @param loader 数据加载器函数
* @param redisExpireTime Redis缓存过期时间(秒)
* @param localExpireTime 本地缓存过期时间(秒)
* @param <T> 返回值类型
* @return 缓存或加载的数据
*/
public static <T> T getFromMultiLevelCache(String cacheKey, Supplier<T> loader,
int redisExpireTime, int localExpireTime) {
// 这里可以集成本地缓存(如Caffeine)
// 由于当前系统上下文未提供本地缓存实现,暂时只处理Redis缓存
return getFromCacheWithLock(cacheKey, loader, redisExpireTime);
}
/**
* 带预热机制的缓存更新方法
*
* @param cacheKey 缓存键
* @param loader 数据加载器函数
* @param expireTime 过期时间(秒)
* @param <T> 返回值类型
* @return 缓存或加载的数据
*/
public static <T> T getFromCacheWithWarmUp(String cacheKey, Supplier<T> loader, int expireTime) {
T result = RedisUtils.getCacheObject(cacheKey);
if (result != null) {
return result;
}
return getFromCacheWithLock(cacheKey, () -> {
T data = loader.get();
// 设置较短的过期时间,促使定期更新
int shortExpire = Math.max(expireTime / 2, 60); // 最少1分钟
int randomExpire = shortExpire + new Random().nextInt(shortExpire / 2);
RedisUtils.setCacheObject(cacheKey, data);
RedisUtils.expire(cacheKey, Duration.ofSeconds(randomExpire));
return data;
}, expireTime);
}
/**
* 批量缓存处理方法
*
* @param cacheKeys 缓存键列表
* @param loader 批量数据加载器函数
* @param expireTime 过期时间(秒)
* @param <T> 返回值类型
* @return 缓存或加载的数据
*/
public static <T> T getBatchFromCacheWithLock(String[] cacheKeys, Supplier<T> loader, int expireTime) {
// 构建复合缓存键
String compositeKey = String.join(":", cacheKeys);
return getFromCacheWithLock(compositeKey, loader, expireTime);
}
}
二、核心方法详解
1. 基础缓存获取(带分布式锁)
java
// 使用示例
User user = ConcurrentCacheUtils.getFromCacheWithLock(
"user:123",
() -> userService.getUserById(123),
300 // 5分钟过期
);
实现原理:
-
双重检查锁定:先查缓存,未命中再尝试加锁
-
分布式锁 :使用 Redis 的
setIfAbsent实现分布式锁 -
随机过期时间:防止缓存雪崩
-
重试机制:获取锁失败时自动重试
2. 防缓存穿透版本
java
// 使用示例
User user = ConcurrentCacheUtils.getFromCacheWithPenetrationProtection(
"user:999",
() -> userService.getUserById(999),
300, // 正常数据过期时间:5分钟
60 // 空值过期时间:1分钟
);
特色功能:
-
空值标记 :对查询结果为 null 的情况,缓存特殊标记
<NULL> -
差异化过期:空值使用较短的过期时间,既防穿透又保证数据最终一致性
3. 多级缓存支持
java
// 使用示例(当前版本主要处理Redis层)
Product product = ConcurrentCacheUtils.getFromMultiLevelCache(
"product:456",
() -> productService.getProductById(456),
1800, // Redis缓存30分钟
300 // 本地缓存5分钟(预留扩展)
);
设计思路:
为未来集成 Caffeine 等本地缓存框架预留了扩展接口。
4. 缓存预热机制
java
// 使用示例 - 适合热点数据
HotNews hotNews = ConcurrentCacheUtils.getFromCacheWithWarmUp(
"hotnews:daily",
() -> newsService.getDailyHotNews(),
7200 // 2小时基础过期时间
);
预热策略:
-
设置较短的基础过期时间
-
通过定期访问触发数据更新
-
避免冷启动问题
三、技术亮点解析
1. 健壮的重试机制
java
// 避免递归可能导致的栈溢出
while (retryCount < maxRetries) {
// 使用循环替代递归,更安全
}
2. 完善的异常处理
java
try {
// 业务逻辑
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 保持中断状态
throw new ServiceException("获取缓存数据被中断");
} catch (Exception e) {
log.error("缓存加载失败", e); // 详细日志记录
throw new ServiceException("缓存加载失败: " + e.getMessage());
}
3. 线程安全设计
-
使用
private构造器防止实例化 -
所有方法都是静态方法
-
无状态设计,线程安全
四、实战应用场景
场景1:电商商品详情页
java
public Product getProductDetail(Long productId) {
String cacheKey = "product:detail:" + productId;
return ConcurrentCacheUtils.getFromCacheWithPenetrationProtection(
cacheKey,
() -> {
// 复杂的业务查询逻辑
Product product = productMapper.selectById(productId);
if (product != null) {
product.setImages(imageService.getProductImages(productId));
product.setSkus(skuService.getProductSkus(productId));
}
return product;
},
1800, // 商品信息缓存30分钟
300 // 空值缓存5分钟
);
}
场景2:秒杀活动信息
java
public SeckillInfo getSeckillInfo(Long activityId) {
String cacheKey = "seckill:info:" + activityId;
return ConcurrentCacheUtils.getFromCacheWithLock(
cacheKey,
() -> seckillService.getSeckillInfo(activityId),
60, // 秒级数据,短时间缓存
5 // 最多重试5次
);
}
五、性能优化建议
1. 参数调优
java
// 根据业务特点调整参数
public static final int DEFAULT_MAX_RETRIES = 5; // 高并发场景增加重试次数
public static final int DEFAULT_RETRY_INTERVAL = 50; // 减少重试间隔
public static final int DEFAULT_LOCK_EXPIRE = 5; // 缩短锁过期时间
2. 监控指标
建议监控以下指标:
-
缓存命中率
-
锁竞争频率
-
重试次数统计
-
空值缓存比例
六、注意事项
-
序列化要求:缓存的对象必须实现 Serializable 接口
-
键名规范:保证缓存键的唯一性和可读性
-
内存控制:注意大对象缓存可能的内存问题
-
一致性考虑:在数据更新时要及时清理或更新缓存
七、总结
工具类为高并发场景下的缓存使用提供了一套完整的解决方案,具有以下优势:
-
✅ 开箱即用:简单的方法调用即可获得完善的缓存保护
-
✅ 灵活配置:支持多种参数自定义,适应不同业务场景
-
✅ 健壮可靠:完善的异常处理和重试机制
-
✅ 易于扩展:良好的设计为未来功能扩展预留了空间
在实际项目中,这个工具类已经帮助我们解决了多个高并发场景下的缓存问题,显著提升了系统的稳定性和性能。