一、说明
在SpringBoot项目开发过程中,我们经常会有用到缓存的需求,这时候整合使用Redis无疑是很好的选择。SpringBoot官方就有很多整合NoSQL的starter库,方便我们集成开发。值得一提的是,SpringBoot版本1.+和2.+关于配置Redis有比较大的区别,网络上的很多旧版的资料都已不适用于高版本的SpringBoot,因此本文记录了基于
SpringBoot2.2.8Release
版本的整合。
二、整合配置
(一)添加pom依赖
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>
</dependencies>
(二)新建Redis配置类
说明:
- redisTemplate:最基础的组件,重写它主要是为了自定义序列化策略
- cacheManager:SpringBoot缓存体系中的缓存管理器
- redisMessageListenerContainer:Redis消息监听器,能实现消息订阅、过期通知等
1. RedisConfiguration
@EnableCaching
@Configuration
public class RedisConfiguration {/** 默认的java8日期时间格式 */private static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";/** 默认的java8日期格式 */private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";/** 默认的java8时间格式 */private static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";/*** Redis访问模板*/@Bean@SuppressWarnings("all")public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();template.setConnectionFactory(factory);StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();// key都采用String的序列化方式template.setKeySerializer(stringRedisSerializer);template.setHashKeySerializer(stringRedisSerializer);// value的序列化方式采用jacksonJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);// 解决查询缓存转换异常的问题ObjectMapper objectMapper = new ObjectMapper();objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL);// 添加java8时间相关序列化/反序列化处理JavaTimeModule javaTimeModule = new JavaTimeModule();javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));objectMapper.registerModule(javaTimeModule);jackson2JsonRedisSerializer.setObjectMapper(objectMapper);jackson2JsonRedisSerializer.setObjectMapper(objectMapper);template.setValueSerializer(jackson2JsonRedisSerializer);template.setHashValueSerializer(jackson2JsonRedisSerializer);template.afterPropertiesSet();return template;}/*** 缓存管理器*/@Beanpublic CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory, RedisTemplate redisTemplate) {// 配置默认过期时间和序列化机制RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(60L)).serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getValueSerializer()));// 自定义缓存空间应用不同配置,可以配置不同的过期时间Set<String> cacheNames = new HashSet<>();Map<String, RedisCacheConfiguration> configMap = new HashMap<>(RedisCacheSpaceExpireConfig.EXPIRE_TIME_MAP.size());for (Map.Entry<String, Integer> entry : RedisCacheSpaceExpireConfig.EXPIRE_TIME_MAP.entrySet()) {String cacheName = entry.getKey();Integer expireMinute = entry.getValue();cacheNames.add(cacheName);configMap.put(cacheName, defaultCacheConfig.entryTtl(Duration.ofMinutes(expireMinute)));}return RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(defaultCacheConfig).initialCacheNames(cacheNames).withInitialCacheConfigurations(configMap).build();}/*** 消息监听器*/@BeanRedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) {RedisMessageListenerContainer container = new RedisMessageListenerContainer();container.setConnectionFactory(connectionFactory);return container;}}
2. RedisCacheSpaceExpireConfig(缓存空间过期时间配置类)
public class RedisCacheSpaceExpireConfig {public static final String MINUTE_1 = "MINUTE_1";public static final String MINUTE_3 = "MINUTE_3";public static final String MINUTE_5 = "MINUTE_5";public static final String MINUTE_15 = "MINUTE_10";public static final String MINUTE_30 = "MINUTE_30";public static final String HOUR_1 = "HOUR_1";public static final String HOUR_2 = "HOUR_2";public static final String HOUR_6 = "HOUR_6";public static final String HOUR_12 = "HOUR_12";public static final String DAY_1 = "DAY_1";public static final String DAY_3 = "DAY_3";public static final String DAY_10 = "DAY_10";public static final String DAY_30 = "DAY_30";/*** 过期时间键值对 k-v : 名称-过期时间(分钟)*/public static final HashMap<String, Integer> EXPIRE_TIME_MAP = new HashMap<String, Integer>(){{put(MINUTE_1, 1);put(MINUTE_3, 3);put(MINUTE_5, 5);put(MINUTE_15, 15);put(MINUTE_30, 30);put(HOUR_1, 60);put(HOUR_2, 2 * 60);put(HOUR_6, 6 * 60);put(HOUR_12, 12 * 60);put(DAY_1, 60 * 24);put(DAY_3, 60 * 24 * 3);put(DAY_10, 60 * 24 * 10);put(DAY_30, 60 * 24 * 30);}};}
三、使用示范
(一)简单读写
@RestController
@RequestMapping("demo")
public class DemoController {@Resourceprivate RedisTemplate redisTemplate;@PostMapping("/simple-string")public String testExpire(String message) {redisTemplate.opsForValue().set("demo", message);return "success";}@GetMapping("/simple-string")public String testExpire(String message) {redisTemplate.opsForValue().set("demo", message);return "success";}}
(二)缓存
以Mybatis的Mapper方法为例:Member
类为实力类,读者视自己情况修改。RedisCacheSpaceExpireConfig
为前文配置缓存空间过期时间的配置类。
SpringBoot的缓存除了加载DAO层上,还能加载各种层的方法上,视业务情况使用即可。
/*** 根据主键ID和商家号查找会员:联合主键进行缓存,缓存时长为5分钟** @param id 主键* @param merchantId 商家号* @return 会员*/@Cacheable(cacheNames = RedisCacheSpaceExpireConfig.MINUTE_5, key = "'member:'.concat(#merchantId).concat(#id)")Member selectByIdAndMerchantId(@Param("id") Long id, @Param("merchantId") Long merchantId);/*** 根据主键ID和商家号修改会员:删除联合主键缓存内容** @param id 主键* @param merchantId 商家号* @return 会员*/@CacheEvict(cacheNames = RedisCacheSpaceExpireConfig.MINUTE_5, key = "'member:'.concat(#merchantId).concat(#id)")void updateByIdAndMerchantId(@Param("id") Long id, @Param("merchantId") Long merchantId, @Param("member") Member member);
说明:关于SpringBoot的缓存,本文不拓展讲,感兴趣的读者可自行查阅相关资料。缓存注解中的key支持SPEL表达式:
(三)过期事件监听
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {super(listenerContainer);}@Resourceprivate RedisTemplate redisTemplate;/*** 针对redis数据失效事件,进行数据处理** @param message redis数据的key值* @param pattern */@Overridepublic void onMessage(Message message, byte[] pattern) {String redisKey = message.toString();// 取出我们自定义的业务标识前缀,可以使用枚举类辅助String prefixStr = redisKey.substring(0, redisKey.indexOf(":"));if (StrUtil.isBlank(prefixStr)) {return;}// 以不同的锁Key作为redis锁,并设置过期时间3分钟,确保多节点并发时仅执行一次任务,且锁会自动释放if (redisTemplate.opsForValue().setIfAbsent("lock:" + redisKey, redisKey, 3, TimeUnit.MINUTES)) {// 业务信息:可以是表ID、数据编码等String businessKey = redisKey.substring(prefixStr.length() + 1);switch (prefixStr) {case "order":System.out.println("处理订单,订单号:" + businessKey );break;case "stock":System.out.println("处理库存,仓库号:" + businessKey );break;default:break;}// 释放redis锁redisTemplate.delete(redisKey);}}
}