代码改变世界

Spring 对缓存的抽象

2017-09-15 21:00  l4y  阅读(1070)  评论(0编辑  收藏  举报

Cache vs. Buffer

A buffer is used traditionally as an intermediate temporary store for data between a fast and slow entity. As one party world have to wait for the other affecting performance, the buffer alleviates this by allowing entire blocks of data to move once rather than in small chunks. The data is written and read only once from the buffer. Furthermore, the buffers are visible to at least one party which is aware of it.

A cache on the other hand by definition is hidden and neither party is aware that caching occurs. It as well improves performance but does that by allowing the same data to be read multiple times in a fast fashion.

更多解释,可以参考维基百科https://en.wikipedia.org/wiki/Cache_(computing)#The_difference_between_buffer_and_cache

本文讨论的 Spring 版本为 4.2,较早版本的不同会在文中提及,但不会重点描述。

一、Spring 中的缓存(cache)

接下来的篇幅中,我将使用 Spring 来代替 Spring Framework。

像 Spring 中提供的其他服务一样,Spring 为缓存也仅仅提供抽象,而不是具体的实现。这样,开发者不需要写具体的缓存逻辑但是需要提供实际的存储的实现。Spring 中缓存的抽象通过 org.springframework.cache.Cacheorg.springframework.cache.CacheManager 这两个接口体现。

虽然 Spring 的缓存抽象本身不提供具体的存储实现,但是 Spring 之外有很多实现,如:EhcacheCaffeineguaua等。

注意,Spring 的 cache 抽象并没有针对多线程、多进行环境做特殊处理,这样的特性由具体的 cache 实现来处理。

如果处于一个多进程的环境,比如你的应用部署在多个节点上,你就必须处理同步问题,以免造成节点间数据不一致。

要使用 cache 抽象,开发者需要关注两个方面:

  1. 缓存声明:表明某个方法需要缓存,及缓存策略;
  2. 缓存配置:缓存数据保存在哪里,从哪里读取。

二、以基于注解的方式使用缓存

@Cacheable

@Cacheable 用于一个表示缓存该方法的返回值。后续对该方法的调用,可以不执行该方法,直接从缓存中返回结果。

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

上边的代码,findBook 方法关联的缓存名字为books。一个缓存也能有多个名字,比如下面的代码段。

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

注意,在上面的代码示例中,如果在名为 isbns 的缓存中命中,而 books 中没有命中,那么会不执行方法,直接返回结果,并且 books 中也会加入在 isbns 中命中的数据。

1. 键

缓存本质上就是键值对,Spring 的缓存抽象基于以下算法通过 KeyGenerator 提供了生成:

  • 如果方法没有参数,返回 SimpleKey.EMPTY
  • 如果方法有且只有一个参数,返回这个参数;
  • 如果方法有超过一个的参数,返回一个包含所有参数的 SimpleKey

如果参数有自然键(nstural keys)并有效的实现了 hashCode()equals() 方法,那么这种方式可以非常好的工作。否则,就需要改变策略。

4.0 版本之前的生成算法不同,需要了解更多的,请参考这里

除了自动的生成策略,开发者还可以根据实际需要自定义缓存的键,主要实现方式是通过 SqEL

@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)

除通过 key 属性自定义键,还可以通过 keyGenerator 来指定一个 KeyGenerator

Spring 提供的缓存抽象不针对多线程环境多任何特殊处理,但可以通过 sync 属性来暗示缓存提供方(具体实现)在计算值的时候锁住缓存。也就是说,在进行计算的时候,只有一个线程工作,其他线程阻塞直到缓存更新。

@Cacheable(cacheNames="books", keyGenerator="myKeyGenerator")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

2. 条件化缓存

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

condition、unless 属性都可以用来实现条件化缓存。如果 unless 属性的表达式计算的结果为 true,那么缓存方法返回的数据就不会放到缓存中。如果 condition 属性的表达式计算的结果为 false,那么对于这个方法的缓存就会被禁用掉。
需要注意的是,unless 属性只能阻止将对象放进缓存,但是在这个方法调用的时候,依然会去缓存中查找。而如果 condition 属性的表达式计算结果为 false,那么直接禁用缓存(不会去缓存查找,返回值也不会放进缓存中)。

@CachePut

@CachePut 仅更新缓存,而不插手方法的执行。也就是说,方法总会执行,并把执行结果放进缓存中。它支持 @Cacheable 的所有选项。

需要注意,在同一个方法上同时使用 @CachePut@Cacheable 将产生不确定的行为,千万不要这么做。

@CacheEvict

@CacheEvict 用于从缓存中删除数据,跟 @Cacheable 一样,@CacheEvict 需要指定一个或多个名字,允许自定义缓存决议、键决议,可以指定条件,除此之外,还可以通过 allEntries 来表示影响范围(单条数据,还是全部数据)。

@Caching

@Caching 用于在一个方法上组合多个 @Cacheable@CachePut@CacheEvict

@CacheConfig

如果在一个类的所有方法的缓存上需要使用相同的缓存名字、自定义的 KeyGenerator、或自定义的 CacheManager,甚至是自定义的 CacheResolver,那么可以通过在类上使用 @CacheConfig 来完成。

在方法上定义会覆盖 @CacheConfig 的定义。

@CacheConfig("books")
public class BookRepositoryImpl implements BookRepository {

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

那么我们来总结一下,在三个级别上为每个缓存操作进行自定义的途径:

  • 全局配置:CacheManagerKeyGenerator
  • 类级别:@CacheConfig
  • 操作级别,即在具体的方法上。

3. 启用基于注解的缓存

虽然具体注解介绍完了,但是像 Spring 中的其他组件一样,需要启用才能真正发挥作用。当然还有很多配置项,这里我只介绍启用方式。

通过基于 Java 注解的方式启用:

@Configuration
@EnableCaching
public class CacheConfig {
}

通过 XML 配置启用:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:cache="http://www.springframework.org/schema/cache"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">

        <cache:annotation-driven />

</beans>

到这里,本文的主要的内容就结束了。如果你想了解使用自定义注解,集成具体的缓存实现,请查阅相应文档。


博主在 Github 上有一个基于 SSM(Spring、Spring MVC、MyBatis)的小项目,部分学习内容会在该项目中使用。

该项目 Spring 相关配置,完全使用基于注解的方式。博主在刚接触各种配置的时候,绕了一些弯路。

对于刚接触这些框架的朋友,该项目或许会有些许帮助。如果在理解该项目时或参考时遇到任何问题,欢迎通过你能找到的任何方式联系博主,非常乐意共同学习。

项目地址为:spittr

如果你喜欢 xml 配置的方式,可参考另外一个项目 seckill 。该项目是博主在慕课网上学习该课程的源代码,项目中没有完全采用基于注解的方式,相比而言,该项目在配置方面更加老道

参考资料: