一.EhCache和SpringCache简介
1.1 EhCache
EhCache 是一个纯 Java 的进程内缓存框架,具有快速、精干等特点,是 Hibernate 中默认CacheProvider。Ehcache 是一种广泛使用的开源 Java 分布式缓存。主要面向通用缓存,Java EE 和轻量级容器。它具有内存和磁盘存储,缓存加载器,缓存扩展,缓存异常处理程序,一个 gzip 缓存 servlet 过滤器,支持 REST 和 SOAP api 等特点。
EhCache 适用于单体应用的缓存,当应用进行分布式部署的时候,各应用的副本之间缓存是不同步的。EhCache 由于没有独立的部署服务,所以它的缓存和应用的内存是耦合在一起的,当缓存数据量比较大的时候要注意系统资源能不能满足应用内存的要求。
1.2 SpringCache
Spring Cache作为缓存框架的门面,之所以说它是门面,是因为它只提供接口层的定义以及AOP注解等,不提供缓存的具体存取操作。缓存的具体存储还需要具体的缓存存储,比如EhCache 、Redis等。Spring Cache与缓存框架的关系有点像SLF4j与logback、log4j的关系。
Spring Cache通过注解的方式来操作缓存,一定程度上减少了程序员缓存操作代码编写量。注解添加和移除都很方便,不与业务代码耦合,容易维护。
二.整合Spring Cache与EhCache
2.1 pom.xml添加SpringCache和Ehcache的jar依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
2.2 添加入口启动类@EnableCaching注解开启Caching
@EnableCaching
在Spring Boot中通过@EnableCaching
注解自动化配置合适的缓存管理器(CacheManager),Spring Boot根据下面的顺序去侦测缓存提供者,也就是说Spring Cache支持下面的这些缓存框架:
- Generic
- JCache (JSR-107) (EhCache 3, Hazelcast, Infinispan, and others)
- EhCache 2.x(发现ehcache的bean,就使用ehcache作为缓存)
- Hazelcast
- Infinispan
- Couchbase
- Redis
- Caffeine
- Simple
2.3添加ehcache配置
spring:
cache:
type: ehcache
ehcache:
config: classpath:/ehcache.xml
config:classpath:/ehcache.xml
可以不用写,因为默认就是这个路径。但ehcache.xml
必须有。
在 resources 目录下,添加 ehcache 的配置文件 ehcache.xml ,文件内容如下:
<ehcache>
<diskStore path="java.io.tmpdir/cache_dongbb"/>
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="false"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
/>
<cache name="article"
maxElementsInMemory="10000"
eternal="true"
overflowToDisk="true"
diskPersistent="true"
diskExpiryThreadIntervalSeconds="600"/>
</ehcache>
配置含义:
- name:缓存名称,与缓存注解的value(cacheNames)属性值相同。
- maxElementsInMemory:缓存最大个数。
- eternal:缓存对象是否永久有效,一但设置了,timeout将不起作用。
- timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
- timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
- overflowToDisk:当内存中对象数量达到maxElementsInMemory时,Ehcache将会对象写到磁盘中。
- diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
- maxElementsOnDisk:硬盘最大缓存个数。
- diskPersistent:是否缓存虚拟机重启期数据。
- diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
- memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
- clearOnFlush:内存数量最大时是否清除。
- diskStore 则表示临时缓存的硬盘目录。
三. 缓存的使用方法
3.1 缓存注解-增删改查
- @Cacheable:针对查询方法配置,能够根据查询方法的请求参数对其结果进行缓存
- @CacheEvict:被注解的方法执行前或者执行之后,删除缓存
- @CachePut:调用被注解的方法,对其返回结果进行缓存更新
- @Caching:可以将上面三种注解,组合起来使用
3.2 单个对象的查询缓存
被@Cacheable
注解的方法,在第一次被请求的时候执行方法体,并将方法的返回值放入缓存。在第二次请求的时候,由于缓存中已经包含该数据,将不执行被注解的方法的方法体,直接从缓存中获取数据。对于查询过程的缓存操作,要满足上图中的蓝色箭头线指引的操作流程,所有的操作流程只需要加上一个@Cacheable
就可以实现。
public static final String CACHE_OBJECT = "article"; //缓存名称
@Cacheable(value = CACHE_OBJECT,key = "#id") //这里的value和key参考下面的redis数据库截图理解
public ArticleVO getArticle(Long id) {
return dozerMapper.map(articleMapper.selectById(id),ArticleVO.class);
}
需要注意的是:缓存注解的key是一个SPEL表达式,“#id”表示获取函数的参数id的值作为缓存的key值。如果参数id=1,那么最终redis缓存的key就是:“article::1”。
3.3 集合对象的查询缓存
大家要注意Object
和List<Object>
是两种不同的业务数据,所以对应的缓存也是两种缓存。注意下文中,缓存注解的key是字符串list,因为缓存注解默认使用SPEL表达式,如果我们想使用字符串需要加上斜杠。
public static final String CACHE_LIST_KEY = "\"list\"";
@Cacheable(value = CACHE_OBJECT,key = CACHE_LIST_KEY)
public List<ArticleVO> getAll() {
List<Article> articles = articleMapper.selectList(null);
return DozerUtils.mapList(articles,ArticleVO.class);
}
对于查询过程的缓存操作,要满足上图中的蓝色箭头线指引的操作流程,所有的操作流程只需要加上一个@Cacheable
就可以实现。目前MySQL数据库的article表有4条数据,所以缓存结果是一个包含4个article元素的数组
3.4 删除单个对象及其缓存
如下面的代码所示,将在函数执行成功之后删除缓存 key为“article::1”的缓存(假设删除id=1的记录)。
@Override
@Caching(evict = {
@CacheEvict(value = CACHE_OBJECT,key = CACHE_LIST_KEY), //删除List集合缓存
@CacheEvict(value = CACHE_OBJECT,key = "#id") //删除单条记录缓存
})
public void deleteArticle(Long id) {
articleMapper.deleteById(id);
}
- 执行该方法传递参数id=1,先执行方法去操作删除MySQL数据;成功之后将key为“article::1”的缓存也将被删除。
- 任何一个article记录被删除,都会引起article::list缓存与MySQL数据库记录不一致的情况,所以需要把article::list集合缓存也删除掉。
- 因为Java 语法不允许在同一个方法上使用两个同样的注解
@CacheEvict
,所以我们用@Caching
注解把两个@CacheEvict
包起来。
3.5 新增一个对象
- 新增MySQL数据的时候新增缓存么?不是的,缓存是在获得查询结果时候回写到缓存里面的,不在新增的时候加缓存。
- 新增的时候删除缓存么“?是的,因为我们缓存了List的集合,一旦新增一条记录。原来MySQL数据库有4条记录,新增之后MySQL数据库有5条记录,缓存缓存数据库缓存结果”article::list“仍然有4条记录。缓存中的数据与MYSQL数据库中的数据不一致,所以把”article::list“缓存删掉。
@CacheEvict(value = CACHE_OBJECT,key = CACHE_LIST_KEY) //删除List集合缓存
public void saveArticle(ArticleVO article) {
Article articlePO = dozerMapper.map(article, Article.class);
articleMapper.insert(articlePO);
}
执行完成上面的方法,MySQL数据库新增了一条article记录;成功之后缓存到中的key为“article::list”的缓存也将被删除。
3.6 更新一个对象
注意更新对象的时候,我们在该方法上面加了两个缓存注解。
- 下文的CachePut注解的作用是在方法执行成功之后,将其返回值放入缓存。
key = "#article.getId()"
表示使用参数article的id属性作为缓存key。 - 下文的CacheEvict注解用于将“article::list”的缓存删除,因为某一条记录的数据更新,就表示原来缓存的List集合数据与MySQL数据库中的数据不一致,所以把它删除掉。缓存数据可以没有,但是不能和后端被缓存的关系数据库数据不一致。
@CachePut(value = CACHE_OBJECT,key = "#article.getId()")
@CacheEvict(value = CACHE_OBJECT,key = CACHE_LIST_KEY)
public ArticleVO updateArticle(ArticleVO article) {
Article articlePO = dozerMapper.map(article,Article.class);
articleMapper.updateById(articlePO);
return article; //为了保证一致性,最后返回的更新结果,最好从数据库去查
}
执行完成该方法,假如ArticleVO参数对象的id=1
- MySQL数据库中的id=1的记录将被更新
- 缓存数据库中”article::1“的记录也将被更新(CachePut)
- 缓存数据库中”article::list“的记录将被删除(CacheEvict)
3.7 更新一个对象(另一种方法)
需要特别注意的是:如果在更新方法上使用CachePut注解,该方法一定要有数据更新之后返回值,因为返回值就是缓存值。比较简单的做法是直接将不一致的缓存删掉,而不是去更新缓存。这样操作对于程序员的要求更低,不容易出错。缓存数据可以没有,但是不能和后端被缓存的关系数据库数据不一致。
@Override
@Caching(evict = {
@CacheEvict(value = CACHE_OBJECT,key = CACHE_LIST_KEY), //删除List集合缓存
@CacheEvict(value = CACHE_OBJECT,key = "#article.getId()") //删除单条记录缓存
})
public void updateArticle(ArticleVO article) {
Article articlePO = dozerMapper.map(article,Article.class);
articleMapper.updateById(articlePO);
}
方法不需要有返回值。执行完成该方法,假如ArticleVO参数对象的id=1
- MySQL数据库中的id=1的记录将被更新
- 缓存数据库中”article::1“的记录将被删除
- 缓存数据库中”article::list“的记录将被删除
四.缓存注解配置说明
@Cacheable 通常应用到读取数据的查询方法上:先从缓存中读取,如果没有再调用方法获取数据,然后把数据查询结果添加到缓存中。如果缓存中查找到数据,被注解的方法将不会执行。
@Cacheable 主要的参数 | 参数说明 | 示例 |
---|---|---|
value(cacheNames) | 缓存的名称,在 spring 配置文件中定义,必须指定至少一个。当value为数组的时候会针对同一条记录做多个缓存。 | 例如: @Cacheable(value=”mycache”) 或者 @Cacheable(value={”cache1”,”cache2”}) |
key | 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 | 例如: @Cacheable(value=”testcache”,key=”#userName”) |
condition | 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 | 例如: @Cacheable(value=”testcache”,condition=”#userName.length()>2”) |
@CachePut通常应用于修改方法配置,能够根据方法的请求参数对其注解的函数返回值进行缓存,和 @Cacheable 不同的是,它每次都会触发被注解方法的调用。
@CachePut 主要的参数 | 参数说明 | 示例 |
---|---|---|
value(cacheNames) | 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 | 例如: @Cacheable(value=”mycache”) 或者 @Cacheable(value={”cache1”,”cache2”} |
key | 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 | 例如: @Cacheable(value=”testcache”,key=”#userName”) |
condition | 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 | 例如: @Cacheable(value=”testcache”,condition=”#userName.length()>2”) |
@CachEvict 通常应用于删除方法配置,能够根据一定的条件对缓存进行删除。可以清除一条或多条缓存。
@CacheEvict 主要的参数 | 参数说明 | 示例 |
---|---|---|
value(cacheNames) | 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 | 例如: @CachEvict(value=”mycache”) 或者 @CachEvict(value={”cache1”,”cache2”} |
key | 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 | 例如: @CachEvict(value=”testcache”,key=”#userName”) |
condition | 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才清空缓存 | 例如: @CachEvict(value=”testcache”, condition=”#userName.length()>2”) |
allEntries | 是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存 | 例如: @CachEvict(value=”testcache”,allEntries=true) |
beforeInvocation | 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存 | 例如:@CachEvict(value=”testcache”,beforeInvocation=true) |
在实际的生产环境中,没有一定之规,哪种注解必须用在哪种方法上,@CachEvict 注解通常也用于更新方法上。数据的缓存策略,要根据资源的使用方式,做出合理的缓存策略规划。保证缓存与业务数据库的数据一致性。并做好测试,对于缓存的正确使用,测试才是王道!