当前位置: 首页 > news >正文

高并发场景下的缓存利器

引言

在高并发系统中,缓存是提升系统性能、降低数据库压力的关键组件。然而,如果使用不当,缓存也会带来一系列问题:缓存穿透缓存击穿缓存雪崩。今天我们来深度解析一个专门为解决这些问题而生的 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. 监控指标

建议监控以下指标:

  • 缓存命中率

  • 锁竞争频率

  • 重试次数统计

  • 空值缓存比例

六、注意事项

  1. 序列化要求:缓存的对象必须实现 Serializable 接口

  2. 键名规范:保证缓存键的唯一性和可读性

  3. 内存控制:注意大对象缓存可能的内存问题

  4. 一致性考虑:在数据更新时要及时清理或更新缓存

七、总结

工具类为高并发场景下的缓存使用提供了一套完整的解决方案,具有以下优势:

  • ✅ 开箱即用:简单的方法调用即可获得完善的缓存保护

  • ✅ 灵活配置:支持多种参数自定义,适应不同业务场景

  • ✅ 健壮可靠:完善的异常处理和重试机制

  • ✅ 易于扩展:良好的设计为未来功能扩展预留了空间

在实际项目中,这个工具类已经帮助我们解决了多个高并发场景下的缓存问题,显著提升了系统的稳定性和性能。

http://www.dtcms.com/a/581321.html

相关文章:

  • XML 元素:解析与运用
  • LegionSpace黑客松指南(二):MCP服务开发与集成详解
  • 关于js导入Excel时,Excel的(年/月/日)日期是五位数字的问题。以及对Excel日期存在的错误的分析和处理。
  • 内网穿透的应用-摆脱局域网!Stable Diffusion3.5 结合cpolar使用更方便
  • postgresql客户端升级
  • 16openlayers加载COG(云优化Geotiff)
  • 万网做的网站咋样wordpress 用户api
  • 小白学规则编写:雷池 WAF 配置教程,用 Nginx 护住 WordPress 博客
  • 升鲜宝生鲜配送供应链管理系统---PMS--商品品牌多语言存储与 Redis 缓存同步实现
  • Spark的容错机制
  • spark性能优化2:Window操作和groupBy操作的区别
  • 用spark-md5实现切片上传前端起node模拟上传文件大小,消耗时间
  • 做网站优化竞价区别开发工具的种类及使用方法
  • Mac安装pnpm步骤以及会出现的问题
  • ofd在线预览js+springboot跳转
  • 基于SpringBoot实习管理系统的设计与实现的设计与实现
  • abuild的使用说明-如何使用vscode进行c/c++开发
  • 宝山php网站开发培训可以看那种东西的手机浏览器
  • 算法28.0
  • Spring Cloud中的@LoadBalanced注解实现原理
  • 建站快车的使用方法电子商务网站对比分析
  • 分布式Web应用场景下存在的Session问题
  • 12.线程(一)
  • 如何做二维码跳转到网站建设网站专家
  • 前端i18n实现中英文切换
  • Java基础——常用算法4
  • SQL50+Hot100系列(11.7)
  • Python 第二十六节 多线程应用详细介绍及使用注意事项
  • 网站建设交接表wordpress编程视频教程
  • LeafView(轻量级电脑图片查看器) v3.8.1 中文绿色便携版