July 15, 2018 · Java 本文字数: 1.9k 阅读时长:7 min 全站字数:361.8k

Spring中的缓存支持

  1. 1. Spring中的Cache支持
  2. 2. Spring中的缓存抽象
    1. 2.1 理解缓存(cache)和缓冲(buffer)的异同
    2. 2.2 缓存抽象实现
    3. 2.3 使用缓存
    4. 2.4 如何自定义缓存后端存储
  3. 3. Spring Boog中使用缓存
  4. 4. Spring Boot缓存踩坑记
    1. 4.1 Null key returned for cache operation
    2. 4.2 缓存null值
  5. 5. 参考资料

1. Spring中的Cache支持

spring框架在3.1支持对应用增加透明的缓存功能,实现类似spring的事务。透明缓存是通过注解来实现,支持多种缓存方案,而不需要对改动代码。在4.1对缓存的实现进行了优化,支持JSR-107 JCache,以及更多的定制化功能。

2. Spring中的缓存抽象

2.1 理解缓存(cache)和缓冲(buffer)的异同

buffer一般用于存在速度差异的两种实体之间临时存储数据。由于两个实体存在速度差异,所以有一方必然需要等待另外一方的数据。buffer使得多个小的数据块合并为一个大的数据块,从而减少由于两个实体之间速度的差异而导致的性能损失。数据只从buffer写入一次或者读取一次。buffer对至少一个实体可见,因为,必然有一方要写入或者读取数据。

cache则不同,对它的上游和下游都不可见。cache使得数据的读取变得高效,而不是每次都从数据库中读取。

写cache一般会涉及buffer操作,然后通过后台线程去同步到具体的后端存储。但buffer不会包含cache操作。

cache主要用于降低延迟,而buffer用于减少数据需要发送的次数。

2.2 缓存抽象实现

Spring中的缓存抽象应用于方法,使用cache来减少对方法的调用。每次目标方法被调用时,缓存抽象层都会检查该方法是否已经使用指定的参数调用执行过。如果执行过,将直接返回缓存中的结果,而不需要再执行目标方法。如果没有执行过,则方法将被调用和执行,结果将被写入缓存,下次调用时将直接放回缓存中的结果。

对于CPU密集型或者IO密集型的方法,对于指定的调用参数,这些方法可以只执行一次,重用缓存中的结果。缓存的逻辑不会对调用方有任何影响。需要注意的是缓存只能应用于那些对于指定的调用参数,无论调用多少次,返回相同结果的方法。

缓存抽象层也实现了其他的缓存操作,如更新缓存,或者移除缓存。用于缓存数据变化的场景。在Spring框架中,缓存服务只是一个抽象层,不是实现。缓存的实现需要使用具体的存储,如内存,redis等,来存储缓存数据。

缓存抽象使得开发者从具体的缓存逻辑代码中解放出来,但是并不提供具体存储缓存的实现。

缓存抽象通过 org.springframework.cache.Cache and org.springframework.cache.CacheManager接口来声明。已经有多种缓存的实现,如使用JDK的java.util.concurrent.ConcurrentMapEhcache 2.x, Gemfire cache, Caffeine and JSR-107 compliant caches (e.g. Ehcache 3.x)等。

缓存抽象层没有对多线程和多进程环境进行处理,都需要由具体的缓存实现来处理。对于多进程的环境,应用部署在不同的服务器上,需要自己配置缓存的实现,分布式缓存。多个服务器上存储相同的数据是没有问题,但是如果缓存需要修改和更行,在需要一种传播机制,来保证所有服务器上的缓存都是相同的。

缓存读取数据的操作等价于 get-if-not-found-then- proceed-and-put-eventually的逻辑代码。在这个操作过程中是没有锁的,可以并发操作。对于缓存的移除,如果多线程同时进行更新,则缓存也有可能有脏数据。

缓存抽象层不支持设置TTL,需要由具体的缓存实现来支持。

2.3 使用缓存

  1. 缓存声明。在需要缓存的方法上配置缓存注解和缓存策略。Spring建议只注解具体的类,而不是接口。
    • @Cacheable 触发缓存填充,标志哪些方法是需要支持缓存。当有多个缓存时,只要有一个缓存命中,则直接返回,其他的缓存也会更新。由于缓存都是key/value类型,所以需要注意key的生成方式,一般只要方法的参数实现了hashCode()equals()方法,就可以使用默认的KeyGenerator。一般可以通过使用SpEL来指定缓存的key。如果仍然无法满足需求,则需要自定义实现org.springframework.cache.interceptor.KeyGenerator@CacheablekeykeyGenerator属性是互斥的,因为两个都指定缓存的key生成方式。可以通过cacheManager或者cacheResolver设置自定的缓存解析器。在多线程环境下,可以通过sync制定对特定缓存条目设置加锁,直到缓存更新完,其他线程可以直接读取缓存,从而避免多个线程同时更新缓存。可以使用conditionunless来实现条件缓存。
    • @CacheEvict触发缓存移除。可以移除所有缓存条目,也可以设置在方法调用前还是调用后触发缓存移除操作
    • @CachePut 触发缓存更新。不建议和@Cacheable同时使用。
    • @Caching 对于指定的方法应用多个缓存
    • @CacheConfig在类级别共用缓存配置。方法级别的会覆盖该配置。
    • @EnableCaching 开启缓存,并可以设置缓存的各种配置选项。否则缓存不会生效。缓存使用AOP实现,默认的advice模式是proxy,会通过proxy来拦截所有的调用,但是类的本地调用会不起作用,同时proxy模式也只能对public的方法。可以使用更高阶的aspectj模式。如果代码无法获取到,从而导致无法使用注解方式,则可以使用xml方式来配置。
  2. 缓存配置。通过配置CacheManager来实现。缓存数据的后端存储配置,以及如何读取。

2.4 如何自定义缓存后端存储

需要实现CacheManagerCache 。这些类就是适配器,用于将缓存抽象层的操作映射为具体后端存储的API操作。可以参考ehcache的实现。大多数 CacheManager 都可以直接使用org.springframework.cache.support 的方法,如AbstractCacheManager实现了缓存操作的代码模板,只剩具体的映射操作没有实现。

3. Spring Boog中使用缓存

使用注解 @EnableCaching,Spring Boot自动配置缓存。虽然Spring同时支持 Spring Cache和 JCache,但建议不要混合使用。

4. Spring Boot缓存踩坑记

4.1 Null key returned for cache operation

1
2
3
4
5
6
@Cachable(key = "#name")
public User findByName(String name) {
}

如果使用缓存是报错:
java.lang.IllegalArgumentException: Null key returned for cache operation (maybe you are using named params on classes without debug info?)

这里的原因是由于key的指定没有生效,如果编译时没有指定debug模式,则class文件时没有参数名信息,所以稳妥的是使用 #a0或者#p0来指定参数。If for some reason the names are not available (e.g. no debug information), the argument names are also available under the #a<#arg> where #arg stands for the argument index (starting from 0).)

4.2 缓存null值

如果缓存对null的结果进行了缓存,需要改成:

1
@Cachable(unless = "#result == null)

5. 参考资料

  1. springboot cache feature https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-caching.html
  2. spring cache https://docs.spring.io/spring/docs/current/spring-framework-reference/integration.html#cache
  3. jsr-107 jcache https://jcp.org/en/jsr/detail?id=107
  4. Buffer vs Cache <https://en.wikipedia.org/wiki/Cache_(computing)#Buffer_vs._cache