Spring Session provides an API and implementations for managing a user’s session information.
Spring Session makes it trivial to support clustered sessions without being tied to an application container specific solution. It also provides transparent integration with:
Spring Session 提供了管理用户会话信息的API和实现
Spring Session 使支持集群会话变得非常简单,而无需绑定到特定于应用程序容器的解决方案
同时,Spring Session 由 Spring 体系下提供的一种分布式 Session 解决方案,基于它我们可以解决 Session 在分布式场景下 Session 同步的,引入第三方的组件:MongoDB、Redis 等,本文主要会介绍如何运用 Spring Session 来集成 Redis 第三方缓存组件,管理我们本地的 Session 会话,同时,在这之上,作一些我们自定义的扩展实现,方便我们的 Session 管理
Spring Boot 一切的开始要从 @EnableXxx 注解介绍,使用 Spring Session 会需要开启此注解,自动装配一些配置类进来,帮我们完成相关的工作
在这里导入了 Selector 类,实现于 ImportSelector 接口,那么我们只需要关注 > selectImports 方法装配了那些类型即可。
由于 @EnableXxx 注解默认的 proxy-mode 为 JDK 代理实现,所以可以直接看它的 getProxyImports
方法的操作即可。
AutoProxyRegistrar:该类型为我们注入一些 AOP 处理所需要的 BeanDefinition,为后续的 AOP 操作 Cache 作准备工作,@EnableXxx 注解都会为我们作这部分工作,若存在了此 BD,那么就不会再重复注入了。
ProxyCachingConfiguration:此类才是开启 Cache 的核心配置类
@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ProxyCachingConfiguration extends AbstractCachingConfiguration {
@Bean(name = CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public BeanFactoryCacheOperationSourceAdvisor cacheAdvisor(
CacheOperationSource cacheOperationSource, CacheInterceptor cacheInterceptor) {
BeanFactoryCacheOperationSourceAdvisor advisor = new BeanFactoryCacheOperationSourceAdvisor();
advisor.setCacheOperationSource(cacheOperationSource);
advisor.setAdvice(cacheInterceptor);
if (this.enableCaching != null) {
advisor.setOrder(this.enableCaching.<Integer>getNumber("order"));
}
return advisor;
}
// 操作的源注解对象,AnnotationCacheOperationSource#SpringCacheAnnotationParser
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public CacheOperationSource cacheOperationSource() {
return new AnnotationCacheOperationSource();
}
// 基于拦截器我需要去拦截那些注解的操作
// AbstractCachingConfiguration > 此抽象类定义了一套规范:CacheManager-缓存管理器、CacheResolver-缓存解析器、KeyGenerator-Key 生成策略、CacheErrorHandler-缓存错误回调处理器
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public CacheInterceptor cacheInterceptor(CacheOperationSource cacheOperationSource) {
CacheInterceptor interceptor = new CacheInterceptor();
interceptor.configure(this.errorHandler, this.keyGenerator, this.cacheResolver, this.cacheManager);
interceptor.setCacheOperationSource(cacheOperationSource);
return interceptor;
}
}
操作的源注解 > AnnotationCacheOperationSource,在它内部的构造方法会注入一个 SpringCacheAnnotationParser
解析器会处理这四个注解的操作:@Cacheable、@CacheEvict、@CachePut、@Caching
四种注解的用法以及含义在后续会详细介绍
CacheInterceptor 缓存拦截器基于 AOP 实现,当类或方法上增加了上面四个注解其中之一,那么它会进入到此拦截器的 invoke 方法中为我们处理不同的注解缓存对应的操作,具体的操作逻辑会在其父类 > CacheAspectSupport#execute 方法中体现,在这里不作再多源码解析
它会先调用 KeyGenerator 生成 Redis Key,RedisCache -> 会存在一个写操作对象:RedisCacheWriter,由它调用 RedisConnectionFactory#getConnection 方法获取与 Redis 之间的连接后,进行 Redis set、get、del 操作
<dependency>
<groupId>org.springframework.sessiongroupId>
<artifactId>spring-session-data-redisartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
引入 spring-session-data-redis 依赖主要为了配置:spring.session.redis,核心配置文件:RedisSessionProperties
它提供以下配置参数
spring.session.redis.namespace:命名空间 > redis key 前缀,默认值 > spring:session
spring.session.redis.flush-mode:缓存刷新模式,ON_SAVE->保存时才刷新,IMMEDIATE->不刷新
spring.session.redis.save-mode:缓存保存模式,ON_SET_ATTRIBUTE->设置缓存时才保存,ON_GET_ATTRIBUTE->获取缓存时才保存,ALWAYS->读写操作都进行缓存
引入 spring-boot-starter-data-redis 依赖主要为了配置 redis 服务信息,核心配置文件:RedisProperties
它提供以下配置参数
spring.redis.database:操作的是那个数据片,0~15
spring.redis.host:Redis 服务的主机名,默认 localhost
spring.redis.password:Redis 服务的验证密码
spring.redis.port:Redis 服务端口,默认 6379
spring.redis.client-type:支持 Jedis、lettuce
其他更多配置。。
RedisTemplate 由 Spring 提供的,便于操作 Redis 模版工具类,但往往我们要基于自身系统诉求来自定义一些配置,如下:
@Configuration
public class RedisConfig {
@Resource
private RedisConnectionFactory factory;
@Bean
public RedisTemplate<String, Object> redisTemplate() {
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(
Object.class);
ObjectMapper om = new ObjectMapper();
// 非 Null 值才进行注入
om.setSerializationInclusion(JsonInclude.Include.NON_NULL);
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.EVERYTHING);
om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
om.registerModule(new JavaTimeModule());
serializer.setObjectMapper(om);
// 操作 Redis 模版
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.setDefaultSerializer(new StringRedisSerializer());
// 属性注入,其他未设置的属性采用默认的实现
template.afterPropertiesSet();
return template;
}
@Bean
public HashOperations<String, String, Object> hashOperations(
RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForHash();
}
@Bean
public ValueOperations<String, String> valueOperations(
RedisTemplate<String, String> redisTemplate) {
return redisTemplate.opsForValue();
}
@Bean
public ListOperations<String, Object> listOperations(
RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForList();
}
@Bean
public SetOperations<String, Object> setOperations(
RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForSet();
}
@Bean
public ZSetOperations<String, Object> zSetOperations(
RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForZSet();
}
}
主要为了设置 Redis Jackson 序列化策略,空值是否要进行序列化 > JsonInclude.Include.NON_NULL,一般这么设置可以减少无用字段占用 Redis 内存空间
前面我们提到了 KeyGenerator Key 生成器,此类本身就是一种规范,需要由我们自身去实现,Spring 内部默认的实现:SimpleKeyGenerator,但你没有指定 key 属性时,Key 后面会莫名其妙的发现多了一段 > SimpleKey []
,可以追踪其源码,探知如下:
public class SimpleKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
return generateKey(params);
}
/**
* Generate a key based on the specified parameters.
*/
public static Object generateKey(Object... params) {
if (params.length == 0) {
return SimpleKey.EMPTY;
}
if (params.length == 1) {
Object param = params[0];
if (param != null && !param.getClass().isArray()) {
return param;
}
}
// SimpleKey 此类来具体
return new SimpleKey(params);
}
}
SimpleKey 类的 toString 方法
public String toString() {
return getClass().getSimpleName() + " [" + StringUtils.arrayToCommaDelimitedString(this.params) + "]";
}
那么基于此,我们需要自定义 KeyGenerator 来解决此问题,前面我们提及到了 AbstractCachingConfiguration > 此抽象类定义了一套规范:CacheManager-缓存管理器、CacheResolver-缓存解析器、KeyGenerator-Key 生成策略、CacheErrorHandler-缓存错误回调处理器
,以下是此类的注入的 Configurers 方法
@Autowired
void setConfigurers(ObjectProvider<CachingConfigurer> configurers) {
Supplier<CachingConfigurer> configurer = () -> {
List<CachingConfigurer> candidates = configurers.stream().collect(Collectors.toList());
if (CollectionUtils.isEmpty(candidates)) {
return null;
}
if (candidates.size() > 1) {
throw new IllegalStateException(candidates.size() + " implementations of " +
"CachingConfigurer were found when only 1 was expected. " +
"Refactor the configuration such that CachingConfigurer is " +
"implemented only once or not at all.");
}
return candidates.get(0);
};
useCachingConfigurer(new CachingConfigurerSupplier(configurer));
}
protected void useCachingConfigurer(CachingConfigurerSupplier cachingConfigurerSupplier) {
this.cacheManager = cachingConfigurerSupplier.adapt(CachingConfigurer::cacheManager);
this.cacheResolver = cachingConfigurerSupplier.adapt(CachingConfigurer::cacheResolver);
this.keyGenerator = cachingConfigurerSupplier.adapt(CachingConfigurer::keyGenerator);
this.errorHandler = cachingConfigurerSupplier.adapt(CachingConfigurer::errorHandler);
}
观察此源码可以得知,它会注入 CachingConfigurer 类型后把缓存管理器、缓存解析器、Key 生成器、错误回调处理器设值;若想让它使用我们的 Key 生成器策略,需要自定义 Bean 实现此类,自定义如下:
// 当注解指定了 key,那么就会追加,若没有,去除尾部的 SimpleKey[ ]
@Bean
public CachingConfigurer customCachingConfigurer() {
return new CachingConfigurer() {
@Override
public KeyGenerator keyGenerator() {
return (target, method, params) -> {
// 返回后缀名 > 注意,这里不能返回 null,否则会报错
// java.lang.IllegalArgumentException: Null key returned for cache operation (maybe you are using named params on classes without debug info?)
return StringUtils.EMPTY;
};
}
};
}
为何不使用默认的 Redis 缓存管理器 > RedisCacheManager
因为 Redis 缓存支持时效性这一说,在配置中,我们只能指定一个时间,若所有缓存都配置相同的时效,那么就会发生缓存雪崩问题,同一时刻大量的 Key 都失效。。所以我们需要自定义缓存管理器来自己管理每个 Key 它的时效性
Spring Session Redis > 通过此方法 RedisCacheConfiguration#entryTtl
指定缓存的过期时长
基于此,我们可以这么设计,在 Value 后通过 ‘#’ 分割,后面的值就是需要存储的时长,由于时间有秒、分、时、天,可以通过 s、m、h、d 字符来指定使用那种时间格式
// key:vn,指定过期时长 1 秒
@Cacheable(value = "vn#1s")
// key:vn,指定过期时长 1 分
@Cacheable(value = "vn#1m")
// key:vn,指定过期时长 1 小时
@Cacheable(value = "vn#1h")
// key:vn,指定过期时长 1 天
@Cacheable(value = "vn#1d")
设计思路有了,那么就通过编码来实现吧,继承 RedisCacheManager 类,如下:
/**
* @author vnjohn
* @since 2023/6/3
*/
@Slf4j
public class CustomRedisCacheManager extends RedisCacheManager {
public CustomRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
super(cacheWriter, defaultCacheConfiguration);
}
@NotNull
@Override
protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
// Constants.POUND_KEY = #
String poundKey = Constants.POUND_KEY;
if (!name.contains(poundKey)) {
return super.createRedisCache(name, cacheConfig);
}
String[] cacheNameArray = name.split(poundKey);
cacheConfig = cacheTTL(cacheNameArray[1], cacheConfig);
return super.createRedisCache(cacheNameArray[0], cacheConfig);
}
private RedisCacheConfiguration cacheTTL(String ttlStr, @NotNull RedisCacheConfiguration cacheConfig) {
// 根据传参设置缓存失效时间
Duration duration = parseDuration(ttlStr);
return cacheConfig.entryTtl(duration);
}
private Duration parseDuration(String ttl) {
String timeUnit = ttl.substring(ttl.length() - 1);
int timeUnitIndex = ttl.indexOf(timeUnit);
String ttlTime = ttl.substring(0, timeUnitIndex);
switch (timeUnit) {
case "d":
return Duration.ofDays(parseLong(ttlTime));
case "h":
return Duration.ofHours(parseLong(ttlTime));
case "m":
return Duration.ofMinutes(parseLong(ttlTime));
default:
return Duration.ofSeconds(parseLong(ttlTime));
}
}
}
由于 RedisCacheManager 只提供了两个参数的构造方法,参数1:RedisCacheWriter、参数2:RedisCacheConfiguration,所以需要先构造出这个实例对象对应参数,如下:
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(Objects.requireNonNull(redisTemplate.getConnectionFactory()));
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext
.SerializationPair.fromSerializer(redisTemplate.getValueSerializer()))
.computePrefixWith(cacheName -> Constants.REDIS_CACHE_PREFIX + cacheName);
最终将该实现组装起来 > CachingConfigurer,整体如下:
@Bean
public CachingConfigurer customCachingConfigurer(RedisTemplate<String, Object> redisTemplate) {
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(Objects.requireNonNull(redisTemplate.getConnectionFactory()));
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext
.SerializationPair.fromSerializer(redisTemplate.getValueSerializer()))
// Constants.REDIS_CACHE_PREFIX = vnjohn:sms:
.computePrefixWith(cacheName -> Constants.REDIS_CACHE_PREFIX + cacheName);
return new CachingConfigurer() {
@Override
public KeyGenerator keyGenerator() {
return (target, method, params) -> {
// 返回后缀名 > 注意,这里不能返回 null,否则会报错
// java.lang.IllegalArgumentException: Null key returned for cache operation (maybe you are using named params on classes without debug info?)
return StringUtils.EMPTY;
};
}
@Override
public CacheManager cacheManager() {
return new CustomRedisCacheManager(redisCacheWriter, redisCacheConfiguration);
}
};
}
由于 @Cacheable、@CacheEvict、@CachePut,使用 AOP 代理实现的 ,通过创建内部类来代理缓存方法,这样就会导致一个问题,类内部的方法调用类内部的缓存方法不会走代理,就不能正常创建使用缓存
因此,一般使用缓存的方法都会单独出来,放到一个缓存组件类统一处理
用于向缓存中存入值,若缓存中存在此 Key 了,那么标注该注解的方法就不会再次执行,直到 Key 缓存过期
@Cacheable(value = "vn#10s", condition = "#id > 1", unless = "#result.length() > 10", key = "'last'")
public String cache(Long id) {
return "vnjohn" + id;
}
如上方法代表的是缓存 Key->vn,10 秒后过期;但缓存此 Key 有对应两个条件,如下介绍:
condition:对入参的值进行判别是否满足缓存条件,若传入的 id 参数值大于 1 才进行缓存;同时它也可以多个参数以及对象属性进行判断
unless:对出参的结果值进行判别,# result 代表的就是结果,#result.length() 对返回结果的字符长度判断,若条件返回为 false 才进行缓存
key:给缓存 Key 追加后缀,比如 > vnlast
用于驱逐缓存,若缓存中存在此数据,将其进行移除,主要是为了当数据库发生了数据变更以后,为了避免其他地方读取到了脏数据,可以通过此注解进行缓存清除
@CacheEvict(value = "vn", condition = "#id > 1", allEntries = true)
public String cacheEvict(Long id) {
return "vnjohn";
}
同样,它也支持 condition 表达式但不支持 unless 表达式,allEntries 代表是否移除 Key 所有元素,满足条件的才会进行缓存的移除操作
用于缓存更新,将原有缓存的数据进行重新赋值,以此来确保缓存中存在的数据是最新状态的,对比 @Cacheable 注解来看,它是每次都会去更新缓存,而 @Cacheable 只有当缓存不存在时,才会去更新,存在时就不会更新缓存信息了
@CachePut(value = "vn#20s", condition = "#id > 1")
public String cachePut(Long id, String content) {
return content;
}
@Caching 注解是 @Cacheable、@CacheEvict、@CachePut 组合一起来用的,一般不会使用此注解来操作缓存
该篇博文介绍了 Spring Session 集成 Redis 缓存,它是如何自动装配进来的 > 核心类:CacheOperationSource、CacheInterceptor,后面我们自我实现了 Redis 序列化策略、Key 生成策略、自定义缓存管理器,用于对我们的业务作扩展工作,简单介绍了 Redis 核心的一些参数配置,最后,基于自定义扩展配置结合实战简单的使用 @Cacheable、@CacheEvict、@CachePut
此部分源码可以在 GitHub 中获取,主要是为了结合短信网关服务作一些本地缓存的引入,希望能帮助到你
基于设计模式改造短信网关服务实战篇(设计思想、方案呈现、源码)
如果觉得博文不错,关注我 vnjohn,后续会有更多实战、源码、架构干货分享!
推荐专栏:Spring、MySQL,订阅一波不再迷路
大家的「关注❤️ + 点赞 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!