高并发场景下的缓存利器
引言
在高并发系统中,缓存是提升系统性能、降低数据库压力的关键组件。然而,如果使用不当,缓存也会带来一系列问题:缓存穿透、缓存击穿、缓存雪崩。今天我们来深度解析一个专门为解决这些问题而生的 Redis 工具类。
一、工具类概述
专门为高并发场景设计的缓存工具类,提供了多种缓存问题的解决方案:
🔒 缓存击穿:通过分布式锁机制防止热点 key 失效时大量请求直达数据库
🕳️ 缓存穿透:通过空值缓存防止恶意查询不存在的 key
❄️ 缓存雪崩:通过随机过期时间避免大量 key 同时失效
🔄 多级缓存:支持本地缓存+Redis 缓存的多级缓存架构
🔥 缓存预热:支持缓存预热机制
工具完整代码:
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. 基础缓存获取(带分布式锁)
// 使用示例
User user = ConcurrentCacheUtils.getFromCacheWithLock("user:123", () -> userService.getUserById(123),300 // 5分钟过期
);实现原理:
双重检查锁定:先查缓存,未命中再尝试加锁
分布式锁:使用 Redis 的
setIfAbsent实现分布式锁随机过期时间:防止缓存雪崩
重试机制:获取锁失败时自动重试
2. 防缓存穿透版本
// 使用示例
User user = ConcurrentCacheUtils.getFromCacheWithPenetrationProtection("user:999", () -> userService.getUserById(999),300, // 正常数据过期时间:5分钟60 // 空值过期时间:1分钟
);特色功能:
空值标记:对查询结果为 null 的情况,缓存特殊标记
<NULL>差异化过期:空值使用较短的过期时间,既防穿透又保证数据最终一致性
3. 多级缓存支持
// 使用示例(当前版本主要处理Redis层)
Product product = ConcurrentCacheUtils.getFromMultiLevelCache("product:456",() -> productService.getProductById(456),1800, // Redis缓存30分钟300 // 本地缓存5分钟(预留扩展)
);设计思路:
为未来集成 Caffeine 等本地缓存框架预留了扩展接口。
4. 缓存预热机制
// 使用示例 - 适合热点数据
HotNews hotNews = ConcurrentCacheUtils.getFromCacheWithWarmUp("hotnews:daily",() -> newsService.getDailyHotNews(),7200 // 2小时基础过期时间
);预热策略:
设置较短的基础过期时间
通过定期访问触发数据更新
避免冷启动问题
三、技术亮点解析
1. 健壮的重试机制
// 避免递归可能导致的栈溢出
while (retryCount < maxRetries) {// 使用循环替代递归,更安全
}2. 完善的异常处理
try {// 业务逻辑
} catch (InterruptedException e) {Thread.currentThread().interrupt(); // 保持中断状态throw new ServiceException("获取缓存数据被中断");
} catch (Exception e) {log.error("缓存加载失败", e); // 详细日志记录throw new ServiceException("缓存加载失败: " + e.getMessage());
}3. 线程安全设计
使用
private构造器防止实例化所有方法都是静态方法
无状态设计,线程安全
四、实战应用场景
场景1:电商商品详情页
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:秒杀活动信息
public SeckillInfo getSeckillInfo(Long activityId) {String cacheKey = "seckill:info:" + activityId;return ConcurrentCacheUtils.getFromCacheWithLock(cacheKey,() -> seckillService.getSeckillInfo(activityId),60, // 秒级数据,短时间缓存5 // 最多重试5次);
}五、性能优化建议
1. 参数调优
// 根据业务特点调整参数
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 接口
键名规范:保证缓存键的唯一性和可读性
内存控制:注意大对象缓存可能的内存问题
一致性考虑:在数据更新时要及时清理或更新缓存
七、总结
工具类为高并发场景下的缓存使用提供了一套完整的解决方案,具有以下优势:
✅ 开箱即用:简单的方法调用即可获得完善的缓存保护
✅ 灵活配置:支持多种参数自定义,适应不同业务场景
✅ 健壮可靠:完善的异常处理和重试机制
✅ 易于扩展:良好的设计为未来功能扩展预留了空间
在实际项目中,这个工具类已经帮助我们解决了多个高并发场景下的缓存问题,显著提升了系统的稳定性和性能。
