1. 缓存雪崩解决方案(随机过期时间工具类 + 使用示例)
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Random;
import java.util.concurrent.TimeUnit;/*** 缓存工具类:解决缓存雪崩问题(随机过期时间)*/
@Component
public class CouponCacheUtil {private final RedisTemplate<String, Object> redisTemplate;// 随机数生成器:用于生成过期时间偏移量private static final Random RANDOM = new Random();// 基础过期时间(30分钟):可根据业务调整private static final int BASE_EXPIRE_MINUTES = 30;// 随机偏移范围(5-30分钟):避免缓存集中过期private static final int MIN_RANDOM_MINUTES = 5;private static final int MAX_RANDOM_MINUTES = 30;public CouponCacheUtil(RedisTemplate<String, Object> redisTemplate) {this.redisTemplate = redisTemplate;}/*** 生成随机过期时间(基础时间+随机偏移)* 解决缓存雪崩:避免大量缓存同时过期*/private int getRandomExpireMinutes() {// 生成5-30分钟的随机值int randomOffset = MIN_RANDOM_MINUTES + RANDOM.nextInt(MAX_RANDOM_MINUTES - MIN_RANDOM_MINUTES + 1);// 总过期时间 = 基础时间 + 随机偏移return BASE_EXPIRE_MINUTES + randomOffset;}/*** 存储用户优惠券到缓存(带随机过期时间)* @param userId 用户ID* @param couponId 优惠券ID* @param couponInfo 优惠券信息(对象)*/public void setUserCouponCache(Long userId, Long couponId, Object couponInfo) {// 缓存key格式:业务前缀+用户ID+优惠券ID(避免key冲突)String cacheKey = "coupon:user:" + userId + ":" + couponId;// 获取随机过期时间int expireMinutes = getRandomExpireMinutes();// 存入Redis并设置过期时间redisTemplate.opsForValue().set(cacheKey,couponInfo,expireMinutes,TimeUnit.MINUTES);}/*** 获取用户优惠券缓存*/public Object getUserCouponCache(Long userId, Long couponId) {String cacheKey = "coupon:user:" + userId + ":" + couponId;return redisTemplate.opsForValue().get(cacheKey);}
}
2. 缓存穿透解决方案(优惠券模板查询示例)
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;/*** 优惠券模板服务:解决缓存穿透问题*/
@Service
public class CouponTemplateService {private final CacheManager cacheManager;private final CouponTemplateDao couponTemplateDao; // 数据库访问层// 缓存穿透标记:用于标记不存在的模板(避免重复查询DB)private static final String CACHE_PENETRATION_MARKER = "NOT_EXIST";// 无效标记的过期时间(10分钟):避免长期占用缓存private static final int MARKER_EXPIRE_MINUTES = 10;public CouponTemplateService(CacheManager cacheManager, CouponTemplateDao couponTemplateDao) {this.cacheManager = cacheManager;this.couponTemplateDao = couponTemplateDao;}/*** 批量查询优惠券模板(解决缓存穿透)* @param templateIds 模板ID列表*/public List<CouponTemplate> queryTemplateBatch(List<Long> templateIds) {// 1. 参数校验:过滤无效ID(如null/负数),避免恶意请求List<Long> validIds = templateIds.stream().filter(id -> id != null && id > 0).toList();if (validIds.isEmpty()) {return new ArrayList<>();}// 2. 从缓存获取数据(使用LoadingCache或Spring Cache)Cache templateCache = cacheManager.getCache("coupon:template");List<CouponTemplate> result = new ArrayList<>();List<Long> missIds = new ArrayList<>(); // 缓存未命中的IDfor (Long id : validIds) {Object cacheValue = templateCache.get(id);if (cacheValue == null) {// 缓存未命中,记录ID后续查DBmissIds.add(id);} else if (CACHE_PENETRATION_MARKER.equals(cacheValue)) {// 命中无效标记,跳过(不查DB)continue;} else {// 命中有效数据,加入结果result.add((CouponTemplate) cacheValue);}}// 3. 缓存未命中的ID查询DBif (!missIds.isEmpty()) {List<CouponTemplate> dbTemplates = couponTemplateDao.queryByIds(missIds);// 4. 处理DB查询结果:存在的存入缓存,不存在的设置无效标记for (Long id : missIds) {// 查找DB中是否存在该模板CouponTemplate template = dbTemplates.stream().filter(t -> Objects.equals(t.getId(), id)).findFirst().orElse(null);if (template != null) {// 存在:存入缓存(使用随机过期时间,复用上面的工具类)templateCache.put(id, template);result.add(template);} else {// 不存在:设置无效标记(短期过期),避免缓存穿透templateCache.put(id, CACHE_PENETRATION_MARKER);// 手动设置标记的过期时间(如果缓存支持)// 示例:redisTemplate.expire(key, MARKER_EXPIRE_MINUTES, TimeUnit.MINUTES);}}}return result;}
}
3. 缓存穿透解决方案(用户优惠券查询示例)
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;/*** 用户优惠券服务:解决缓存穿透问题*/
@Service
public class UserCouponService {private final RedisTemplate<String, Object> redisTemplate;private final UserCouponDao userCouponDao; // 数据库访问层private final CouponCacheUtil cacheUtil; // 复用上面的缓存工具类// 无效用户优惠券标记(如:-1表示不存在)private static final Long INVALID_COUPON_MARKER = -1L;public UserCouponService(RedisTemplate<String, Object> redisTemplate,UserCouponDao userCouponDao,CouponCacheUtil cacheUtil) {this.redisTemplate = redisTemplate;this.userCouponDao = userCouponDao;this.cacheUtil = cacheUtil;}/*** 查询用户是否拥有某优惠券(解决缓存穿透)* @param userId 用户ID(需校验合法性)* @param couponId 优惠券ID*/public boolean hasUserCoupon(Long userId, Long couponId) {// 1. 参数合法性校验:拦截无效用户ID(如null/负数)if (userId == null || userId <= 0 || couponId == null || couponId <= 0) {// 记录恶意请求日志(可选)return false;}// 2. 从缓存查询String cacheKey = "coupon:user:has:" + userId + ":" + couponId;Object cacheValue = redisTemplate.opsForValue().get(cacheKey);if (cacheValue != null) {// 缓存命中:判断是否为有效标记return !INVALID_COUPON_MARKER.equals(cacheValue);}// 3. 缓存未命中,查询DBboolean exists = userCouponDao.exists(userId, couponId);// 4. 结果存入缓存:存在则存true,不存在则存无效标记if (exists) {// 有效数据:用随机过期时间(复用工具类)cacheUtil.setUserCouponCache(userId, couponId, true);} else {// 无效数据:设置标记(短期过期)redisTemplate.opsForValue().set(cacheKey,INVALID_COUPON_MARKER,5, // 5分钟过期,避免长期缓存无效数据TimeUnit.MINUTES);}return exists;}
}
关键实现说明:
- 缓存雪崩:通过
CouponCacheUtil
生成随机过期时间(基础时间 + 5-30 分钟偏移),避免大量缓存同时失效。 - 缓存穿透:
- 对无效参数(如负数 ID)直接拦截,减少无效请求。
- 对 DB 中不存在的数据,缓存 “无效标记”(如 "NOT_EXIST"),短期过期,避免重复查询 DB。
- 结合业务场景区分有效 / 无效数据(如用户优惠券用
-1
标记不存在)。
- 代码复用:缓存工具类和过期时间策略可全局复用,减少冗余代码。