缓存作为提升应用性能的重要手段,其管理策略的合理性直接影响到应用的响应速度和数据一致性。在Spring框架中,Spring Cache提供了一种声明式缓存的解决方案,而Redis作为高性能的缓存数据库,被广泛应用于缓存实现。本文将介绍一种通过自定义注解实现Spring Cache与Redis缓存过期时间管理及自动刷新的策略。
1、自定义注解CacheExpireConfig
为了更灵活地控制缓存的过期时间,我们定义了一个名为CacheExpireConfig
的自定义注解。此注解支持在方法级别配置缓存的过期时间和自动刷新时间。
import java.lang.annotation.*;
/**
* @author tangzx
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface CacheExpireConfig {
/**
* 缓存过期时间,支持单位天(d)、小时(h)、分钟(m)、秒钟(s)(不填单位默认秒)
* 例:2h
*/
String expireTime() default "";
/**
* 缓存过期刷新时间,支持单位天(d)、小时(h)、分钟(m)、秒钟(s)(不填单位默认秒)
* 例:2h
*/
String expireRefreshTime() default "";
}
2、使用注解
在Spring的@Cacheable
注解基础上,通过@CacheExpireConfig
注解,我们可以轻松地为特定方法设置缓存过期和刷新策略。
@Override
@CacheExpireConfig(expireTime = "60s", expireRefreshTime = "30s")
@Cacheable(value = "testCache", condition = "#userId != null && #userName == null ")
public String testCache(String userId, String userName) {
System.out.println("=====================>");
return "success";
}
3、启动时加载缓存过期配置
在Spring Boot应用启动时,通过TaRedisCacheConfigListener
监听器,扫描所有类和方法,加载带有@CacheExpireConfig
注解的方法的缓存过期配置。
import cn.hutool.core.lang.ClassScanner;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.boot.context.event.ApplicationPreparedEvent;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.ApplicationListener;
import java.lang.reflect.Method;
import java.util.Set;
/**
* @author tangzx
* @date 2022/12/17 11:05
*/
public class TaRedisCacheConfigListener implements ApplicationListener<ApplicationPreparedEvent> {
@Override
public void onApplicationEvent(ApplicationPreparedEvent applicationPreparedEvent) {
// 扫描所有类
Set<Class<?>> classes = scanPackage();
for (Class<?> target : classes) {
Method[] methods = target.getMethods();
for (Method method : methods) {
// 如果方法上未同时注解@Cacheable和@CacheExpireConfig,不需要配置
if (!method.isAnnotationPresent(Cacheable.class) || !method.isAnnotationPresent(CacheExpireConfig.class)) {
continue;
}
Cacheable cacheable = method.getAnnotation(Cacheable.class);
CacheExpireConfig cacheExpireConfig = method.getAnnotation(CacheExpireConfig.class);
String expireTime = cacheExpireConfig.expireTime();
String expireRefreshTime = cacheExpireConfig.expireRefreshTime();
String[] cacheNames = ArrayUtils.addAll(cacheable.cacheNames(), cacheable.value());
boolean autoRefresh = cacheExpireConfig.autoRefresh();
for (String cacheName : cacheNames) {
MethodCacheExpireConfig methodCacheExpireConfig = MethodCacheExpireConfig.builder()
.expireTime(DurationUtils.parseDuration(expireTime).getSeconds())
.expireRefreshTime(DurationUtils.parseDuration(expireRefreshTime).getSeconds())
.autoRefresh(autoRefresh)
.target(target)
.method(method)
.build();
TaRedisCacheFactory.addCacheExpireConfig(cacheName, methodCacheExpireConfig);
}
}
}
}
private Set<Class<?>> scanPackage() {
// 使用的hutool的类扫描器,如果项目中未使用工具类,可自行实现
return ClassScanner.scanPackage();
}
}
public static void main(String[] args) {
SpringApplication application = new SpringApplicationBuilder().sources(StartApplication.class).build(args);
try {
application.addListeners(new TaRedisCacheConfigListener());
application.run(args);
} catch (Exception e) {
e.printStackTrace();
}
}
4、重写RedisCacheManager,设置过期时间
通过重写RedisCacheManager
,我们可以根据配置动态设置每个缓存的过期时间。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import java.time.Duration;
import java.util.Map;
/**
* @author Tzx
* @date 2022/12/13 19:33
*/
public class TaRedisCacheManager extends RedisCacheManager {
private static final Logger LOGGER = LoggerFactory.getLogger(TaRedisCacheManager.class);
public TaRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
super(cacheWriter, defaultCacheConfiguration);
}
public TaRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, String... initialCacheNames) {
super(cacheWriter, defaultCacheConfiguration, initialCacheNames);
}
public TaRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, boolean allowInFlightCacheCreation, String... initialCacheNames) {
super(cacheWriter, defaultCacheConfiguration, allowInFlightCacheCreation, initialCacheNames);
}
public TaRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, Map<String, RedisCacheConfiguration> initialCacheConfigurations) {
super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations);
}
public TaRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, Map<String, RedisCacheConfiguration> initialCacheConfigurations, boolean allowInFlightCacheCreation) {
super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations, allowInFlightCacheCreation);
}
@Override
protected RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfig) {
MethodCacheExpireConfig cacheable = TaRedisCacheFactory.getCacheExpireConfig(name);
if (null != cacheable && cacheable.getExpireTime() > 0) {
cacheConfig = entryTtl(name, cacheable.getExpireTime(), cacheConfig);
}
return super.createRedisCache(name, cacheConfig);
}
private RedisCacheConfiguration entryTtl(String cacheName, long ttl, @Nullable RedisCacheConfiguration cacheConfig) {
Assert.notNull(cacheConfig, "RedisCacheConfiguration is required; it must not be null");
cacheConfig = cacheConfig.entryTtl(Duration.ofSeconds(ttl));
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("redisCache {} 过期时间为{}秒", cacheName, ttl);
}
return cacheConfig;
}
}
5、缓存自动刷新
在RedisCache
的get
方法中,如果缓存未过期,检查是否需要进行自动刷新。
@Override
public ValueWrapper get(@Nullable Object o) {
if (null == o) {
return null;
}
ValueWrapper wrapper = this.cache.get(o);
// 刷新缓存
if (null != wrapper) {
SpringContextUtil.getApplicationContext().getBean(TaRedisCacheFactory.class).refreshCache(getName(),o.toString(), this::put);
}
return wrapper;
}
6、TaRedisCacheFactory刷新策略
TaRedisCacheFactory
负责缓存的刷新逻辑,确保缓存数据的实时性。
import com.alibaba.fastjson.JSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.util.MethodInvoker;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* @author tangzx
* @date 2022/12/17 11:09
*/
public class TaRedisCacheFactory {
/**
* 缓存过期配置
*/
private static final ConcurrentHashMap<String, MethodCacheExpireConfig> CACHE_EXPIRE_CONFIG = new ConcurrentHashMap<>();
private static final Logger LOGGER = LoggerFactory.getLogger(TaRedisCacheFactory.class);
public TaRedisCacheFactory() {
// document why this method is empty
}
public static void addCacheExpireConfig(String cacheName, MethodCacheExpireConfig methodCacheExpireConfig) {
CACHE_EXPIRE_CONFIG.put(cacheName, methodCacheExpireConfig);
}
public static MethodCacheExpireConfig getCacheExpireConfig(String cacheName) {
return CACHE_EXPIRE_CONFIG.get(cacheName);
}
/**
* 刷新缓存
*
* @param cacheName 缓存名称
* @param cacheKey 缓存key
*/
public void refreshCache(String cacheName, String cacheKey, RefreshCacheFunction f) {
MethodCacheExpireConfig cacheable = getCacheExpireConfig(cacheName);
if (null == cacheable) {
return;
}
Class<?> targetClass = cacheable.getTarget();
Method method = cacheable.getMethod();
long expireRefreshTime = cacheable.getExpireRefreshTime();
String redisKey = cacheName + cacheKey;
long expire = RedisUtil.KeyOps.getExpire(redisKey);
if (expire > expireRefreshTime) {
return;
}
String argsStr = cacheKey.split("\\^")[1];
Object[] args = JSON.parseObject(argsStr, Object[].class);
if (null == args) {
return;
}
try {
// 创建方法执行器
MethodInvoker methodInvoker = new MethodInvoker();
methodInvoker.setArguments(args);
methodInvoker.setTargetClass(targetClass);
methodInvoker.setTargetMethod(method.getName());
methodInvoker.setTargetObject(AopProxyUtils.getSingletonTarget(SpringContextUtil.getApplicationContext().getBean(targetClass)));
methodInvoker.prepare();
Object invoke = methodInvoker.invoke();
//然后设置进缓存和重新设置过期时间
f.put(cacheKey, invoke);
RedisUtil.KeyOps.expire(cacheKey, cacheable.getExpireTime(), TimeUnit.SECONDS);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException e) {
LOGGER.error("刷新缓存失败:" + e.getMessage(), e);
}
}
}
7、MethodCacheExpireConfig
import lombok.Builder;
import lombok.Data;
import java.lang.reflect.Method;
/**
* @author Tzx
* @date 2022/12/17 11:10
*/
@Data
@Builder
public class MethodCacheExpireConfig {
/**
* 缓存过期时间
*/
private long expireTime;
/**
* 缓存过期自动刷新阈值
*/
private long expireRefreshTime;
/**
* 是否自动刷新
*/
private boolean autoRefresh;
/**
* 类对象
*/
private Class<?> target;
/**
* 缓存方法
*/
private Method method;
}
8、RefreshCacheFunction
/**
* @author tangzx
*/
@FunctionalInterface
public interface RefreshCacheFunction {
/**
* 缓存put
*
* @param key key
* @param value value
*/
void put(String key, Object value);
}
9、DurationUtils
import java.time.Duration;
/**
* @author Tzx
* @date 2022/12/17 12:04
*/
public class DurationUtils {
private DurationUtils(){
// 2022/12/18
}
public static Duration parseDuration(String ttlStr) {
String timeUnit = ttlStr.substring(ttlStr.length() - 1);
switch (timeUnit) {
case "d":
return Duration.ofDays(parseLong(ttlStr));
case "h":
return Duration.ofHours(parseLong(ttlStr));
case "m":
return Duration.ofMinutes(parseLong(ttlStr));
case "s":
return Duration.ofSeconds(parseLong(ttlStr));
default:
return Duration.ofSeconds(Long.parseLong(ttlStr));
}
}
private static long parseLong(String ttlStr) {
return Long.parseLong(ttlStr.substring(0, ttlStr.length() - 1));
}
}