前言
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 管理
@EnableCaching
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.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </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
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
前面我们提到了 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 代理实现的 ,通过创建内部类来代理缓存方法,这样就会导致一个问题,类内部的方法调用类内部的缓存方法不会走代理,就不能正常创建使用缓存
因此,一般使用缓存的方法都会单独出来,放到一个缓存组件类统一处理
@Cacheable
用于向缓存中存入值,若缓存中存在此 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
用于驱逐缓存,若缓存中存在此数据,将其进行移除,主要是为了当数据库发生了数据变更以后,为了避免其他地方读取到了脏数据,可以通过此注解进行缓存清除
@CacheEvict(value = "vn", condition = "#id > 1", allEntries = true) public String cacheEvict(Long id) { return "vnjohn"; }
同样,它也支持 condition 表达式但不支持 unless 表达式,allEntries 代表是否移除 Key 所有元素,满足条件的才会进行缓存的移除操作
@CachePut
用于缓存更新,将原有缓存的数据进行重新赋值,以此来确保缓存中存在的数据是最新状态的,对比 @Cacheable 注解来看,它是每次都会去更新缓存,而 @Cacheable 只有当缓存不存在时,才会去更新,存在时就不会更新缓存信息了
@CachePut(value = "vn#20s", condition = "#id > 1") public String cachePut(Long id, String content) { return content; }
@Caching
@Caching 注解是 @Cacheable、@CacheEvict、@CachePut 组合一起来用的,一般不会使用此注解来操作缓存
总结
该篇博文介绍了 Spring Session 集成 Redis 缓存,它是如何自动装配进来的 > 核心类:CacheOperationSource、CacheInterceptor,后面我们自我实现了 Redis 序列化策略、Key 生成策略、自定义缓存管理器,用于对我们的业务作扩展工作,简单介绍了 Redis 核心的一些参数配置,最后,基于自定义扩展配置结合实战简单的使用 @Cacheable、@CacheEvict、@CachePut
此部分源码可以在 GitHub 中获取,主要是为了结合短信网关服务作一些本地缓存的引入,希望能帮助到你
基于设计模式改造短信网关服务实战篇(设计思想、方案呈现、源码)
如果觉得博文不错,关注我 vnjohn,后续会有更多实战、源码、架构干货分享!
推荐专栏:Spring、MySQL,订阅一波不再迷路
大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!