SpringBoot+EHcache实现缓存

首先给大家推荐一下我老师大神的人工智能教学网站。教学不仅零基础,通俗易懂,而且非常风趣幽默,还时不时有内涵黄段子!点这里可以跳转到网站

撰文背景

公司开发中的一个驱动模块,需要用到本地缓存,来提高驱动模块的访问速度和性能,然后就想到了Ehcache缓存,Ehcache是Hibernate 中默认的CacheProvider,hibernate就是使用Ehcache来实现二级缓存的。本质上来说Ehcache是一个缓存管理器,不仅仅可以和Hibernate配合实现缓存,也可以和其他框架比如spring boot 结合,作为一个缓存管理器,缓存管理器有很多,但是公司的项目说实话只是用到了Ehcache的本地缓存,Ehcache还支持分布式的缓存,虽然公司的项目没有用到,但是本着深究到底的想法,还是想深入的把Ehcache详细的学习下,下面就是我学习中的一些总结,算是做个记录。

常见的缓存管理器:

* Generic 
* JCache (JSR-107) 
* EhCache 2.x 
* Hazelcast 
* Infinispan 
* Redis 
* Guava 

* Simple

spring boot本身是提供数据缓存的功能,SpringBoot自带的cache技术我想大家都应该用过,为了解决数据库输入输出的瓶颈所以一般情况下我们都会引入非常多的缓存策略,例如引入redis缓存,引入Hibernate的二级缓存等等。

SpringBoot在annotation的层面给我们实现了cache,当然这也是得益于Spring的AOP。所有的缓存配置只是在annotation层面配置,完全没有侵入到我们的代码当中,就像我们的声明式事务一样。

Spring定义了CacheManager和Cache接口统一不同的缓存技术。其中CacheManager是Spring提供的各种缓存技术的抽象接口。而Cache接口包含缓存的各种操作,当然我们一般情况下不会直接操作Cache接口。

Spring针对不同的缓存技术,需要实现不同的cacheManager,Spring定义了如下的cacheManger实现

CacheManger描述
SimpleCacheManager使用简单的Collection来存储缓存,主要用于测试
ConcurrentMapCacheManager使用ConcurrentMap作为缓存技术(默认)
NoOpCacheManager测试用
EhCacheCacheManager使用Ehcache作为缓存技术,以前在HIbernate的时候经常用
GuavaCacheManager使用google guava的GuavaCache作为缓存技术
HazelcastCacheManager使用Hazelcast作为缓存技术
JCacheCacheManager使用JCache标准的实现作为缓存技术,如Apache Commons JCS
RedisCacheManager使用Redis作为缓存技术

常规的SpringBoot已经为我们自动配置了EhCache、Collection、Guava、ConcurrentMap等缓存,默认使用SimpleCacheConfiguration,即使用ConcurrentMapCacheManager。EHcache官网:点击打开链接

一、EHcache简介

Ehcache是​​一个开源的基于标准的缓存,是一个纯java的在进程中的缓存,可提高性能,卸载数据库并简化可伸缩性。它是使用最广泛的基于Java的高速缓存,因为它非常强大,经过验证,功能全面,并且与其他流行的库和框架集成在一起。Ehcache从进程内缓存扩展到混合进程内/进程外部署与TB级缓存。

EHCache是一个快速的、轻量级的、易于使用的、进程内的缓存。它支持read-only 和 read/write 缓存,内存和磁盘缓存。是一个非常轻量级的缓存实现,而且从 1.2 之后就支持了集群。

现在的Ehcache已经更新到了3.5版本,版本3加入一些新的功能,包括

  • 改进了API,可以利用Java泛型并简化缓存交互,
  • 与javax.cache API(JSR-107)完全兼容,
  • Offheap存储功能,包括仅堆高速缓存,
  • Spring Caching和Hibernate集成得益于javax.cache支持,
  • 还有很多 …

与Ehcache 2.x相比,Ehcache 3具有简化,现代化的类型安全API(和配置),可以大大提高您的编码体验。

特点:

  1.  快速.
  2. 简单.
  3. 多种缓存策略
  4. 缓存数据有两级:内存和磁盘,因此无需担心容量问题
  5. 缓存数据会在虚拟机重启的过程中写入磁盘
  6. 可以通过 RMI、可插入 API 等方式进行分布式缓存
  7. 具有缓存和缓存管理器的侦听接口
  8. 支持多缓存管理器实例,以及一个实例的多个缓存区域
  9. 提供 Hibernate 的缓存实现

存储方式:

  1. 内存
  2. 磁盘

对应的jar包下载地址:点击打开链接

又或者可以采用添加pom依赖的方式来添加依赖包:

    <dependency>      <groupId>org.ehcache</groupId>      <artifactId>ehcache</artifactId>      <version>3.5.2</version>    </dependency> 

二、EHCache页面缓存的配置

1.EHCache 的类层次模型

主要为三层,最上层的是 CacheManager,他是操作Ehcache 的入口。我们可以通过CacheManager.getInstance()获得一个单子的 CacheManger,或者通过CacheManger 的构造函数创建 一个新的CacheManger。每个 CacheManager 都管理着多个 Cache。而每个Cache 都以一种类 Hash 的方式,关联着多个Element。Element 则是我们用于存放要缓存内容的地方

2.ehcache配置文件中元素说明

ehcach.xml配置文件主要参数的解释,其实文件里有详细的英文注释//DiskStore 配置,

cache 文件的存放目录,主要的值有

* user.home – 用户主目录

* user.dir – 用户当前的工作目录

* java.io.tmpdir – Default temp file path 默认的 temp 文件目录

以下有个范例:

<?xml version="1.0" encoding="UTF-8"?><ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"         updateCheck="false">    <diskStore path = "Java.io.tmpdir"/>     <defaultCache            eternal="false" <--意味着该缓存会死亡-->            maxElementsInMemory="900"<--缓存的最大数目-->            overflowToDisk="false" <--内存不足时,是否启用磁盘缓存,如果为true则表示启动磁盘来存储,如果为false则表示不启动磁盘-->            diskPersistent="false"             timeToIdleSeconds="0"  <--当缓存的内容闲置多少时间销毁-->            timeToLiveSeconds="60" <--当缓存存活多少时间销毁(单位是秒,如果我们想设置2分钟的缓存存活时间,那么这个值我们需要设置120)-->            memoryStoreEvictionPolicy="LRU" /> <--自动销毁策略-->    <!-- 这里的 users 缓存空间是为了下面的 demo 做准备 -->    <cache            name="data"            eternal="false"            maxElementsInMemory="200"            overflowToDisk="false"            diskPersistent="false"            timeToIdleSeconds="0"            timeToLiveSeconds="60"            memoryStoreEvictionPolicy="LRU" /></ehcache><!--<diskStore>==========当内存缓存中对象数量超过maxElementsInMemory时,将缓存对象写到磁盘缓存中(需对象实现序列化接口)  --><!--<diskStore path="">==用来配置磁盘缓存使用的物理路径,Ehcache磁盘缓存使用的文件后缀名是*.data和*.index  --><!--name=================缓存名称,cache的唯一标识(ehcache会把这个cache放到HashMap里)  --><!--maxElementsOnDisk====磁盘缓存中最多可以存放的元素数量,0表示无穷大  --><!--maxElementsInMemory==内存缓存中最多可以存放的元素数量,若放入Cache中的元素超过这个数值,则有以下两种情况  -->                     <!--1)若overflowToDisk=true,则会将Cache中多出的元素放入磁盘文件中  -->                     <!--2)若overflowToDisk=false,则根据memoryStoreEvictionPolicy策略替换Cache中原有的元素  --><!--eternal==============缓存中对象是否永久有效,即是否永驻内存,true时将忽略timeToIdleSeconds和timeToLiveSeconds  --><!--timeToIdleSeconds====缓存数据在失效前的允许闲置时间(单位:秒),仅当eternal=false时使用,默认值是0表示可闲置时间无穷大,此为可选属性  -->                     <!--即访问这个cache中元素的最大间隔时间,若超过这个时间没有访问此Cache中的某个元素,那么此元素将被从Cache中清除  --><!--timeToLiveSeconds====缓存数据在失效前的允许存活时间(单位:秒),仅当eternal=false时使用,默认值是0表示可存活时间无穷大  -->                     <!--即Cache中的某元素从创建到清楚的生存时间,也就是说从创建开始计时,当超过这个时间时,此元素将从Cache中清除  --><!--overflowToDisk=======内存不足时,是否启用磁盘缓存(即内存中对象数量达到maxElementsInMemory时,Ehcache会将对象写到磁盘中)  -->                     <!--会根据标签中path值查找对应的属性值,写入磁盘的文件会放在path文件夹下,文件的名称是cache的名称,后缀名是data  --><!--diskPersistent=======是否持久化磁盘缓存,当这个属性的值为true时,系统在初始化时会在磁盘中查找文件名为cache名称,后缀名为index的文件  -->                     <!--这个文件中存放了已经持久化在磁盘中的cache的index,找到后会把cache加载到内存  -->                     <!--要想把cache真正持久化到磁盘,写程序时注意执行net.sf.ehcache.Cache.put(Element element)后要调用flush()方法  --><!--diskExpiryThreadIntervalSeconds==磁盘缓存的清理线程运行间隔,默认是120秒  --><!--diskSpoolBufferSizeMB============设置DiskStore(磁盘缓存)的缓存区大小,默认是30MB  --><!--memoryStoreEvictionPolicy========内存存储与释放策略,即达到maxElementsInMemory限制时,Ehcache会根据指定策略清理内存  -->                                 <!--共有三种策略,分别为LRU(Least Recently Used 最近最少使用)、LFU(Less Frequently Used最不常用的)、FIFO(first in first out先进先出)  -->

Ehcache 的三种清空策略

1 FIFO,first in first out,这个是大家最熟的,先进先出。

2 LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来

最少被使用的。如上面所讲,缓存的元素有一个 hit 属性,hit 值最小的将会被清出缓存。

3 LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量

满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时

间最远的元素将被清出缓存。

接着我们来看一下SimplePageCachingFilter 的配置,

XML/HTML 代码

<filter> <filter-name>indexCacheFilterfilter-name>Page9 of 26 <filter-class> net.sf.ehcache.constructs.web.filter.SimplePageCachingFilter <filter-class> <filter> <filter-mapping> <filter-name>indexCacheFilterfilter-name> <url-pattern>*index.actionurl-pattern> <filter-mapping>

就只需要这么多步骤,我们就可以给某个页面做一个缓存的,把上面这段配置放到你的web.xml 中,那么当你打开首页的时候,你会发现,2 分钟才会有一堆 sql 语句出现在控制台上。当然你也可以调成5 分钟,总之一切都在控制中。好了,缓存整个页面看上去是非常的简单,甚至都不需要写一行代码,只需要几行配置就行了,够简单吧,虽然看上去简单,但是事实上内部实现却不简单哦,有兴趣的话,大家可以看看SimplePageCachingFilter 继承体系的源代码。

上面的配置针对的情况是缓存首页的全部,如果你只想缓存首页的部分内容时,你需要使用SimplePageFragmentCachingFilter 这个 filter。我们看一下如下片断:

XML/HTML 代码

<filter><filter-name>indexCacheFilterfilter-name><filter-class>net.sf.ehcache.constructs.web.filter.SimplePageFragmentCachingFilter<filter-class>filter><filter-mapping><filter-name>indexCacheFilterfilter-name><url-pattern>*/index_right.jspurl-pattern><filter-mapping>

这个 jsp 需要被 jsp:include 到其他页面,这样就做到的局部页面的缓存。这一点貌似没有 oscache 的 tag 好用。事实上在cachefilter 中还有一个特性,就是 gzip,也就是说缓存中的元素是被压缩过的,如果客户浏览器支持压缩的话,filter会直接返回压缩过的流,这样节省了带宽,把解压的工作交给了客户浏览器,如果客户的浏览器不支持 gzip,那么 filter 会把缓存的元素拿出来解压后再返回给客户浏览器(大多数爬虫是不支持 gzip 的,所以 filter 也会解压后再返回流),这样做的优点是节省带宽,缺点就是增加了客户浏览器的负担(但是我觉得对当代的计算机而言,这个负担微乎其微)。好了,如果你的页面正好也需要用到页面缓存,不防可以考虑一下 ehcache,因为它实在是非常简单,而且易用。

三、spring boot + mybatis + Ehcache 实现本地缓存

这个例子其实关于本地缓存我这写了一个例子,这个例子是一个springboot工程项目,集成了mybatis来进行数据库的访问,只是一个简单的数据库表操作,只是在具体的方法上添加了相应的注解,从而实现了,本地缓存。没有用到Ehcache集群和分布式,只是将信息缓存到内存中,从而降低数据库的之间的访问,提高数据的访问速度。

github地址:点击打开链接

核心的代码我来讲解下:

(1)EhcacheProviderApplication启动类

package com.huntkey.rx.ehcache.provider; import org.mybatis.spring.annotation.MapperScan;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.EnableAutoConfiguration;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;import org.springframework.cache.annotation.EnableCaching;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;import org.springframework.cloud.config.server.EnableConfigServer; /** * Created by sunwei on 2017/12/20 Time:17:52 *//** * 链接Mysql数据库简单的集成Mybatis、ehcache框架采用MapperXml访问数据库。 * * 简单用户链接Mysql数据库微服务(通过 mybatis 链接 mysql 并用 MapperXml 编写数据访问,并且通过 EhCache 缓存来访问)。 */@EnableDiscoveryClient@SpringBootApplication@EnableCachingpublic class EhcacheProviderApplication {    public static void main(String[] args) {        SpringApplication.run(EhcacheProviderApplication.class,args);        System.out.println("【【【【【【 链接MysqlMybatisMapperEhCache数据库微服务 】】】】】】已启动.");    }}

如果想用Ehcache缓存,在启动类上一定要加上 @EnableCaching注解,否则缓存会不起作用。

(2)UserServiceImpl实现类

package com.huntkey.rx.ehcache.provider.service.impl; import com.github.pagehelper.util.StringUtil;import com.huntkey.rx.ehcache.common.model.User;import com.huntkey.rx.ehcache.common.util.Result;import com.huntkey.rx.ehcache.provider.dao.UserDao;import com.huntkey.rx.ehcache.provider.service.UserService;import com.mysql.jdbc.StringUtils;import com.sun.org.apache.regexp.internal.RE;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.cache.annotation.CacheEvict;import org.springframework.cache.annotation.Cacheable;import org.springframework.stereotype.Service; import java.util.List; /** * Created by sunwei on 2017/12/20 Time:15:53 */@Servicepublic class UserServiceImpl implements UserService {     private Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);     private static final String CACHE_KEY = "'user'";    private static final String CACHE_NAME_B = "cache-b";     //* @Cacheable : Spring在每次执行前都会检查Cache中是否存在相同key的缓存元素,如果存在就不再执行该方法,而是直接从缓存中获取结果进行返回,否则才会执行并将返回结果存入指定的缓存中。    //* @CacheEvict : 清除缓存。    //* @CachePut : @CachePut也可以声明一个方法支持缓存功能。使用@CachePut标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。     @Autowired    private UserDao userDao;      @CacheEvict(value = CACHE_NAME_B, key = CACHE_KEY)    @Override    public int insert(User user) {        return userDao.insert(user);    }     @CacheEvict(value = CACHE_NAME_B, key = "'user_' + #id") //这是清除缓存    @Override    public int deleteByPrimaryKey(String id) {        Result result = new Result();        return userDao.deleteByPrimaryKey(id);    }     @CacheEvict(value = CACHE_NAME_B, key = "'user_'+ #user.id")    @Override    public User updateByPrimaryKey(User user) {        userDao.updateByPrimaryKey(user);        return user;    }     @Cacheable(value = CACHE_NAME_B, key = "'user_'+ #id")    @Override    public User selectByPrimaryKey(String id) {        return userDao.selectByPrimaryKey(id);    }     @Override    public List<User> selectAllUser() {        return userDao.selectAllUser();    }     @Cacheable(value = CACHE_NAME_B, key = "#userId+'_'+#userName")    @Override    public Result selectUserByAcount(Integer userId, String userName) {        Result result = new Result();        try {            List<User> list = userDao.selectUserByAcount(userId, userName);            if (list.size() == 0 || list.isEmpty()) {                result.setRetCode(Result.RECODE_ERROR);                result.setErrMsg("查找数据不存在");                return result;            }            result.setData(list);        } catch (Exception e) {            result.setRetCode(Result.RECODE_ERROR);            result.setErrMsg("方法执行出错");            logger.error("方法执行出错", e);            throw new RuntimeException(e);        }        return result;    }}

然后我们开始讲讲四个注解:

@Cacheable  在方法执行前Spring先是否有缓存数据,如果有直接返回。如果没有数据,调用方法并将方法返回值存放在缓存当中。

这个注解会先查询是否有缓存过的数据,如果有,就直接返回原来缓存好的数据,如果没有,则再执行一次方法,将方法的返回结果放到缓存中。

@CachePut   无论怎样,都将方法的返回结果放到缓存当中。

这个注解不会询问是否有缓存好的数据,而是每次都会执行方法,将方法的返回结果放到缓存中,相当于每次都更新缓存中的数据,每次缓存中的数据都是最新的一次缓存数据。

@CacheEvict   将一条或者多条数据从缓存中删除。

这个是删除一条或者多条的缓存数据。

@Caching  可以通过@Caching注解组合多个注解集合在一个方法上

这个注解可以组合多个注解,从而实现自定义注解

注意:缓存其实存放的是以注解里面的key为key 方法的返回值作为key的value,不是注解里面的value。

总结:

ehcache 是一个非常轻量级的缓存实现,而且从 1.2 之后就支持了集群,目前的最新版本是 1.3,而且是 hibernate 默认的缓存provider。虽然本文是介绍的是 ehcache 对页面缓存的支持,但是ehcache 的功能远不止如此,当然要使用好缓存,对 JEE 中缓存的原理,使用范围,适用场景等等都需要有比较深刻的理解,这样才能用好缓存,用对缓存。

点这里可以跳转到人工智能网站

发表评论