- [Spring Cache](#spring-cache) - [Cache Abstract](#cache-abstract) - [使用cache abstraction的要点](#使用cache-abstraction的要点) - [缓存可能会存在的问题](#缓存可能会存在的问题) - [基于声明式注解的缓存](#基于声明式注解的缓存) - [@Cacheable](#cacheable) - [默认的key生成](#默认的key生成) - [自定义key生成的方式](#自定义key生成的方式) - [sync caching](#sync-caching) - [condition cache](#condition-cache) - [@Cacheable和Optional](#cacheable和optional) - [@CachePut](#cacheput) - [@CacheEvict](#cacheevict) - [@Caching](#caching) - [@CacheConfig](#cacheconfig) - [@EnableCaching](#enablecaching) - [配置cache 存储](#配置cache-存储) - [Cache Redis](#cache-redis) # Spring Cache ## Cache Abstract Cache针对java方法进行缓存,当想要获取的信息在cache中可获取时,可以从cache中进行获取,从而降低了java方法的执行次数。 每次在目标方法执行时,cache abstraction应用了一个cache check,检查该方法是否已经通过指定参数调用过。如果该方法已经通过指定参数调用过,会直接从缓存中获取已缓存的执行结果,而无需重复的调用方法。如果该方法尚未被调用过,那么调用该方法,并且将该方法的返回结果添加到缓存中,从而在方法下次调用时直接从cache中获取值。 通过cache,可以缓存开销较高的方法结果(io开销较高和cpu开销较高的方法结果均可被缓存),cache逻辑对于调用者来说是透明的。 > cache abstraction提供了一系列缓存相关的api,可以对cache内容进行更新或是移除,当缓存数据内容会改变的情况下这些api会很有用。 在spring中,cache abstraction的caching service是一个抽象接口而不是实现,需要使用实际的storage来对缓存数据进行存储。 spring提供了cache abstraction的一系列实现,例如基于JDK java.util.concurrent.ConcurrentMap的缓存、Caffeine等 ### 使用cache abstraction的要点 1. cache声明:表示需要使用cache的方法,并且指明cache策略 2. cache配置:将cache数据存储到何处,并从何处读取 ## 缓存可能会存在的问题 在使用缓存时,可能会面临如下问题: - 多进程环境问题:在多进程环境之下(如微服务在多个server上部署服务)使用缓存时,如果想要缓存在多个节点之间进行共享,需要解决缓存的共享问题 - 并发安全问题:使用缓存实际上是get-if-not-found-then-proceed-and-put-eventually的过程,在这个过程中并不会加锁,故而会存在并发安全的问题。如果多个线程都同时发生缓存miss的问题,那么同一个方法会背调用多次并且返回结果会背load多次;淘汰时同样会面临竞争问题,并发环境下如果有一个线程想要淘汰过时key,但另一个线程load了最新的value并更新ttl,那么更新后的key仍有可能被淘汰。 ## 基于声明式注解的缓存 ### @Cacheable 可以使用@Cacheable将方法声明为可缓存的,在将方法声明为@Cacheable之后,方法的返回结果会被存储到缓存中。在一系列具有相同参数的调用中,cache中缓存的值将会被返回,而无需真的调用方法。 在@Cacheable注解最简单的使用场景中,需要指明和方法关联的cache name: ```java // 如下示例中,findBook方法将会和名为books的缓存相关联 @Cacheable("books") public Book findBook(ISBN isbn) {...} ``` > 虽然大多数场景下@Cacheable注解只会指定一个cache name,但是**也能为@Cacheable注解指定多个cache**。当任一缓存命中时,关联的值都会被返回。 > 即使其他缓存没有命中,该命中值也会添加到所有其他缓存中(方法并未被实际调用) 如下实例为@Cacheable方法指定了多个cache: ```java @Cacheable({"books", "isbns"}) public Book findBook(ISBN isbn) {...} ``` #### 默认的key生成 缓存实际上是通过key-value的形式存储的,每次对cached method的调用都需要被转化为一个合适的key。cache abstraction通过使用SimpleKeyGenerator来生成key,该generator生成算法如下: - 如果没有给定参数,那么返回SimpleKey.EMPTY - 如果给定了一个参数,返回该参数 - 如果指定了多于一个参数,那么返回一个包含所有参数的SimpleKey 该算法在大多数用例之下运行正常,只要参数含有正常key并且具有有效的hashCode和equals实现。 #### 自定义key生成的方式 当在cached method的参数中,仅仅只有一部分参数应参与key的生成,另一部分不参与时,可以通过指定@Cacheable注解的key属性来指定如何生成key。可以在key属性中使用spel表达式: ```java @Cacheable(cacheNames="books", key="#isbn") public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed) @Cacheable(cacheNames="books", key="#isbn.rawNumber") public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed) @Cacheable(cacheNames="books", key="T(someType).hash(#isbn)") public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed) ``` #### sync caching 在使用@Cacheable时,默认情况下cached method并没有使用任何lock。可以使用@Cacheable注解的sync属性来指示底层的缓存provider在value被计算时锁定指定条目(防止在竞争场景下同一个值被重复计算)。 ```java @Cacheable(cacheNames="foos", sync=true) public Foo executeExpensiveOperation(String id) {...} ``` #### condition cache 可以通过指定@Cacheable注解的condition属性来指定该方法调用是否被缓存。condition可以为其指定一个spel表达式,如果表达式结果为true,该方法调用被缓存,否则不被缓存。 ```java // 仅当name参数的长度小于32时被缓存 @Cacheable(cacheNames="book", condition="#name.length() < 32") public Book findBook(String name) ``` 还可以使用unless属性来阻止方法被缓存,unless同样可以为其指定一个spel表达式。unless在方法调用完成之后被计算,可以通过#result来根据方法返回结果决定是否缓存该方法: ```java @Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result.hardback") public Book findBook(String name) ``` #### @Cacheable和Optional cache abstraction支持Optional类型,如果Optional对象的value不为空,那么value将会被存储到cache中,如果Optional为空,那么null将会被存储到管来奶的cache中。 在@Cacheable的spel表达式中,#result获取的是Optional对象的value而不是wrapper。 ```java @Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result?.hardback") public Optional findBook(String name) ``` ### @CachePut 在不影响方法执行的情况下,要将方法的执行结果更新到cache中,可以使用@CachePut注解。标注了@CachePut的方法总会被执行,并且执行的返回结果会被更新到cache中。 @CachePut注解支持和@Cacheable相同的参数,且@CachePut用于的是缓存的注入,而不应用于优化方法流程。 @CachePut的使用如下所示: ```java @CachePut(cacheNames="book", key="#isbn") public Book updateBook(ISBN isbn, BookDescriptor descriptor) ``` > @CachePut注解和@Cacheable注解通常情况下不应用于同一方法上 ### @CacheEvict cache abstraction不仅支持缓存的注入,而且支持缓存的淘汰,该淘汰过程可以从缓存中移除过时或未使用的数据。 @CacheEvict注解具有allEntries属性,将该属性标记为true可以逐出所有的缓存条目: ```java // 该场景下,cache中的条目将会被清空 // 该场景下并不是依此逐出所有的key,而是一次性清空所有缓存 @CacheEvict(cacheNames="books", allEntries=true) public void loadBooks(InputStream batch) ``` 可以为@CacheEvict注解指定beforeInvocation属性,来指定该eviction行为是否应该在@CacheEvict方法被调用之前执行: - 默认情况下,beforeInvocation属性的默认值为false,eviction操作在方法成功返回之后才执行(如果该方法被cached或执行时抛出异常,eviction操作不会执行) - 如果beforeInvocation属性被设置为true,代表该eviction行为在方法被调用之前就会被执行 ### @Caching 如果想要指定同一类型的多个注解(例如@CacheEvict或CachePut),例如对不同的cache,condition和key expression不同。 @Caching注解允许对同一方法内嵌多个@Cacheable、@CachePut、@CacheEvict注解,使用示例如下: ```java @Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") }) public Book importBooks(String deposit, Date date) ``` ### @CacheConfig 在使用cache注解时,可能在同一个类中需要为所有cache operation指定相同的选项(例如为类中所有的cache注解指定相同的cache name)。此时,可以通过一个class-level的@CacheConfig注解来指定类中所有cache operation的相同选项: ```java @CacheConfig("books") public class BookRepositoryImpl implements BookRepository { @Cacheable public Book findBook(ISBN isbn) {...} } ``` > 通过@CacheConfig注解指定的属性可以在cache operation中被覆盖 ### @EnableCaching 想要在项目中启用所有的缓存注解,需要在一个@Configuration类上指定@EnableCaching注解 ## 配置cache 存储 cache abstraction提供了一些存储集成选项。如果没有指定任何cache library,那么spring boot会自动装配一个simple provider,该provider使用concurrent map来进行缓存存储。当需要一个cache时,cache provider会创建一个cache并返回给调用者。 ### Cache Redis 如果redis在项目中配置并且可以获取,那么spring boot会自动配置一个RedisCacheManager。可以在启动时配置其他cache,通过在properties文件中指定spring.cache.cache-names属性,并且cache的默认属性可以通过spring.cache.redis.*属性来进行配置: ```properties spring.cache.cache-names=cache1,cache2 spring.cache.redis.time-to-live=10m ``` > 默认情况下,对于不同的cache,会添加一个key前缀,继而如果两个不同的cache使用相同的key,不会出现key冲突的情况 如果想要更细粒度的对cache进行配置,可以通过如下方式进行配置: ```java @Configuration(proxyBeanMethods = false) public class MyRedisCacheManagerConfiguration { @Bean public RedisCacheManagerBuilderCustomizer myRedisCacheManagerBuilderCustomizer() { return (builder) -> builder .withCacheConfiguration("cache1", RedisCacheConfiguration .defaultCacheConfig().entryTtl(Duration.ofSeconds(10))) .withCacheConfiguration("cache2", RedisCacheConfiguration .defaultCacheConfig().entryTtl(Duration.ofMinutes(1))); } } ```