高并发场景下的缓存利器

引言

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

建议监控以下指标:

  • 缓存命中率

  • 锁竞争频率

  • 重试次数统计

  • 空值缓存比例

六、注意事项

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

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

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

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

七、总结

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

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

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

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

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

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

相关推荐
2301_801252222 小时前
Tomcat的基本使用作用
java·tomcat
lkbhua莱克瓦242 小时前
Java基础——常用算法3
java·数据结构·笔记·算法·github·排序算法·学习方法
麦麦鸡腿堡2 小时前
Java_TreeSet与TreeMap源码解读
java·开发语言
教练、我想打篮球2 小时前
05 kafka 如何存储较大数据记录
java·kafka·record
uesowys2 小时前
华为OD算法开发指导-简易内存池
java·算法·华为od
gladiator+2 小时前
Java中的设计模式------策略设计模式
java·开发语言·设计模式
期待のcode3 小时前
Dockerfile镜像构建
java·docker·容器
小满、3 小时前
对象住哪里?——深入剖析 JVM 内存结构与对象分配机制
java·jvm·#java对象分配·#hotspot实现
How_doyou_do3 小时前
模态框的两种管理思路
java·服务器·前端