Spring cache简单使用guava cache

时间:2023-01-15 20:44:56

Spring cache简单使用

前言

spring有一套和各种缓存的集成方式。类似于sl4j,你可以选择log框架实现,也一样可以实现缓存实现,比如ehcache,guava cache.

[TOC]

什么时候用缓存

首先,缓存是为了省略消耗时间的步骤,比如io。当我需要从数据库查询的数据几乎没有变化,或者变化很少的时候,我就没必要每次都去数据库里拿数据了。大可以放到本地,直接取出来就可以了。这时候需要注意的是数据一致性问题,缓存的数据是否被更改了,数据是否有效。

我的项目是分布式部署的,但还没有搭建分布式缓存服务。我采用的本地缓存,也就是说,我的缓存只能在本实例中,跨机器访问则不命中。即便如此也大大减少了访问数据库的开销了。

配置缓存

这里采用guava cache作为本地缓存。将guava cache注册到cacheManger里就可以调用了。

1.配置cacheManger

首先针对要缓存的类型,配置缓存策略。这里设置最大缓存数量和缓存过期时间

public static final String HOTEL_POSTION = "hotel_position";  //cache key
@Value("${cache.guavaCache.hotelPosition.maxSize}")
private long hotelPositionMaxSize;
@Value("${cache.guavaCache.hotelPosition.duration}")
private long hotelPositionDuration;

private GuavaCache buildHotelPositionCache() {
return new GuavaCache(HOTEL_POSTION,
CacheBuilder.newBuilder()
.recordStats()
.maximumSize(hotelPositionMaxSize)
.expireAfterWrite(hotelPositionDuration, TimeUnit.DAYS)
.build());
}

将刚才创建的缓存策略添加到cacheManger:

    @Bean
public CacheManager cacheManager() {
SimpleCacheManager manager = new SimpleCacheManager();
List list = new ArrayList();
list.add(buildHotelPositionCache());
manager.setCaches( list );

return manager;
}
2.配置要缓存的方法

在需要使用这个缓存的地方,增加一行注解

@Cacheable(value = CacheManagementConfig.HOTEL_POSTION, key = "{#hotelId}", condition = "", unless = "!#result.isSuccessful()")
public BaseDomainResponse<HotelPosition> getHotelPosition(int hotelId, String apiToken) {
//......
}
  • @Cacheable表示这个方法要被缓存
  • value string,表示这个方法缓存的唯一性标识,即这方法缓存的key。语法为SpEL.
  • key String,表示每条请求缓存的key,即如果key相同,则返回缓存中对应的数据
  • condition boolean,可以额外添加缓存的条件.语法为SpEL.
  • unless boolean, 配置哪些条件下的记录不缓存。语法为SpEL.
  • result表示return的这个对象,可以同result来调用这个对象的属性,比如isSuccessful()就是我返回对象的一个方法。





官方文档

此处学习官方文档cache部分,spring版本4.1+。

At its core, the abstraction applies caching to Java methods, reducing thus the number of executions based on the information available in the cache. That is, each time a targeted method is invoked, the abstraction will apply a caching behavior checking whether the method has been already executed for the given arguments. If it has, then the cached result is returned without having to execute the actual method; if it has not, then method is executed, the result cached and returned to the user so that, the next time the method is invoked, the cached result is returned. This way, expensive methods (whether CPU or IO bound) can be executed only once for a given set of parameters and the result reused without having to actually execute the method again. The caching logic is applied transparently without any interference to the invoker.

这个缓存应用于java 方法级别缓存,通过缓存中的数据来减少方法执行次数。每当目标方法被调用,spring cache会执行一个缓存行为来检查这个相同参数的方法是否已经被执行。如果被执行过了,那么不执行方法直接返回缓存中的结果。 通过这样,代价高的方法(CPU或IO依赖)可以只执行一次,相同参数的结果会复用而不是真正的执行这个方法。这个缓存逻辑对调用者来说是透明的,也就是调用者不用管这个缓存逻辑。

Just like other services in the Spring Framework, the caching service is an abstraction (not a cache implementation) and requires the use of an actual storage to store the cache data - that is, the abstraction frees the developer from having to write the caching logic but does not provide the actual stores. This abstraction is materialized by the org.springframework.cache.Cache and org.springframework.cache.CacheManager interfaces.

There are a few implementations of that abstraction available out of the box: JDK java.util.concurrent.ConcurrentMap based caches, Ehcache 2.x, Gemfire cache, Caffeine, Guava caches and JSR-107 compliant caches (e.g. Ehcache 3.x). See Section 36.7, “Plugging-in different back-end caches” for more information on plugging in other cache stores/providers.

spring cache是一个抽象的概念,没有提供实现方式去存储数据,开发者可以自己选择任意的实现。比如DK java.util.concurrent.ConcurrentMap based caches, Ehcache 2.x, Gemfire cache, Caffeine, Guava caches and JSR-107 compliant caches (e.g. Ehcache 3.x)。

If you have a multi-process environment (i.e. an application deployed on several nodes), you will need to configure your cache provider accordingly. Depending on your use cases, a copy of the same data on several nodes may be enough but if you change the data during the course of the application, you may need to enable other propagation mechanisms.

Caching a particular item is a direct equivalent of the typical get-if-not-found-then- proceed-and-put-eventually code blocks found with programmatic cache interaction: no locks are applied and several threads may try to load the same item concurrently. The same applies to eviction: if several threads are trying to update or evict data concurrently, you may use stale data. Certain cache providers offer advanced features in that area, refer to the documentation of the cache provider that you are using for more details.

To use the cache abstraction, the developer needs to take care of two aspects:

  • caching declaration - identify the methods that need to be cached and their policy
  • cache configuration - the backing cache where the data is stored and read from

如果你采用多过程环境(比如,一个项目部署到多个服务节点,即分布式部署),你需要配置相应的的缓存实现。在你的使用案例中,同样数据的拷贝已经足够使用了。但如果你在这期间修改了数据,你需要使用其他传播机制来控制缓存的一致性。
缓存一个指定的条目直接等价于获取-如果-不存在-然后-执行-并且-最好放入缓存的程序逻辑的代码块:不会阻塞并且多线程可以并发地加载相同的条目。缓存更新策略也一样:如果几个县城尝试并发地更新或者移除缓存的数据,你需要使用过期的数据。在这个领域,特定的缓存实现提供更先进的方式,参考你使用的缓存实现的文档来获取等多的详情。
想要使用这个抽象的缓存,开发者需要关心两个方面:

  • 缓存声明 - 定义需要被缓存的方法以及对应的缓存策略。
  • 缓存配置 - 数据存储和读取的实现。

1.基于注解的声明式缓存

缓存抽象提供了一系列的java注解:

  • @Cacheable 触发缓存逻辑
  • @CacheEvict 触发缓存逐出逻辑
  • @CachePut 不干涉方法执行地更新缓存
  • @Caching 重组一个方法上的多重缓存操作

1.1@Cacheable 注解

就像名字所暗示的,@Cacheable是用来区分方法是否可缓存的。也就是说,哪个方法可以把结果存储到cache中,所以随后调用(相同的参数)时会返回cache中的值,而且并不会实际上运行这个method。最简单的用法:注解需要一个cache的name来关联这个method。

@Cacheable("books")
public Book findBook(ISBN isbn) {...}

在上述的片段中,method findBook关联到名字叫做books的cache。每次这个方法被调用的时候,cache会检查这个调用是否已经被执行过了并且不必重复执行。大多数情况下,只声明一个cache,但这个注解支持声明多个name,因此可以使用多个cahce。这样,在执行method之前每个cache都会检查是否存在 - 如果至少一个cache命中了,然后就会返回关联的值。

默认key注册模式

因为cache本质上是key-value存储,每次调用缓存的method需要被翻译成一个合适的key来获取缓存。Out of the box(这句话不知道该怎么翻译,box应该是指这个类,即在这个method所在的类之外), 缓存代理(cache abstraction)使用一个基于以下算法的简单的KeyGenerator

  • 如果没有参数,key就是SimpleKey.EMPTY.
  • 如果只有一个参数,则返回那个参数.
  • 如果多个参数,返回SimpleKey包含所有的参数

只要参数有__natural keys__ 并且实现了合法的hashCode()equals(),这个方法适合于大多数使用案例。如果不是,则key产生策略就需要改变。

不想使用默认的key生产机制,你需要实现接口:org.springframework.cache.interceptor.KeyGenerator.



自定义Key产生声明

因为caching是普遍的,所以很可能目标method有各种签名(signatures)不可以简单的映射到cache结构中。这个在目标mothod有多个参数但只有部分参数和缓存关联的时候就变得明显。
简单的说,cache默认把参数组合成一个key,这个key对应一个结果,下载遇到相同参数就会对应这个key,可以去除这个key对应的结果。然而,有时候,我们有多个参数,比如a,b,c。只有a和b和缓存的结果有关,c是变化的。

@Cacheable("books")
public Book findBook(ISBN a, String b, String token)

假设我这个findBook需要一个token来获取权限,但和book无关。那么我们遇到相同的a就可以返回对应的book了,不需要关心token。换句话说,自己可以定义缓存的条件,只要a和b相同,则命中同一个缓存。
然而,默认的会将a和b还有token组成一个key,只有这个三个相同的时候才会命中缓存。这时候就需要我们自定义key的组成了。

以下示例各种SpEL声明,通过SpEL语法来声明key:

//仅仅使用key(isbn)
@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, String b, String token)

//使用isbn的一个属性当做key
@Cacheable(cacheNames="books", key="#isbn.rawNumber")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

//调用某个类的某个方法来生成key
@Cacheable(cacheNames="books", key="T(someType).hash(#isbn)")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

//组合key(a和b)
@Cacheable(cacheNames="books", key="#a.concat(#b)")
public Book findBook(String a, String b, String token)

上述代码片段显示了选择一个特定的参数或者一个参数的属性或者任意的方法或者组合参数作为key是多么简单。

如果产生key的算法太特殊或者如果这个key需要共享,你可以自定义一个keyGenerator。只要声明自定义的KeyGenerator的bean实现就可以了:

@Cacheable(cacheNames="books", keyGenerator="myKeyGenerator")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
`key`和`keyGenerator`是互斥的,如果同时声明会抛出异常。



默认的Cache Resolution

Out of the box, 缓存代理使用简单的CacheResolver来获取cache, 这个是可以使用CacheManager来手动配置的。
如果不想使用默认的cache resolver,你需要实现接口:org.springframework.cache.interceptor.CacheResolver

自定义Cache Resolution

默认的cache resolution适合于使用一个CacheManager并且没有复杂的cache resolution.

对于采用多个cache managers的应用,要设置cacheManger

@Cacheable(cacheNames="books", cacheManager="anotherCacheManager")
public Book findBook(ISBN isbn) {...}

当然也可以完全替换CacheResolver, 就像key generation一样简单。每次cache操作都会请求这个resolution,基于运行时的参数来交给它的实现。

@Cacheable(cacheResolver="runtimeCacheResolver")
public Book findBook(ISBN isbn) {...}
就像`key`和`keyGenerator`一样,`cacheManager`和`cacheResolver`参数也是互斥的,同时声明会抛出异常。



同步caching

在多线程环境,一个操作也许会并发的执行(比如启动的时候)。 默认的,cache代理不会lock并且同样的数据也许会计算多次,这与cache的目标相悖。

在这些特殊的场景,当计算的时候,参数sync可以用来通知将cache lock cache entry. 这样,只有一个线程可以计算,其他的等待entry被更新到cache。

@Cacheable(cacheNames="foos", sync="true")
public Foo executeExpensiveOperation(String id) {...}



条件缓存(conditional caching)

有时候,一个method也许并不适合全部缓存(比如,根据参数缓存)。cache注解通过参数condition来支持这种功能,同样使用SpEL表达式,结果为true或false, 如果是true则缓存,否则表现为这个method没有缓存。这个判断会在每次获取value的时候执行,无论缓存的value是什么以及无论使用哪个参数。

一个简单的示例,一下method只有在参数name长度小于32的时候执行缓存。

@Cacheable(cacheNames="book", condition="#name.length < 32")
public Book findBook(String name)

除了使用condition, unless可以用来否决把结果加入缓存。不同的是,unless的表达式会在method执行结束后考量,就是mehtod执行完后判断是否加入缓存。扩展之前的示例 -- 我们只需要缓存paperback books. unless为true的时候不缓存。

@Cacheable(cacheNames="book", condition="#name.length < 32", unless="#result.hardback")
public Book findBook(String name)

这里#result就是指向返回值。



可使用的SpEL表达式

每个SpEL表达式都有一个专门的context。除了采用参数构建表达式,框架提供了专门的与caching相关的元数据,比如参数名。下表列出了在context中可用的参数,你可以用来当做key和conditional 处理。

Name Location Description Example
methodName root object 被执行的method的名字 #root.methodName
method root object 被执行的method #root.method.name
target root object 执行的对象 #root.target
targetClass root object 执行对象的class #root.targetClass
args root object 执行对象的参数们(数组) #root.args[0]
caches root object 当前method对应的缓存集合 #root.caches[0].name
argument name evaluation context 任意method的参数。如果特殊情况下参数还没有被赋值(e.g. 没有debug信息),参数可以使用#a<#arg>来表示,其中#arg代表参数顺序,从0开始 #iban或者#a0(也可以使用#p0或者#p<#arg>注解来启用别名)
result evaluation context method执行的结果(要缓存的对象),仅仅在unless表达式中可以使用,或者cache put(用来计算key),或者cache evict表达式(当beforeInvocation=false). 为了支持wrapper,比如Optional#result指向世纪的对象,不是wrapper. #result
参考