简述 Spring Session 集成 Redis 底层实现及自定义扩展配置

在这里插入图片描述

  • 前言
  • @EnableCaching
  • 前置准备
  • RedisTemplate
  • KeyGenerator
  • 自定义缓存管理器
  • 实战
    • @Cacheable
    • @CacheEvict
    • @CachePut
    • @Caching
  • 总结

前言

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.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

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);
        }
    };
}

实战

简述 Spring Session 集成 Redis 底层实现及自定义扩展配置_第1张图片

由于 @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,订阅一波不再迷路

大家的「关注❤️ + 点赞 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!

你可能感兴趣的:(Spring,Redis,redis,spring,java)