目录
- 说明
- 实现思路
- 实现步骤
- 创建项目添加依赖
- 创建自定义缓存管理器
- 定义redis配置
- redis 缓存值格式序列化
- redis 操作方法(可省略)
- 使用
- spring cache 缓存注解
- @Cacheable
- 说明
- 参数
- value 或者 cacheNames
- 描述
- 类型
- 示例
- key
- 描述
- 类型
- 示例
- keyGenerator
- 描述
- 类型
- 示例
- condition
- 描述
- 类型
- 示例
- unless
- 描述
- 类型
- 示例
- cacheManager
- 描述
- 类型
- 示例
- cacheResolver
- 描述
- 类型
- 示例:
- sync
- 描述
- 类型
- 默认值
- 示例:
说明
spring cache 缓存自带了很多缓存注解,但是,这些注解并没有对key进行设置过期时间的参数。当我们需要为某些key设置过期时间时,使用spring cache 的注解并不能满足我们的需求。此时我们需要自定义缓存管理器来实现此需求,满足我们使用spring cache 注解的同时能为指定的key加上过期时间。
此文的缓存中间件使用的是redis.
实现思路
@Cacheable 注解在使用时传递的value属性为缓存的名称,我们可以将时间拼接到这个名称中,通过自定义的缓存管理器来实现分割截取时间和名称,截取到的时间则为key的缓存过期TTL的值。
例如:
user:get@25_s
缓存键名为: user:get
TTL值为:25
TTL值的单位:s 秒
缓存键名和TTL值分割符号:@
TTL值和TTL值的单位分割符号:_
实现步骤
创建项目添加依赖
省创建springboot 项目,以下为关键依赖
<!-- Redisson 锁功能 --><dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>${redisson-spring-boot-starter.version}</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId><version>${spring-boot-starter-cache.version}</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><version>${spring-boot-starter-data-redis.version}</version></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId><version>${commons-pool2.version}</version></dependency><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>${fastjson2.version}</version></dependency>
创建自定义缓存管理器
自定义缓存管理器名称:ThreeCacheManager
import org.springframework.data.redis.cache.*;
import org.springframework.util.StringUtils;
import java.time.Duration;/*** 说明:* 自定义 缓存管理器* 支持再缓存名称中设置 过期时间* 格式: 缓存名称@时间值_单位* 单位支持:(单位区分大小写)* y:年* M:月* d:日* h:小时* m:分* s:秒,默认* S :毫秒** @author 张小三* @create 2024-12-05 11:46* @verson 1.0.0*/
public class ThreeCacheManager extends RedisCacheManager {private final static String namePrefix = "three";/*** 说明: 自定义缓存时分割符号** @author zhangxiaosan* @create 2024/12/5* @param* @return*/private final static String cacheNameTTLSplit = "@";/*** 说明: 重写缓存创建逻辑** @param* @return* @author zhangxiaosan* @create 2024/12/5*/@Overrideprotected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfiguration) {// 使用分割符号分割缓存名称和过期时间String[] split = name.split(cacheNameTTLSplit);if (split.length <= 1) {// 返回默认缓存管理器创建的缓存,没有过期时间return super.createRedisCache(name, cacheConfiguration);}String cacheName = split[0]; //第1部分为缓存名String ttlValue = split[1]; //第2部分为缓存过期时间if (!StringUtils.hasText(ttlValue)) {// 返回默认缓存管理器创建的缓存,有分割符号但是没有过期时间return super.createRedisCache(name, cacheConfiguration);}String[] ttl_type = ttlValue.split("_");if (ttl_type.length <= 1) {// 时间和单位分割符不存在,则返回默认缓存管理器创建的缓存return super.createRedisCache(name, cacheConfiguration);}String ttl = ttl_type[0];if (!StringUtils.hasText(ttl)) {// 时间不存在,则返回默认缓存管理器创建的缓存return super.createRedisCache(name, cacheConfiguration);}Duration duration = null;String type = ttl_type[1];// 时间单位if (!StringUtils.hasText(type)) {// 时间和单位分割符不存在,则默认为秒duration = Duration.ofSeconds(Long.parseLong(ttl));}switch (type) {case "y": // 年duration = Duration.ofDays(Long.parseLong(ttl) * 365);break;case "M": // 月duration = Duration.ofDays(Long.parseLong(ttl) * 30);break;case "d": // 日duration = Duration.ofDays(Long.parseLong(ttl));break;case "h": // 小时duration = Duration.ofHours(Long.parseLong(ttl));break;case "m": // 分钟duration = Duration.ofMinutes(Long.parseLong(ttl));break;case "S": // 毫秒duration = Duration.ofMillis(Long.parseLong(ttl));break;default: // 默认。秒duration = Duration.ofSeconds(Long.parseLong(ttl));break;}// 配置缓存cacheConfiguration = cacheConfiguration.computePrefixWith(item -> namePrefix+":"+cacheName + ":").entryTtl(duration);return super.createRedisCache(name, cacheConfiguration);}/*** 说明: 自定义缓存管理器的构造器** @param* @return* @author zhangxiaosan* @create 2024/12/5*/public ThreeCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {super(cacheWriter, defaultCacheConfiguration);}
}
定义redis配置
import io.lettuce.core.ClientOptions;
import io.lettuce.core.SocketOptions;
import io.lettuce.core.TimeoutOptions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurer;
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.cache.interceptor.SimpleCacheResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;@Configuration
@EnableCaching
@AutoConfigureBefore(RedisAutoConfiguration.class)
public class RedisConfig implements CachingConfigurer {@Autowiredprivate RedisProperties redisProperties;@Bean@SuppressWarnings(value = { "unchecked", "rawtypes" })public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {RedisTemplate<Object, Object> template = new RedisTemplate<>();template.setConnectionFactory(connectionFactory);FastJson2JsonRedisSerializer<Object> serializer = new FastJson2JsonRedisSerializer<>(Object.class);// 使用StringRedisSerializer来序列化和反序列化redis的key值template.setKeySerializer(new StringRedisSerializer());template.setValueSerializer(serializer);// Hash的key也采用StringRedisSerializer的序列化方式template.setHashKeySerializer(new StringRedisSerializer());template.setHashValueSerializer(serializer);template.afterPropertiesSet();return template;}@Overridepublic CacheManager cacheManager() {RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues() //禁用缓存空值//.entryTtl(Duration.ofHours(1)) // 设置缓存过期时间为1小时.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new FastJson2JsonRedisSerializer<>(Object.class)));/*return RedisCacheManager.builder(redisConnectionFactory()).cacheDefaults(config).build();*/RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory());return new ThreeCacheManager(redisCacheWriter,config);}@Overridepublic KeyGenerator keyGenerator() {return (target, method, params) -> {StringBuilder sb = new StringBuilder();sb.append(target.getClass().getName());sb.append("#");sb.append(method.getName());for (Object param : params) {sb.append(param.toString());}return sb.toString();};}@Overridepublic CacheResolver cacheResolver() {return new SimpleCacheResolver(cacheManager());}@Overridepublic CacheErrorHandler errorHandler() {return new CacheErrorHandler() {@Overridepublic void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {// 处理获取缓存错误throw new RuntimeException("redis 获取缓存异常,key:"+key,exception);}@Overridepublic void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) {// 处理放入缓存错误throw new RuntimeException("redis 放入缓存异常,key:"+key,exception);}@Overridepublic void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {// 处理清除缓存错误throw new RuntimeException("redis 清除缓存异常,key:"+key,exception);}@Overridepublic void handleCacheClearError(RuntimeException exception, Cache cache) {// 处理清空缓存错误throw new RuntimeException("redis 清空缓存异",exception);}};}@Beanpublic RedisConnectionFactory redisConnectionFactory() {RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration();redisConfig.setHostName(redisProperties.getHost());redisConfig.setPort(redisProperties.getPort());redisConfig.setPassword(redisProperties.getPassword());redisConfig.setDatabase(redisProperties.getDatabase());// 创建 LettuceClientConfigurationLettuceClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder().clientOptions(ClientOptions.builder().socketOptions(SocketOptions.builder().connectTimeout(Duration.ofMillis(redisProperties.getTimeout().toMillis())).build()).timeoutOptions(TimeoutOptions.enabled()).build()).build();// 创建 LettuceConnectionFactoryLettuceConnectionFactory factory = new LettuceConnectionFactory(redisConfig, clientConfig);factory.afterPropertiesSet();return factory;}
}
redis 缓存值格式序列化
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import java.nio.charset.Charset;public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> {public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");private Class<T> clazz;public FastJson2JsonRedisSerializer(Class<T> clazz) {super();this.clazz = clazz;}@Overridepublic byte[] serialize(T t) throws SerializationException {if (t == null) {return new byte[0];}return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET);}@Overridepublic T deserialize(byte[] bytes) throws SerializationException {if (bytes == null || bytes.length <= 0) {return null;}String str = new String(bytes, DEFAULT_CHARSET);return JSON.parseObject(str, clazz, JSONReader.Feature.SupportAutoType);}
}
redis 操作方法(可省略)
package www.three.components.redis.service;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;import java.util.*;
import java.util.concurrent.TimeUnit;/*** 说明:* redis的操作类* @author 张小三* @create 2024-10-21 14:08* @verson 1.0.0*/
@SuppressWarnings(value = {"unchecked", "rawtypes"})
@Component
public class RedisService {@Autowiredpublic RedisTemplate redisTemplate;/*** 缓存基本的对象,Integer、String、实体类等** @param key 缓存的键值* @param value 缓存的值*/public <T> void setCacheObject(final String key, final T value) {redisTemplate.opsForValue().set(key, value);}/*** 缓存基本的对象,Integer、String、实体类等** @param key 缓存的键值* @param value 缓存的值* @param timeout 时间* @param timeUnit 时间颗粒度*/public <T> void setCacheObject(final String key, final T value, final Long timeout, final TimeUnit timeUnit) {redisTemplate.opsForValue().set(key, value, timeout, timeUnit);}/*** 设置有效时间** @param key Redis键* @param timeout 超时时间* @return true=设置成功;false=设置失败*/public boolean expire(final String key, final long timeout) {return expire(key, timeout, TimeUnit.SECONDS);}/*** 设置有效时间** @param key Redis键* @param timeout 超时时间* @param unit 时间单位* @return true=设置成功;false=设置失败*/public boolean expire(final String key, final long timeout, final TimeUnit unit) {return redisTemplate.expire(key, timeout, unit);}/*** 获取有效时间** @param key Redis键* @return 有效时间*/public long getExpire(final String key) {return redisTemplate.getExpire(key);}/*** 判断 key是否存在** @param key 键* @return true 存在 false不存在*/public Boolean hasKey(String key) {return redisTemplate.hasKey(key);}/*** 获得缓存的基本对象。** @param key 缓存键值* @return 缓存键值对应的数据*/public <T> T getCacheObject(final String key) {ValueOperations<String, T> operation = redisTemplate.opsForValue();return operation.get(key);}/*** 删除单个对象** @param key*/public boolean deleteObject(final String key) {return redisTemplate.delete(key);}/*** 删除集合对象** @param collection 多个对象* @return*/public long deleteObject(final Collection collection) {return redisTemplate.delete(collection);}/*** 缓存List数据** @param key 缓存的键值* @param dataList 待缓存的List数据* @return 缓存的对象*/public <T> long setCacheList(final String key, final List<T> dataList) {Long count = redisTemplate.opsForList().rightPushAll(key, dataList);return count == null ? 0 : count;}/*** 获得缓存的list对象** @param key 缓存的键值* @return 缓存键值对应的数据*/public <T> List<T> getCacheList(final String key) {return redisTemplate.opsForList().range(key, 0, -1);}/*** 缓存Set** @param key 缓存键值* @param dataSet 缓存的数据* @return 缓存数据的对象*/public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) {BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);Iterator<T> it = dataSet.iterator();while (it.hasNext()) {setOperation.add(it.next());}return setOperation;}/*** 获得缓存的set** @param key* @return*/public <T> Set<T> getCacheSet(final String key) {return redisTemplate.opsForSet().members(key);}/*** 缓存Map** @param key* @param dataMap*/public <T> void setCacheMap(final String key, final Map<String, T> dataMap) {if (dataMap != null) {redisTemplate.opsForHash().putAll(key, dataMap);}}/*** 获得缓存的Map** @param key* @return*/public <T> Map<String, T> getCacheMap(final String key) {return redisTemplate.opsForHash().entries(key);}/*** 往Hash中存入数据** @param key Redis键* @param hKey Hash键* @param value 值*/public <T> void setCacheMapValue(final String key, final String hKey, final T value) {redisTemplate.opsForHash().put(key, hKey, value);}/*** 获取Hash中的数据** @param key Redis键* @param hKey Hash键* @return Hash中的对象*/public <T> T getCacheMapValue(final String key, final String hKey) {HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();return opsForHash.get(key, hKey);}/*** 获取多个Hash中的数据** @param key Redis键* @param hKeys Hash键集合* @return Hash对象集合*/public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) {return redisTemplate.opsForHash().multiGet(key, hKeys);}/*** 获得缓存的基本对象列表** @param pattern 字符串前缀* @return 对象列表*/public Collection<String> keys(final String pattern) {return redisTemplate.keys(pattern);}
}
使用
在 service 的实现方法中使用
// 键名为user:get的数据缓存25秒,25秒过后自动从redis中删除
@Cacheable(value = "user:get@25_s", key = "#param.id",unless = "#result==null")
public User getUserById(User param) { ... }
通过客户端查询,验证结果
spring cache 缓存注解
@Cacheable
说明
用于标记一个方法,表示该方法的结果可以被缓存。Spring 会检查缓存中是否已有该方法的结果,如果有,就直接返回缓存中的数据,否则执行方法并将结果存入缓存
参数
value 或者 cacheNames
描述
指定缓存的名称。一个或多个缓存名称,可以指定多个缓存区域来存储数据。
类型
String[] 或者String
示例
@Cacheable(value = "users")
public User getUserById(Long userId) { ... }
key
描述
指定缓存的键。通常是一个 SpEL(Spring Expression Language)表达式,用来动态生成缓存的键。
类型
String
示例
@Cacheable(value = "users", key = "#userId")
public User getUserById(Long userId) { ... }
keyGenerator
描述
指定自定义的 KeyGenerator 来生成缓存键。如果设置了 key,keyGenerator 会被忽略。
类型
String
示例
@Cacheable(value = "users", keyGenerator = "customKeyGenerator")
public User getUserById(Long userId) { ... }
condition
描述
SpEL 表达式,只有在满足某个条件时,才会将方法的返回值缓存。如果条件为 false,缓存将不会存储该值。
类型
String
示例
@Cacheable(value = "users", condition = "#userId > 100")
public User getUserById(Long userId) { ... }
unless
描述
SpEL 表达式,用于排除某些缓存的情况。当表达式为 true 时,缓存不会存储返回值。
类型
String
示例
@Cacheable(value = "users", unless = "#result == null")
public User getUserById(Long userId) { ... }
cacheManager
描述
指定使用的 CacheManager,可以通过不同的缓存管理器管理缓存。
类型
String
示例
@Cacheable(value = "users", cacheManager = "customCacheManager")
public User getUserById(Long userId) { ... }
cacheResolver
描述
指定自定义的 CacheResolver,用于决定使用哪个缓存。通常在复杂的缓存场景中使用。
类型
String
示例:
@Cacheable(value = "users", cacheResolver = "customCacheResolver")
public User getUserById(Long userId) { ... }
sync
描述
指定是否启用同步缓存行为。在多个线程并发访问相同缓存时,启用同步会确保只有一个线程可以执行该方法,其他线程等待方法执行完成后再返回结果。
类型
boolean
默认值
false
示例:
@Cacheable(value = "users", sync = true)public User getUserById(Long userId) { ... }