一、说明
在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的序列化方式采用jackson
Jackson2JsonRedisSerializer 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;
}
/**
* 缓存管理器
*/
@Bean
public 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();
}
/**
* 消息监听器
*/
@Bean
RedisMessageListenerContainer 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 {
@Resource
private 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);
}
@Resource
private RedisTemplate redisTemplate;
/**
* 针对redis数据失效事件,进行数据处理
*
* @param message redis数据的key值
* @param pattern
*/
@Override
public 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);
}
}
}