为什么需要二级缓存? 在分布式系统开发中,缓存是提升性能的 "利器",但单一缓存方案总有局限: 纯本地缓存(如 HashMap、Caffeine)速度快但无法跨服务共享,分布式环境下数据一致性难保证; 纯 Redis 缓存解决了分布式共享问题,但远程网络调用存在延迟,高并发下仍可能成为瓶颈; 更麻烦的是,高并发场景下还可能遇到缓存穿透、击穿、雪崩 "老三样" 问题,稍不注意就会压垮数据库。 基于这些痛点,我设计了这套本地缓存 + Redis 二级缓存工具类,结合两者优势:本地缓存负责 "快",Redis 负责 "稳",再通过精心设计的机制解决缓存常见问题。今天就把这套工具类的设计思路、使用方法分享出来,方便大家在项目中直接复用~
核心功能与适用场景
什么是 "二级缓存"?
简单说,就是在应用本地维护一层缓存(本文基于 Guava Cache 实现),同时结合分布式 Redis 缓存,形成 "本地缓存→Redis→数据库" 的三级查询链路。查询时优先命中本地缓存,减少远程调用;更新时同步维护两级缓存,保证数据一致性。
适用场景
这套工具类尤其适合以下场景:
- 高并发读场景:如商品详情、用户信息、配置参数等高频访问数据,本地缓存可将响应时间压到微秒级;
- 分布式一致性要求:需要多服务共享缓存状态(如库存、订单状态),Redis 保证跨服务数据一致;
- 缓存问题高发区:存在热点 Key、数据库压力大、需要防缓存穿透 / 击穿 / 雪崩的场景。
痛点案例: 在电商商品详情页、新闻资讯列表、用户个人中心等场景中,某类数据(如爆款商品信息、热门文章)会被数万用户同时访问,单次请求即使耗时 10ms,高并发下也会累积成秒级延迟,甚至触发数据库连接池耗尽。
设计架构与核心组件解析
整体架构
工具类采用 "注解驱动 + 管理器核心 + 自动初始化" 的架构,核心组件包括 3 个:
@LocalCacheConfig:缓存配置注解,定义缓存名称、大小、过期时间; LocalCacheConfigProcessor:缓存初始化处理器,自动扫描注解并初始化缓存; TwoLevelCacheManager:二级缓存核心管理器,封装本地缓存 + Redis 操作,解决缓存问题。
核心组件深度解析
- @LocalCacheConfig:用注解定义缓存规则 这是缓存的 "配置入口",通过注解在类上声明缓存属性,无需手动编写初始化代码。
less
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LocalCacheConfig {
String value(); // 缓存名称(必填)
int maxSize() default 5000; // 最大条目数,默认5000
int expireSeconds() default 60; // 过期时间(秒),默认60秒
}
作用:通过注解声明缓存元数据,后续由处理器自动读取并初始化缓存实例。
- LocalCacheConfigProcessor:自动初始化的 "幕后推手"
作为 Spring 的BeanPostProcessor,它会在 Bean 初始化前扫描@LocalCacheConfig注解,调用TwoLevelCacheManager初始化缓存。
kotlin
@Component
public class LocalCacheConfigProcessor implements BeanPostProcessor {
private final TwoLevelCacheManager cacheManager;
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
LocalCacheConfig annotation = bean.getClass().getAnnotation(LocalCacheConfig.class);
if (annotation != null) {
// 初始化缓存:名称、最大大小、过期时间
cacheManager.initLocalCache(annotation.value(), annotation.maxSize(), annotation.expireSeconds());
log.info("✅ 初始化本地缓存: {} (maxSize={}, expireSeconds={})",
annotation.value(), annotation.maxSize(), annotation.expireSeconds());
}
return bean;
}
}
作用:实现缓存的 "零侵入" 初始化,开发者只需加注解,无需手动调用初始化方法。
- TwoLevelCacheManager:缓存逻辑的 "核心大脑"
这是工具类的核心,封装了本地缓存(Guava Cache)和 Redis 的交互逻辑,解决了缓存三大问题,同时提供丰富的 API。
- 核心属性:
- localCacheMap:存储本地缓存实例(key 为缓存名称);
- redisTemplate:操作 Redis 的客户端;
- refreshExecutor:异步刷新缓存的线程池;
- keyLocks:防击穿的锁集合,配合双重检查机制。
防 "三高" 问题设计:从根源解决缓存痛点
在高并发场景下,缓存容易出现三大问题,工具类通过以下设计逐个击破:
- 防缓存击穿:锁住热点 Key,避免数据库 "单点过载"
当热点 Key 过期瞬间,大量请求会穿透到数据库,导致压力骤增。解决方案是加锁 + 双重检查:
csharp
// 伪代码流程
public T get(...) {
// 1. 查本地缓存 → 2. 查Redis
if (缓存未命中) {
ReentrantLock lock = getLock(key); // 获取锁
lock.lock();
try {
// 双重检查:再次查Redis,避免锁等待期间缓存已更新
if (Redis仍未命中) {
查数据库 → 更新缓存
}
} finally {
lock.unlock();
异步清理锁(避免内存泄漏)
}
}
}
关键:通过keyLocks维护锁集合,结合cleanupLockAsync异步清理过期锁,防止内存泄漏。
- 防缓存雪崩:随机过期时间,避免 "集体失效"
当大量缓存同时过期,请求会集中穿透到数据库,导致雪崩。解决方案是给过期时间加随机偏移:
scss
// 防雪崩:过期时间=基础时间+随机偏移
long randomOffset = redisExpireSeconds > 10 ?
ThreadLocalRandom.current().nextLong(1, redisExpireSeconds / 10 + 1) : 1;
redisTemplate.opsForValue().set(key, cacheStr, redisExpireSeconds + randomOffset, TimeUnit.SECONDS);
效果:将缓存过期时间打散,避免同一时间大量缓存失效。
- 防缓存穿透:谨慎处理空值,避免 "无效请求"
当请求不存在的数据(如 ID=-1),缓存和数据库都无结果,会导致请求持续穿透到数据库。工具类通过不缓存空值 + 快速返回处理:
kotlin
// 数据库查询结果为null时,不写入缓存
T dbValue = dbLoader.get();
if (dbValue == null) {
return null; // 直接返回,不缓存空值
}
说明:相比缓存空值,这种方式更节省空间,适合明确 "无效 Key" 场景;若需缓存空值,可根据业务调整逻辑。
设计亮点:那些藏在细节里的思考
-
自动初始化机制 通过LocalCacheConfigProcessor实现缓存 "声明式配置",开发者无需手动调用initLocalCache,降低使用成本。
-
线程安全与资源管理 锁机制:用ReentrantLock替代synchronized,支持更灵活的锁控制; 线程池:异步刷新使用ThreadPoolExecutor,核心线程 2 个,最大 10 个,拒绝策略为 "调用者执行",避免任务丢失; 资源清理:@PreDestroy和DisposableBean确保服务关闭时,线程池优雅退出、缓存和锁集合清空,无内存泄漏。
-
灵活的序列化与类型支持 通过 FastJSON 的TypeReference处理泛型类型(如List),支持基础类型(int、long 等)、日期类型(Date、LocalDateTime)和自定义对象的序列化 / 反序列化,兼容性强。
复制代码看这里
LocalCacheConfig
less
/**
* 本地缓存配置(用于自动初始化缓存维度)
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LocalCacheConfig {
String value();
int maxSize() default 5000;
int expireSeconds() default 60;
}
LocalCacheConfigProcessor
kotlin
@Component
@Slf4j
public class LocalCacheConfigProcessor implements BeanPostProcessor {
private final TwoLevelCacheManager cacheManager;
public LocalCacheConfigProcessor(TwoLevelCacheManager cacheManager) {
this.cacheManager = cacheManager;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
LocalCacheConfig annotation = bean.getClass().getAnnotation(LocalCacheConfig.class);
if (annotation != null) {
cacheManager.initLocalCache(annotation.value(), annotation.maxSize(), annotation.expireSeconds());
log.info("✅ 初始化本地缓存: {} (maxSize={}, expireMinutes={})%n",
annotation.value(), annotation.maxSize(), annotation.expireSeconds());
}
return bean;
}
}
TwoLevelCacheManager
scss
package com.barcke.component.utils.cache;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.PreDestroy;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;
/**
* 二级缓存管理器 - 支持本地缓存(Guava Cache)+ Redis 分布式缓存
* 具备防击穿、防穿透、防雪崩等功能
*
* @author Barcke
* @version 2.0
* @projectName self-barcke
* @className TwoLevelCacheManager
* @date 2025/8/9 11:56
* @slogan: 源于生活 高于生活
*/
@Component
@Slf4j
public class TwoLevelCacheManager implements DisposableBean {
private final Map<String, Cache<String, String>> localCacheMap = new ConcurrentHashMap<>();
private final StringRedisTemplate redisTemplate;
// 线程池状态控制
private volatile boolean isShutdown = false;
private final AtomicInteger activeRefreshTasks = new AtomicInteger(0);
// 优化的线程池配置(异步刷新用)
private final ExecutorService refreshExecutor = new ThreadPoolExecutor(
2, // 核心线程
10, // 最大线程
60L, TimeUnit.SECONDS, // 空闲线程存活时间
new ArrayBlockingQueue<>(1000), // 队列容量
r -> {
Thread thread = new Thread(r, "TwoLevelCache-Refresh-" + System.currentTimeMillis());
thread.setDaemon(true); // 设置为守护线程
return thread;
},
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:由调用线程执行
);
// 使用ReentrantLock替代Object锁,提供更好的性能和可控性
private final Map<String, ReentrantLock> keyLocks = new ConcurrentHashMap<>();
// 锁的最大数量限制,防止内存泄漏
private static final int MAX_LOCKS = 10000;
public TwoLevelCacheManager(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
log.info("TwoLevelCacheManager initialized successfully");
}
/**
* 初始化本地缓存
* @param cacheName 缓存名称,不能为空
* @param maxSize 最大缓存条目数,默认5000
* @param expireSeconds 过期时间(秒),默认1分钟
*/
public void initLocalCache(String cacheName, Integer maxSize, Integer expireSeconds) {
if (!StringUtils.hasText(cacheName)) {
throw new IllegalArgumentException("Cache name cannot be null or empty");
}
try {
Cache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(Optional.ofNullable(maxSize).orElse(5000))
.expireAfterWrite(Optional.ofNullable(expireSeconds).orElse(5), TimeUnit.SECONDS)
.recordStats() // 启用统计信息
.build();
localCacheMap.put(cacheName, cache);
log.info("Local cache [{}] initialized with maxSize={}, expireMinutes={}",
cacheName, maxSize, expireSeconds);
} catch (Exception e) {
log.error("Failed to initialize local cache [{}]", cacheName, e);
throw new RuntimeException("Failed to initialize local cache: " + cacheName, e);
}
}
/**
* 获取锁对象,如果锁数量超过限制则清理
*/
private ReentrantLock getLock(String key) {
// 简单的清理策略:当锁数量过多时,清空所有锁
if (keyLocks.size() > MAX_LOCKS) {
synchronized (keyLocks) {
if (keyLocks.size() > MAX_LOCKS) {
keyLocks.clear();
log.warn("Key locks size exceeded limit, cleared all locks");
}
}
}
return keyLocks.computeIfAbsent(key, k -> new ReentrantLock());
}
/**
* 获取缓存(带防击穿/穿透/雪崩)
* @param cacheName 缓存名称
* @param key 缓存键
* @param clazz 返回类型
* @param redisExpireSeconds Redis过期时间(秒)
* @param dbLoader 数据加载器
* @return 缓存值
*/
public <T> T get(String cacheName, String key, Class<T> clazz, long redisExpireSeconds, Supplier<T> dbLoader) {
return get(cacheName, key, clazz, null, redisExpireSeconds, dbLoader);
}
/**
* 支持基础类型、List等复杂类型的缓存获取
* @param cacheName 缓存名称
* @param key 缓存键
* @param clazz 返回类型
* @param typeReference List等复杂类型时传入
* @param redisExpireSeconds Redis过期时间
* @param dbLoader 数据加载器
* @return 缓存值
*/
public <T> T get(String cacheName, String key, Class<T> clazz, TypeReference<T> typeReference, long redisExpireSeconds, Supplier<T> dbLoader) {
// 参数校验
if (!StringUtils.hasText(cacheName) || !StringUtils.hasText(key) || clazz == null || dbLoader == null) {
throw new IllegalArgumentException("Invalid parameters for cache get operation");
}
if (isShutdown) {
log.warn("Cache manager is shutdown, executing dbLoader directly");
return dbLoader.get();
}
Cache<String, String> localCache = localCacheMap.get(cacheName);
if (localCache == null) {
throw new IllegalArgumentException("Local cache not initialized: " + cacheName);
}
try {
// 1. 本地缓存查询
String localValue = localCache.getIfPresent(key);
if (localValue != null) {
log.debug("Cache hit from local cache: key={}", key);
return "null".equals(localValue) ? null : parseValue(localValue, clazz, typeReference);
}
// 2. Redis缓存查询
String redisValue = null;
try {
redisValue = redisTemplate.opsForValue().get(key);
} catch (Exception e) {
log.warn("Failed to get value from Redis for key: {}", key, e);
}
if (redisValue != null) {
log.debug("Cache hit from Redis: key={}", key);
localCache.put(key, redisValue);
return "null".equals(redisValue) ? null : parseValue(redisValue, clazz, typeReference);
}
// 3. 防击穿:加锁查询数据库
ReentrantLock lock = getLock(key);
lock.lock();
try {
// 双重检查:再次检查缓存
try {
redisValue = redisTemplate.opsForValue().get(key);
if (redisValue != null) {
localCache.put(key, redisValue);
return "null".equals(redisValue) ? null : parseValue(redisValue, clazz, typeReference);
}
} catch (Exception e) {
log.warn("Failed to double-check Redis for key: {}", key, e);
}
// 4. 查询数据库
log.debug("Cache miss, loading from database: key={}", key);
T dbValue;
try {
dbValue = dbLoader.get();
} catch (Exception e) {
log.error("Failed to load data from database for key: {}", key, e);
throw new RuntimeException("Failed to load data from database", e);
}
// 5. 结果处理和缓存设置
if (dbValue == null) {
// 如果dbValue为null,直接返回,不进行缓存
return null;
}
// 序列化并缓存
String cacheStr;
try {
cacheStr = toCacheString(dbValue, clazz, typeReference);
} catch (Exception e) {
log.error("Failed to serialize object for key: {}", key, e);
return dbValue; // 返回数据,但不缓存
}
// 防雪崩:添加随机过期时间偏移
long randomOffset = redisExpireSeconds > 10 ?
ThreadLocalRandom.current().nextLong(1, redisExpireSeconds / 10 + 1) : 1;
try {
redisTemplate.opsForValue().set(key, cacheStr, redisExpireSeconds + randomOffset, TimeUnit.SECONDS);
localCache.put(key, cacheStr);
} catch (Exception e) {
log.warn("Failed to cache value for key: {}", key, e);
}
return dbValue;
} finally {
lock.unlock();
// 异步清理锁(避免内存泄漏)
cleanupLockAsync(key);
}
} catch (Exception e) {
log.error("Unexpected error in cache get operation for key: {}", key, e);
try {
return dbLoader.get();
} catch (Exception dbException) {
log.error("Fallback database query also failed for key: {}", key, dbException);
throw new RuntimeException("Both cache and database query failed", dbException);
}
}
}
/**
* 支持直接指定List元素类型的缓存获取(简化版)
* @param cacheName 缓存名称
* @param key 缓存键
* @param elementType List元素类型
* @param redisExpireSeconds Redis过期时间
* @param dbLoader 数据加载器
* @return List<T>
*/
public <T> List<T> getList(String cacheName, String key, Class<T> elementType, long redisExpireSeconds, Supplier<List<T>> dbLoader) {
// 由于泛型擦除,List.class 不能直接用于反序列化泛型列表,需用TypeReference<List<T>>
TypeReference<List<T>> typeReference = new TypeReference<List<T>>() {
@Override
public Type getType() {
// 这里必须返回 List<T> 的 ParameterizedType
return new com.alibaba.fastjson.util.ParameterizedTypeImpl(
new Type[]{elementType}, null, List.class);
}
};
// 这里clazz参数必须传List.class,typeReference为List<T>,dbLoader需要适配Supplier<T>
// 由于主get方法的泛型T,需强转dbLoader
Supplier<List<T>> safeDbLoader = dbLoader;
return get(cacheName, key, List.class, (TypeReference) typeReference, redisExpireSeconds, (Supplier) safeDbLoader);
}
/**
* 兼容基础类型、List等复杂类型的解析
*/
@SuppressWarnings("unchecked")
private <T> T parseValue(String value, Class<T> clazz, TypeReference<T> typeReference) {
try {
// 基础类型直接返回
if (clazz == String.class) {
return (T) value;
}
if (clazz == Integer.class || clazz == int.class) {
return (T) Integer.valueOf(value);
}
if (clazz == Long.class || clazz == long.class) {
return (T) Long.valueOf(value);
}
if (clazz == Boolean.class || clazz == boolean.class) {
return (T) Boolean.valueOf(value);
}
if (clazz == Double.class || clazz == double.class) {
return (T) Double.valueOf(value);
}
if (clazz == Float.class || clazz == float.class) {
return (T) Float.valueOf(value);
}
if (clazz == Short.class || clazz == short.class) {
return (T) Short.valueOf(value);
}
if (clazz == Byte.class || clazz == byte.class) {
return (T) Byte.valueOf(value);
}
if (clazz == java.util.Date.class) {
// fastjson默认格式
return (T) new java.util.Date(Long.parseLong(value));
}
if (clazz == LocalDateTime.class) {
// fastjson默认格式为yyyy-MM-ddTHH:mm:ss
// 兼容ISO_LOCAL_DATE_TIME和yyyy-MM-dd HH:mm:ss
try {
return (T) LocalDateTime.parse(value, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
} catch (Exception e) {
return (T) LocalDateTime.parse(value, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
}
// List等复杂类型
if (typeReference != null) {
return JSON.parseObject(value, typeReference);
}
// 其他对象
return JSON.parseObject(value, clazz);
} catch (Exception e) {
log.error("Failed to parse value: {} to type: {}", value, clazz, e);
throw new RuntimeException("Failed to parse cached value", e);
}
}
/**
* 兼容基础类型、List等复杂类型的序列化
*/
@SuppressWarnings("unchecked")
private <T> String toCacheString(T value, Class<T> clazz, TypeReference<T> typeReference) {
if (value == null) {
return "null";
}
// 基础类型直接toString
if (clazz == String.class || clazz == Integer.class || clazz == int.class
|| clazz == Long.class || clazz == long.class
|| clazz == Boolean.class || clazz == boolean.class
|| clazz == Double.class || clazz == double.class
|| clazz == Float.class || clazz == float.class
|| clazz == Short.class || clazz == short.class
|| clazz == Byte.class || clazz == byte.class) {
return value.toString();
}
if (clazz == java.util.Date.class) {
return String.valueOf(((java.util.Date) value).getTime());
}
if (clazz == LocalDateTime.class) {
// 使用ISO_LOCAL_DATE_TIME格式
return ((LocalDateTime) value).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}
// List等复杂类型
if (typeReference != null) {
return JSON.toJSONString(value);
}
// 其他对象
return JSON.toJSONString(value);
}
/**
* 安全解析JSON(兼容老接口)
*/
private <T> T parseJson(String json, Class<T> clazz) {
return parseValue(json, clazz, null);
}
/**
* 异步清理锁
*/
private void cleanupLockAsync(String key) {
if (!isShutdown && !refreshExecutor.isShutdown()) {
refreshExecutor.submit(() -> {
try {
// 延迟清理,给其他可能等待的线程一些时间
Thread.sleep(1000);
keyLocks.remove(key);
} catch (Exception e) {
log.debug("Error during lock cleanup for key: {}", key, e);
}
});
}
}
/**
* 异步刷新缓存
* @param cacheName 缓存名称
* @param key 缓存键
* @param redisExpireSeconds Redis过期时间
* @param dbLoader 数据加载器
*/
public <T> void refreshAsync(String cacheName, String key, long redisExpireSeconds, Supplier<T> dbLoader) {
refreshAsync(cacheName, key, null, redisExpireSeconds, dbLoader);
}
/**
* 支持基础类型、List等复杂类型的异步刷新
*/
public <T> void refreshAsync(String cacheName, String key, TypeReference<T> typeReference, long redisExpireSeconds, Supplier<T> dbLoader) {
if (!StringUtils.hasText(cacheName) || !StringUtils.hasText(key) || dbLoader == null) {
log.warn("Invalid parameters for async refresh operation");
return;
}
if (isShutdown || refreshExecutor.isShutdown()) {
log.warn("Cache manager or refresh executor is shutdown, skipping async refresh");
return;
}
Cache<String, String> localCache = localCacheMap.get(cacheName);
if (localCache == null) {
log.warn("Local cache not initialized: {}", cacheName);
return;
}
// 控制并发刷新任务数量
if (activeRefreshTasks.get() > 100) {
log.warn("Too many active refresh tasks, skipping refresh for key: {}", key);
return;
}
activeRefreshTasks.incrementAndGet();
refreshExecutor.submit(() -> {
try {
log.debug("Starting async refresh for key: {}", key);
T dbValue = dbLoader.get();
// 如果dbValue为null,不进行缓存,直接返回
if (dbValue == null) {
log.debug("Async refresh returned null for key: {}", key);
return;
}
String cacheStr = toCacheString(dbValue, (Class<T>) dbValue.getClass(), typeReference);
localCache.put(key, cacheStr);
long randomOffset = redisExpireSeconds > 10 ?
ThreadLocalRandom.current().nextLong(1, redisExpireSeconds / 10 + 1) : 1;
redisTemplate.opsForValue().set(key, cacheStr, redisExpireSeconds + randomOffset, TimeUnit.SECONDS);
log.debug("Async refresh completed for key: {}", key);
} catch (Exception e) {
log.error("Async refresh failed for key: {}", key, e);
} finally {
activeRefreshTasks.decrementAndGet();
}
});
}
/**
* 支持直接指定List元素类型的异步刷新(简化版)
*/
public <T> void refreshListAsync(String cacheName, String key, Class<T> elementType, long redisExpireSeconds, Supplier<List<T>> dbLoader) {
refreshAsync(cacheName, key, new TypeReference<List<T>>() {
@Override
public Type getType() {
return new com.alibaba.fastjson.util.ParameterizedTypeImpl(new Type[]{elementType}, null, List.class);
}
}, redisExpireSeconds, dbLoader);
}
/**
* 设置缓存
* @param cacheName 缓存名称
* @param key 缓存键
* @param value 缓存值
* @param redisExpireSeconds Redis过期时间
*/
public void put(String cacheName, String key, Object value, long redisExpireSeconds) {
put(cacheName, key, value, null, redisExpireSeconds);
}
/**
* 支持基础类型、List等复杂类型的put
*/
@SuppressWarnings("unchecked")
public <T> void put(String cacheName, String key, T value, TypeReference<T> typeReference, long redisExpireSeconds) {
if (!StringUtils.hasText(cacheName) || !StringUtils.hasText(key)) {
throw new IllegalArgumentException("Cache name and key cannot be null or empty");
}
// 如果value为null,直接返回,不进行缓存
if (value == null) {
return;
}
Cache<String, String> localCache = localCacheMap.get(cacheName);
if (localCache == null) {
throw new IllegalArgumentException("Local cache not initialized: " + cacheName);
}
try {
String cacheStr = toCacheString(value, (Class<T>) (value != null ? value.getClass() : Object.class), typeReference);
localCache.put(key, cacheStr);
long randomOffset = redisExpireSeconds > 10 ?
ThreadLocalRandom.current().nextLong(1, redisExpireSeconds / 10 + 1) : 1;
redisTemplate.opsForValue().set(key, cacheStr, redisExpireSeconds + randomOffset, TimeUnit.SECONDS);
log.debug("Cache put successful for key: {}", key);
} catch (Exception e) {
log.error("Failed to put cache for key: {}", key, e);
throw new RuntimeException("Failed to put cache", e);
}
}
/**
* 支持直接指定List元素类型的put(简化版)
*/
public <T> void putList(String cacheName, String key, List<T> value, Class<T> elementType, long redisExpireSeconds) {
put(cacheName, key, value, new TypeReference<List<T>>() {
@Override
public Type getType() {
return new com.alibaba.fastjson.util.ParameterizedTypeImpl(new Type[]{elementType}, null, List.class);
}
}, redisExpireSeconds);
}
/**
* 删除缓存
* @param cacheName 缓存名称
* @param key 缓存键
*/
public void evict(String cacheName, String key) {
if (!StringUtils.hasText(key)) {
log.warn("Key cannot be null or empty for evict operation");
return;
}
try {
// 删除本地缓存
if (StringUtils.hasText(cacheName)) {
Cache<String, String> localCache = localCacheMap.get(cacheName);
if (localCache != null) {
localCache.invalidate(key);
log.debug("Local cache evicted for key: {}", key);
}
} else {
// 如果没有指定缓存名称,清理所有本地缓存
localCacheMap.values().forEach(cache -> cache.invalidate(key));
log.debug("All local caches evicted for key: {}", key);
}
// 删除Redis缓存
redisTemplate.delete(key);
log.debug("Redis cache evicted for key: {}", key);
} catch (Exception e) {
log.error("Failed to evict cache for key: {}", key, e);
}
}
/**
* 清理指定缓存的所有数据
* @param cacheName 缓存名称
*/
public void clear(String cacheName) {
if (!StringUtils.hasText(cacheName)) {
log.warn("Cache name cannot be null or empty for clear operation");
return;
}
Cache<String, String> localCache = localCacheMap.get(cacheName);
if (localCache != null) {
localCache.invalidateAll();
log.info("Local cache [{}] cleared", cacheName);
}
}
/**
* 获取缓存统计信息
* @param cacheName 缓存名称
* @return 统计信息字符串
*/
public String getCacheStats(String cacheName) {
if (!StringUtils.hasText(cacheName)) {
return "Invalid cache name";
}
Cache<String, String> localCache = localCacheMap.get(cacheName);
if (localCache == null) {
return "Cache not found: " + cacheName;
}
return String.format("Cache[%s] - Size: %d, Stats: %s",
cacheName, localCache.size(), localCache.stats());
}
/**
* 资源清理
*/
@PreDestroy
@Override
public void destroy() {
log.info("Starting TwoLevelCacheManager shutdown...");
isShutdown = true;
if (!refreshExecutor.isShutdown()) {
refreshExecutor.shutdown();
try {
if (!refreshExecutor.awaitTermination(10, TimeUnit.SECONDS)) {
log.warn("Refresh executor did not terminate gracefully, forcing shutdown");
refreshExecutor.shutdownNow();
}
} catch (InterruptedException e) {
log.warn("Interrupted while waiting for refresh executor termination");
refreshExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
}
// 清理本地缓存
localCacheMap.values().forEach(Cache::invalidateAll);
localCacheMap.clear();
// 清理锁
keyLocks.clear();
log.info("TwoLevelCacheManager shutdown completed");
}
}
如何使用(LocalCacheConfigTest)
kotlin
package com.barcke.component.utils.cache;
import cn.hutool.json.ObjectMapper;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import com.barcke.Main;
import com.barcke.component.utils.cache.annotation.LocalCacheConfig;
import com.barcke.pojo.entity.Visitor;
import com.google.common.collect.Lists;
import com.rometools.rome.feed.atom.Person;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Order;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import javax.cache.Cache;
import javax.cache.CacheManager;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Barcke
* @version 1.0
* @projectName self-barcke
* @className LocalCacheConfigTest
* @date 2025/8/9 12:26
* @slogan: 源于生活 高于生活
* @description:
**/
@Slf4j
@SpringBootTest(classes = Main.class,webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@RunWith(SpringRunner.class)
@LocalCacheConfig("test")
public class LocalCacheConfigTest {
@Autowired
private TwoLevelCacheManager twoLevelCacheManager;
@Test
public void test(){
assertThat(twoLevelCacheManager.get("test", "test", String.class, 10, () -> {
return "a";
})).isEqualTo("a");
assertThat(twoLevelCacheManager.get("test", "test", String.class, 10, () -> {
return "a";
})).isEqualTo("a");
assertThat(twoLevelCacheManager.get("test", "test1", Integer.class, 10, () -> {
return 1;
})).isEqualTo(1);
assertThat(twoLevelCacheManager.get("test", "test1", Integer.class, 10, () -> {
return 1;
})).isEqualTo(1);
LocalDateTime now = LocalDateTime.now();
assertThat(twoLevelCacheManager.get("test", "test2", LocalDateTime.class, 10, () -> {
return now;
})).isEqualTo(now);
assertThat(twoLevelCacheManager.get("test", "test2", LocalDateTime.class, 10, () -> {
return now;
})).isEqualTo(now);
List<Visitor> list = twoLevelCacheManager.get("test", "test3", List.class, 10, () -> {
return null;
});
System.out.println(JSONObject.toJSONString(list));
Map map = twoLevelCacheManager.get("test", "test4", Map.class, 10, () -> {
HashMap hashMap = new HashMap();
hashMap.put("a", "a");
return hashMap;
});
Visitor visitor = twoLevelCacheManager.get("test", "test5", Visitor.class, 10, () -> {
Visitor hashMap = new Visitor();
hashMap.setId(1111L);
hashMap.setLoginTime(new Date());
return hashMap;
});
List<Visitor> test = twoLevelCacheManager.getList("test", "test3", Visitor.class, 10, () -> {
return Lists.newArrayList(new Visitor().setNickName("aaa"));
});
List<Visitor> aa = twoLevelCacheManager.getList("test", "test3", Visitor.class, 10, () -> {
return null;
});
System.out.println(JSONObject.toJSONString(test));
}
}
总结与展望
这套二级缓存工具类通过 "本地缓存 + Redis" 的组合,在性能与一致性之间找到了平衡,同时解决了缓存穿透、击穿、雪崩等高频问题。核心优势在于:
易用性:注解驱动配置,API 简洁直观; 可靠性:完善的线程安全和资源管理; 扩展性:预留统计接口、支持自定义序列化,方便后续扩展。
后续计划加入缓存预热、动态配置刷新等功能,让工具类更强大。