spring-cache 动态过期时间配置

实现思路

  • 第 1 步
    • 自定义 CacheResolver
    • 通过 CacheOperationInvocationContext 获取到方法的参数名和参数值
    • 重写一个 cacheName(原始cacheName + “#” + 过期时间拼接在一起)
  • 第 2 步
    • 重写 CacheManger 的 getCache 方法,通过第一步拼接的 cacheName split 出原始 cacheName 和过期时间
    • 根据 split 出来的 cacheName 和过期时间,初始化一个 RedisCacheConfiguration,并通过 createRedisCache(cacheName, cacheConfiguration) 实例化一个 cache

实现代码

关键代码

以下代码基于 spring-boot 2.5.2,其他版本可以参考实现

CacheUtils.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 缓存工具
*
* @author YL
*/
public final class CacheUtils {
private static final String SYMBOL = "#";

/**
* 构建一个新的 cacheName,以实现动态过期时间配置
*
* @param cacheName 原始 cacheName
* @param ttl 过期时间
*/
public static String buildCacheNameForTtl(String cacheName, long ttl) {
return cacheName + SYMBOL + ttl;
}

public static String[] splitCacheNameForTtl(String cacheNameForTtl) {
return cacheNameForTtl.split(SYMBOL, -1);
}
}

Expired.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* 自定义缓存过期时间配置注解,过期时间单位:s(秒)
* <pre>
* 1、{@link Expired#el()}优先级比{@link Expired#value()}高,优先使用{@link Expired#el()}的配置
* </pre>
*
* @author YL
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Expired {
/**
* 过期时间,单位:秒,默认:-1(使用全局默认过期时间)
* <pre>
* 与 {@link #el()} 属性互斥,优先使用 {@link #el()} 配置。
* </pre>
*/
long value() default -1;

/**
* 过期时间,单位:秒
* Spring Expression Language (SpEL) expression for computing the expiration time dynamically.
* <pre>
* 与 {@link #value()} 属性互斥,优先使用当前配置。
* </pre>
*/
String el() default "";
}

TCacheResolver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.CacheManager;
import org.springframework.cache.interceptor.AbstractCacheResolver;
import org.springframework.cache.interceptor.CacheOperationInvocationContext;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

/**
* 自定义 {@link org.springframework.cache.interceptor.CacheResolver} 以实现动态过期时间配置
*
* @author YL
*/
@Slf4j
public class TCacheResolver extends AbstractCacheResolver {
private final ExpressionParser parser = new SpelExpressionParser();
private final LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();

public TCacheResolver(CacheManager cacheManager) {
super(cacheManager);
}

@Override
protected Collection<String> getCacheNames(CacheOperationInvocationContext<?> context) {
Method method = context.getMethod();
Object[] args = context.getArgs();
Set<String> cacheNames = context.getOperation().getCacheNames();
// Shortcut if no args need to be loaded
if (ObjectUtils.isEmpty(args)) {
return cacheNames;
}
Expired expired = AnnotationUtils.findAnnotation(method, Expired.class);
if (expired == null) {
return cacheNames;
}
String expiredEl = expired.el();
if (StringUtils.hasText(expiredEl) && expiredEl.startsWith("#")) {
// Expose indexed variables as well as parameter names (if discoverable)
String[] paramNames = this.discoverer.getParameterNames(method);
int paramCount = (paramNames != null ? paramNames.length : method.getParameterCount());
int argsCount = args.length;

EvaluationContext eval = new StandardEvaluationContext();
for (int i = 0; i < paramCount; i++) {
Object value = null;
if (argsCount > paramCount && i == paramCount - 1) {
// Expose remaining arguments as vararg array for last parameter
value = Arrays.copyOfRange(args, i, argsCount);
} else if (argsCount > i) {
// Actual argument found - otherwise left as null
value = args[i];
}
/**
* see {@link MethodBasedEvaluationContext#lazyLoadArguments()}
*/
eval.setVariable("a" + i, value);
eval.setVariable("p" + i, value);
if (paramNames != null) {
eval.setVariable(paramNames[i], value);
}
}
Expression expression = parser.parseExpression(expiredEl);
/**
* 这里可能会抛出异常,不用处理,这样有利于程序发现{@link Expired#el}配置错误问题
*/
Long ttl = expression.getValue(eval, Long.class);
if (ttl == null || ttl <= 0) {
return cacheNames;
}
Set<String> names = new HashSet<>();
for (String cacheName : cacheNames) {
names.add(CacheUtils.buildCacheNameForTtl(cacheName, ttl));
}
return names;
} else {
long expiredValue = expired.value();
if (expiredValue <= 0) {
return cacheNames;
}
Set<String> names = new HashSet<>();
for (String cacheName : cacheNames) {
names.add(CacheUtils.buildCacheNameForTtl(cacheName, expiredValue));
}
return names;
}
}
}

TRedisCacheManager.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;

import java.time.Duration;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
* 自定义 RedisCacheManager 缓存管理器
*
* @author YL
* @since spring-data-redis 2.0.0.RELEASE
*/
@Slf4j
public class TRedisCacheManager extends RedisCacheManager {
@Getter
private final RedisCacheConfiguration defaultCacheConfig;

public TRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfig,
boolean allowInFlightCacheCreation, String... initialCacheNames) {
super(cacheWriter, defaultCacheConfig, allowInFlightCacheCreation, initialCacheNames);
this.defaultCacheConfig = defaultCacheConfig;
}

private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>(16);

@Override
public Cache getCache(String name) {
log.info("getCache ---> name: {}", name);
// Quick check for existing cache...
Cache cache = this.cacheMap.get(name);
if (cache != null) {
return cache;
}
String[] array = CacheUtils.splitCacheNameForTtl(name);
String cacheName = array[0];
cache = super.getCache(cacheName);
if (cache != null && array.length > 1) {
long ttl = Long.parseLong(array[1]);
log.info("getCache ---> cacheName: {}, ttl: {}", cacheName, ttl);
RedisCacheConfiguration cacheConfiguration = this.defaultCacheConfig
.entryTtl(Duration.ofSeconds(ttl));
cache = super.createRedisCache(cacheName, cacheConfiguration);
cacheMap.put(name, cache);
}
return cache;
}
}

TRedisAutoConfiguration.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.cache.interceptor.CacheResolver;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ResourceLoader;
import org.springframework.data.redis.cache.CacheStatisticsCollector;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.util.CollectionUtils;

import java.time.Duration;
import java.util.List;

/**
* 初始化 redis 相关
*
* @author YL
*/
@Configuration
@AutoConfigureAfter(value = {RedisAutoConfiguration.class})
@ConditionalOnClass(value = {RedisConnectionFactory.class, RedisOperations.class})
@ConditionalOnBean(
value = {
RedisConnectionFactory.class
}
)
@EnableConfigurationProperties({RedisProperties.class, CacheProperties.class})
@EnableCaching(proxyTargetClass = true)
@Slf4j
@RequiredArgsConstructor
public class TRedisAutoConfiguration extends CachingConfigurerSupport {
private final CacheProperties cacheProperties;
private final ObjectProvider<RedisCacheConfiguration> redisCacheConfiguration;
private final RedisConnectionFactory redisConnectionFactory;
private final ResourceLoader resourceLoader;

// /**
// * 如果 @Cacheable、@CachePut、@CacheEvict 等注解没有配置 key,则使用这个自定义 key 生成器
// * <pre>
// * 但自定义了缓存的 key 时,难以保证 key 的唯一性,此时最好指定方法名,比如:@Cacheable(value="", key="{#root.methodName, #id}")
// * </pre>
// */
// @Bean
// @Override
// public KeyGenerator keyGenerator() {
// return new TKeyGenerator();
// }
//
// @Bean
// @Override
// public CacheResolver cacheResolver() {
// return new TCacheResolver(cacheManager());
// }
//
// @Bean
// @Override
// public CacheErrorHandler errorHandler() {
// return new TCacheErrorHandler();
// }

/**
* 配置 RedisCacheManager,使用 cache 注解管理 redis 缓存
* <pre>
* 这里一定要加上&#64;{@link Bean}注解
* </pre>
*/
@Bean
@Override
public CacheManager cacheManager() {
RedisCacheConfiguration defaultCacheConfig =
determineConfiguration(cacheProperties, redisCacheConfiguration, resourceLoader.getClassLoader());
// 初始化一个 nonLocking RedisCacheWriter
RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
boolean enableStatistics = cacheProperties.getRedis().isEnableStatistics();
CacheStatisticsCollector statisticsCollector = CacheStatisticsCollector.none();
if (enableStatistics) {
statisticsCollector = CacheStatisticsCollector.create();
}
if (!statisticsCollector.equals(CacheStatisticsCollector.none())) {
cacheWriter = cacheWriter.withStatisticsCollector(statisticsCollector);
}
boolean allowInFlightCacheCreation = true;
List<String> cacheNames = cacheProperties.getCacheNames();
String[] initialCacheNames = new String[]{};
if (!CollectionUtils.isEmpty(cacheNames)) {
initialCacheNames = cacheNames.toArray(new String[0]);
}
TRedisCacheManager cm = new TRedisCacheManager(cacheWriter, defaultCacheConfig, allowInFlightCacheCreation, initialCacheNames);
cm.setTransactionAware(enableStatistics);
return cm;
}

/**
* 以下代码参考 {@link org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration#createConfiguration(CacheProperties, ClassLoader)}
*/
private org.springframework.data.redis.cache.RedisCacheConfiguration determineConfiguration(
CacheProperties cacheProperties,
ObjectProvider<RedisCacheConfiguration> redisCacheConfiguration,
ClassLoader classLoader) {
return redisCacheConfiguration.getIfAvailable(() -> createConfiguration(cacheProperties, classLoader));
}

private RedisCacheConfiguration createConfiguration(CacheProperties cacheProperties, ClassLoader classLoader) {
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
config = config.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(classLoader)));
// config = config.serializeKeysWith(STRING_PAIR)
// .serializeValuesWith(FASTJSON_PAIR);
Duration timeToLive = redisProperties.getTimeToLive();
if (timeToLive == null) {
// 因为未配置 spring.cache.redis.time-to-live,将其设置为 1 小时。
timeToLive = Duration.ofHours(1);
log.warn("Because spring.cache.redis.time-to-live is not configured, set it to 1 hour.");
}
config = config.entryTtl(timeToLive);
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixCacheNameWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}

测试用例

TRedisApp.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;

/**
* @author YL
*/
@SpringBootApplication(
exclude = {
DataSourceAutoConfiguration.class,
HibernateJpaAutoConfiguration.class,
JdbcTemplateAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class
}
)
public class TRedisApp {

public static void main(String[] args) {
SpringApplication.run(TRedisApp.class, args);
synchronized (TRedisApp.class) {
while (true) {
try {
TRedisApp.class.wait();
} catch (Exception e) {
// ignore
break;
}
}
}
}
}

TRedisService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

/**
* @author YL
*/
@Service
@Slf4j
@CacheConfig(cacheNames = "redis.v1.service")
public class TRedisService {

@Cacheable(
value = "redis.v2.service",
condition = "#cache eq true",
unless = "#result == null or #result.empty")
@Caching(
cacheable = {
@Cacheable(
value = "redis.v3.service",
condition = "#cache eq true",
unless = "#result == null or #result.empty"),
@Cacheable(
value = "redis.v4.service",
condition = "#cache eq true",
unless = "#result == null or #result.empty")
}
)
@Expired(el = "#expirationTime")
// @Expired(value = 300, el = "#id")
public Map<String, Object> get(boolean cache, long expirationTime, int id) {
Map<String, Object> map = new HashMap<>();
map.put("id", id);
map.put("name", "name-" + id);

log.info("map: {}", map);
return map;
}
}

TRedisTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SetOperations;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

/**
* @author YL
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = TRedisApp.class)
@ActiveProfiles("redis")
@Slf4j
public class TRedisTest {
@Autowired
private TRedisService redisService;
@Autowired
private RedisConnectionFactory redisConnectionFactory;

@Test
public void testString() {
log.info("redisConnectionFactory: {}", redisConnectionFactory);
if (redisConnectionFactory instanceof LettuceConnectionFactory) {
LettuceConnectionFactory factory = (LettuceConnectionFactory) redisConnectionFactory;
log.info("spring.redis.database: {}", factory.getDatabase());
log.info("spring.redis.host: {}", factory.getHostName());
log.info("spring.redis.port: {}", factory.getPort());
log.info("spring.redis.timeout: {}", factory.getTimeout());
log.info("spring.redis.password: {}", factory.getPassword());
}
for (int i = 1; i <= 2; i++) {
log.info("service get ---> {}", redisService.get(true, 120 * i, i + 1000));
}
for (int i = 1; i <= 2; i++) {
log.info("service get ---> {}", redisService.get(true, 120 * i, i + 1000));
}
}
}
  • 本文作者: forever杨
  • 本文链接: https://blog.yl-online.top/posts/23adfb31.html
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。如果文章内容对你有用,请记录到你的笔记中。本博客站点随时会停止服务,请不要收藏、转载!