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

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

前言

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

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

目录
相关文章
|
人工智能 Java Serverless
【MCP教程系列】搭建基于 Spring AI 的 SSE 模式 MCP 服务并自定义部署至阿里云百炼
本文详细介绍了如何基于Spring AI搭建支持SSE模式的MCP服务,并成功集成至阿里云百炼大模型平台。通过四个步骤实现从零到Agent的构建,包括项目创建、工具开发、服务测试与部署。文章还提供了具体代码示例和操作截图,帮助读者快速上手。最终,将自定义SSE MCP服务集成到百炼平台,完成智能体应用的创建与测试。适合希望了解SSE实时交互及大模型集成的开发者参考。
15316 60
|
NoSQL 安全 Java
深入理解 RedisConnectionFactory:Spring Data Redis 的核心组件
在 Spring Data Redis 中,`RedisConnectionFactory` 是核心组件,负责创建和管理与 Redis 的连接。它支持单机、集群及哨兵等多种模式,为上层组件(如 `RedisTemplate`)提供连接抽象。Spring 提供了 Lettuce 和 Jedis 两种主要实现,其中 Lettuce 因其线程安全和高性能特性被广泛推荐。通过手动配置或 Spring Boot 自动化配置,开发者可轻松集成 Redis,提升应用性能与扩展性。本文深入解析其作用、实现方式及常见问题解决方法,助你高效使用 Redis。
1394 4
|
9月前
|
NoSQL Java 调度
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
分布式锁是分布式系统中用于同步多节点访问共享资源的机制,防止并发操作带来的冲突。本文介绍了基于Spring Boot和Redis实现分布式锁的技术方案,涵盖锁的获取与释放、Redis配置、服务调度及多实例运行等内容,通过Docker Compose搭建环境,验证了锁的有效性与互斥特性。
822 0
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
|
9月前
|
监控 安全 Java
使用 @HealthEndpoint 在 Spring Boot 中实现自定义健康检查
Spring Boot 通过 Actuator 模块提供了强大的健康检查功能,帮助开发者快速了解应用程序的运行状态。默认健康检查可检测数据库连接、依赖服务、资源可用性等,但在实际应用中,业务需求和依赖关系各不相同,因此需要实现自定义健康检查来更精确地监控关键组件。本文介绍了如何使用 @HealthEndpoint 注解及实现 HealthIndicator 接口来扩展 Spring Boot 的健康检查功能,从而提升系统的可观测性与稳定性。
673 0
使用 @HealthEndpoint 在 Spring Boot 中实现自定义健康检查
|
11月前
|
NoSQL Java Redis
Redis基本数据类型及Spring Data Redis应用
Redis 是开源高性能键值对数据库,支持 String、Hash、List、Set、Sorted Set 等数据结构,适用于缓存、消息队列、排行榜等场景。具备高性能、原子操作及丰富功能,是分布式系统核心组件。
813 2
|
消息中间件 缓存 NoSQL
基于Spring Data Redis与RabbitMQ实现字符串缓存和计数功能(数据同步)
总的来说,借助Spring Data Redis和RabbitMQ,我们可以轻松实现字符串缓存和计数的功能。而关键的部分不过是一些"厨房的套路",一旦你掌握了这些套路,那么你就像厨师一样可以准备出一道道饕餮美食了。通过这种方式促进数据处理效率无疑将大大提高我们的生产力。
388 32
|
Java Maven Docker
gitlab-ci 集成 k3s 部署spring boot 应用
gitlab-ci 集成 k3s 部署spring boot 应用
|
消息中间件 监控 Java
您是否已集成 Spring Boot 与 ActiveMQ?
您是否已集成 Spring Boot 与 ActiveMQ?
588 0
|
监控 druid Java
spring boot 集成配置阿里 Druid监控配置
spring boot 集成配置阿里 Druid监控配置
1658 6
|
Java 关系型数据库 MySQL
如何实现Springboot+camunda+mysql的集成
【7月更文挑战第2天】集成Spring Boot、Camunda和MySQL的简要步骤: 1. 初始化Spring Boot项目,添加Camunda和MySQL驱动依赖。 2. 配置`application.properties`,包括数据库URL、用户名和密码。 3. 设置Camunda引擎属性,指定数据源。 4. 引入流程定义文件(如`.bpmn`)。 5. 创建服务处理流程操作,创建控制器接收请求。 6. Camunda自动在数据库创建表结构。 7. 启动应用,测试流程启动,如通过服务和控制器开始流程实例。 示例代码包括服务类启动流程实例及控制器接口。实际集成需按业务需求调整。
1260 4