Skip to content

Extend Spring Cache to implement individual caching for each item in batch data, which can improve cache hit rate and utilization. Additionally, this extension allows the method for operating on batch data to share cache with the method for operating on individual data.扩展 Spring Cache 以实现批量数据中每项的单独缓存,从而提升缓存的命中率和使用率。

License

Notifications You must be signed in to change notification settings

ms100/cache-as-multi

Repository files navigation

CacheAsMulti

In English

安装

Maven

<dependency>
   <groupId>io.github.ms100</groupId>
   <artifactId>cache-as-multi</artifactId>
   <version>1.3.1</version>
</dependency>

最近更新

v1.3

若批量方法返回的 List 不能保证与【对象集合参数】大小相同并顺序一致时,可以使用 @CacheAsMulti.asElementField 指定【对象集合参数】的元素在 List 的元素中所在的字段。这将更适合数据库查询。

class CarService {
   @Cacheable(cacheNames = "car")
   @CacheResult(cacheName = "car")
   public List<CarPO> findCars(@CacheAsMulti(asElementField = "info.id") List<Integer> ids) {
      // 返回值 List 的大小不必与 ids 相同
   }

   public static class CarPO {
      private CarInfoPO info;
      private String name;
   }

   public static class CarInfoPO {
      private Integer id;
   }
}

使用

本注解需要与下面两套注解搭配使用,以实现对被注解参数所在的方法进行批量的缓存操作。

  • Spring的缓存注解 @Cacheable@CachePut@CacheEvict

  • JSR-107的注解 @CacheResult、JSR-107的@CachePut@CacheRemove@CacheKey

只支持 PROXY 模式,不支持 ASPECTJ 模式

@Cacheable 和 @CacheResult

普通方法

假设已有获取单个对象的方法,如下:

class FooService {
    public Foo getFoo(Integer fooId) {
      //...
    }
}

此时如果需要获取批量对象的方法,通常会是下面两种写法:

class FooService {
    public Map<Integer, Foo> getMultiFoo(Collection<Integer> fooIds) {
       //...
    }

   public List<Foo> getMultiFoo(List<Integer> fooIds) {
      //...
   }
}

获取批量对象的方法相对于获取单个对象的方法会有两点变化:

  1. 入参从单个对象(以下称【对象参数】)变为对象集合(以下称【对象集合参数】),例如 Integer 变为 Collection<Integer>Set<Integer>List<Integer>
  2. 返回值从单个对象变为 Map<K,V> 或者 List<V> 。例如 Map<Integer,Foo>List<Foo>,若返回的是 List 类型,那应与【对象集合参数】大小相同并顺序一致(PS: v1.3版本之后不再有此限制,详见更新细节)。

加缓存

在上面例子中,如果需要对获取单个对象的方法做缓存,会使用 @Cacheable@CacheResult 注解: (PS: 这里将 @CacheResult@Cacheable 放在一起举例子,实际使用时通常只用其中的一个)

class FooService {
   @Cacheable(cacheNames = "foo")
   @CacheResult(cacheName = "foo")
   public Foo getFoo(Integer fooId) {
      // 用 fooId 生成缓存 key 和计算 condition、unless 条件,用 Foo 为缓存值
   }
}

如果对获取批量对象的方法直接加上 @Cacheable@CacheResult,则会使用【对象集合参数】整体生成一个缓存 key,将返回的 MapList 整体作为一个缓存值。

但通常我们会希望它能变为多个 fooId => Foo 的缓存,即:使用【对象集合参数】中每个【元素】和它对应的值分别作缓存。此时只需要在【对象集合参数】上加上 @CacheAsMulti 注解即可实现我们想要的缓存方式。

class FooService {
   @Cacheable(cacheNames = "foo")
   @CacheResult(cacheName = "foo")
   public Map<Integer, Foo> getMultiFoo(@CacheAsMulti Collection<Integer> fooIds) {
      // 为 fooIds 集合中每个元素分别生成缓存 key 和计算 condition、unless 条件,用 Map 中对应的值作为缓存值
   }

   @Cacheable(cacheNames = "foo")
   @CacheResult(cacheName = "foo")
   public List<Foo> getMultiFoo(@CacheAsMulti List<Integer> fooIds) {
      // 为 fooIds 集合中每个元素分别生成缓存 key 和计算 condition、unless 条件,用 List 中对应的值作为缓存值
      // 之后的例子中,返回 List 和 返回 Map 的处理方式都一样,就不再单独举例
   }
}

更多示例

与其他注解搭配使用时的说明

  • 与 Spring 的 @CachePut 搭配时,同样符合上面的例子。
  • @CacheEvict 搭配时,若注解的 @CacheEvict.key() 参数中没有 #result,对【方法】返回类型无要求;若 key 中有 #result ,【方法】返回类型需要是 MapList
  • 与 Spring 的 @CachePut@CacheEvict 搭配,若 key 参数中已有 #result, 则可以没有【对象集合参数】的引用。
  • @CacheRemove 搭配时,对【方法】返回类型无要求。
  • 与 JSR-107 的 @CachePut 搭配时,对【方法】返回类型无要求,可参照下面的示例:

JSR-107 的 @CachePut

单个参数做key,未配置 @CacheKey

class FooService {
   @CachePut(cacheName = "foo")
   public void putFoo(Integer fooId, @CacheValue String value) {
      // 用 fooId 参数生成缓存的 key,用 value 作为缓存值
   }

   @CachePut(cacheName = "foo")
   public void putMultiFoo(@CacheAsMulti @CacheValue Map<Integer, String> fooIdValueMap) {
      // 此时方法的 @CacheValue 参数必须为 Map 类型
      // 用 fooIdValueMap 中的每个 Entry 的 key 分别生成缓存的 key,用 Entry 的 value 作为缓存值
   }
}

更多示例

总结和补充

  1. @CacheAsMulti 注解不能替代 Spring 缓存注解中的 key 参数,例如:@Cacheable.key() ,也不能替代 @CacheKey@CacheValue 注解。
  2. 如果使用自定义的 KeyGenerator,则会用【对象集合参数】的每个【元素】和其他参数组成 Object[] 传入 KeyGenerator.generate(Object, Method, Object...) 计算缓存 key;自定义的 CacheKeyGenerator 也一样。
  3. @Cacheable@CacheResult@CachePut 注解搭配使用时,若 CacheAsMulti.strictNull()true 且方法的返回类型是 Map,【元素】在 Map 中对应的值为 null 就会缓存 null,【元素】在 Map 中不存在就不缓存。
  4. @CachePut@CacheEvict 搭配,注解的 key 参数配置了 #result 时,若方法的返回类型是 Map,对于 Map 中不存在的【元素】会使用 null 作为缺省值来计算缓存 key 和 condition、unless 条件。
  5. @Cacheable.condition()@Cacheable.unless() 等条件表达式是用【对象集合参数】中的每个【元素】分别计算,只将不符合的【元素】排除,而不是整个集合。

缓存接口及转换

EnhancedCache 接口

org.springframework.cache.Cache 接口只定义了单个缓存操作,并不支持批量操作,为此定义了 EnhancedCache 接口扩充了 multiGetmultiPutmultiEvict 三个批量操作方法。

当使用某种缓存介质时,需要有对应的 EnhancedCache 接口实现。如果使用的介质没有对应的 EnhancedCache 实现,则会使用默认的 EnhancedCacheConversionService.EnhancedCacheAdapter 进行适配,会使用循环单个操作来实现批量操作,效率较低。同时在对象创建的时候会出现一条 warn 级别的日志。

EnhancedCacheConverter<T> 接口

每种缓存介质还需要定义一个转换器用来将 Cache 自动转为 EnhancedCache,转换器实现的接口为 EnhancedCacheConverter。BeanFactory 中注册的转换器会自动加载到 EnhancedCacheConversionService 中用来将 Spring 原有的 Cache 转为 EnhancedCache

默认实现

包里已经实现了 RedisCacheConcurrentMapCacheEhcachecaffeineCacheEnhancedCache 接口和相应的转换器,具体可到 cache.convert.converter 下查看。

工作原理

拦截器

  1. 在标准的 BeanDefinition 之后,修改原有的 OperationSourceInterceptor 的 Bean 定义,使用自定义的(继承原有的)来替换。
  2. 在原有 OperationSource 查询构建 Operation 后,查询构建 MultiOperation 并缓存。
  3. 在原有 Interceptor 执行拦截前,查询是否缓存有对应的 MultiOperation,如果有则拦截执行。

批量缓存

  1. 定义 EnhancedCache 扩充 Spring 的 Cache
  2. 定义 EnhancedCacheConverterCache 转为 EnhancedCache
  3. 对应 Cache 的实现类,增加 EnhancedCacheEnhancedCacheConverter 的实现类。
  4. 定义 EnhancedCacheConversionService 将所有 EnhancedCacheConverter (包括使用者自定义的)自动注入进来。
  5. 定义 EnhancedCacheResolver 包装 CacheResolver,并注入 EnhancedCacheConversionService,在调用 resolveCaches 获取 Cache 时将其转换为 EnhancedCache

开发总结

用到的 Utils

  • GenericTypeResolver 处理泛型类
  • ReflectionUtils 反射工具类
  • ResolvableType 处理各种字段类型、返回类型、参数类型
  • AnnotationUtils 注解工具类,例如向上找父类的注解
  • AnnotatedElementUtils 被注释的元素工具类,例如查找合并注解
  • MergedAnnotations 合并注解的操作工具

小知识点

  • Spring 的 @AliasFor 注解参数别名就是利用上述的 Spring 注解工具实现的。
  • Aware 的处理需要显示的实现,例如在 BeanPostProcessor 的实现中。
  • 如果一个 Map 没有对应的 Set 实现,可以用 Collections.newSetFromMap(new ConcurrentHashMap<>(16))
  • 处理 @Autowried 注解的是 AutowiredAnnotationBeanPostProcessor
  • 处理实现 ApplicationContextAware 接口的是 ApplicationContextAwareProcessor
  • java使用反射获取参数名得到的是arg0、arg1时,除了网上的解决办法(javac -parameters),还可以使用spring的 DefaultParameterNameDiscoverer。

扩展

缓存有效期

Spring 的缓存没有有效期?

针对不同的缓存缓存方式单独配置。例如Redis:

spring:
  cache:
    redis:
      time-to-live: PT15M  #缓存15分钟

灵活的TTL

前提:Spring 为缓存注解中每一个配置的 CacheName,都生成一个单独的 Cache 对象。

通常可以通过下面三种方式来实现:

  • 自定义 CacheManagerCacheResolver
  • 使用其他缓存框架(不能再使用 @CacheAsMulti),例如 JetCache。
  • 针对不同的缓存的扩展点单独定制。

针对Redis的扩展点单独配置

只要实现 RedisCacheManagerBuilderCustomizer 接口,就可以在 RedisCacheManager 生成前,设置 RedisCacheManagerBuilder 实现自定义配置。

具体实现查看 RedisCacheCustomizer 类。

之后只需增加如下配置:

spring:
  cache:
    redis:
      time-to-live: PT15M  #默认缓存15分钟
      cache-as-multi: #下面是 cache-as-multi 的配置
        serialize-to-json: true #使用 RedisSerializer.json() 序列化
        cache-name-time-to-live-map: #cacheName对应的缓存时间
          foo: PT15S  #foo缓存15秒
          demo: PT5M  #demo缓存5分钟

About

Extend Spring Cache to implement individual caching for each item in batch data, which can improve cache hit rate and utilization. Additionally, this extension allows the method for operating on batch data to share cache with the method for operating on individual data.扩展 Spring Cache 以实现批量数据中每项的单独缓存,从而提升缓存的命中率和使用率。

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages