整合SpringCache与EhCache实现本地缓存

一.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>

配置含义:

  1. name:缓存名称,与缓存注解的value(cacheNames)属性值相同。
  2. maxElementsInMemory:缓存最大个数。
  3. eternal:缓存对象是否永久有效,一但设置了,timeout将不起作用。
  4. timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
  5. timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
  6. overflowToDisk:当内存中对象数量达到maxElementsInMemory时,Ehcache将会对象写到磁盘中。
  7. diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
  8. maxElementsOnDisk:硬盘最大缓存个数。
  9. diskPersistent:是否缓存虚拟机重启期数据。
  10. diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
  11. memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
  12. clearOnFlush:内存数量最大时是否清除。
  13. 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 集合对象的查询缓存

大家要注意ObjectList<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 注解通常也用于更新方法上。数据的缓存策略,要根据资源的使用方式,做出合理的缓存策略规划。保证缓存与业务数据库的数据一致性。并做好测试,对于缓存的正确使用,测试才是王道

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352

推荐阅读更多精彩内容