撰文背景
公司开发中的一个驱动模块,需要用到本地缓存,来提高驱动模块的访问速度和性能,然后就想到了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是一个开源的基于标准的缓存,是一个纯java的在进程中的缓存,可提高性能,卸载数据库并简化可伸缩性。它是使用最广泛的基于Java的高速缓存,因为它非常强大,经过验证,功能全面,并且与其他流行的库和框架集成在一起。Ehcache从进程内缓存扩展到混合进程内/进程外部署与TB级缓存。
EHCache是一个快速的、轻量级的、易于使用的、进程内的缓存。它支持read-only 和 read/write 缓存,内存和磁盘缓存。是一个非常轻量级的缓存实现,而且从 1.2 之后就支持了集群。
现在的Ehcache已经更新到了3.5版本,版本3加入一些新的功能,包括
与Ehcache 2.x相比,Ehcache 3具有简化,现代化的类型安全API(和配置),可以大大提高您的编码体验。
特点:
存储方式:
对应的jar包下载地址:点击打开链接
又或者可以采用添加pom依赖的方式来添加依赖包:
org.ehcache
ehcache
3.5.2
二、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 文件目录
以下有个范例:
maxElementsInMemory="900"<--缓存的最大数目-->
overflowToDisk="false" <--内存不足时,是否启用磁盘缓存,如果为true则表示启动磁盘来存储,如果为false则表示不启动磁盘-->
diskPersistent="false"
timeToIdleSeconds="0" <--当缓存的内容闲置多少时间销毁-->
timeToLiveSeconds="60" <--当缓存存活多少时间销毁(单位是秒,如果我们想设置2分钟的缓存存活时间,那么这个值我们需要设置120)-->
memoryStoreEvictionPolicy="LRU" /> <--自动销毁策略-->
Ehcache 的三种清空策略
1 FIFO,first in first out,这个是大家最熟的,先进先出。
2 LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来
最少被使用的。如上面所讲,缓存的元素有一个 hit 属性,hit 值最小的将会被清出缓存。
3 LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量
满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时
间最远的元素将被清出缓存。
接着我们来看一下SimplePageCachingFilter 的配置,
XML/HTML 代码
indexCacheFilterfilter-name>Page9 of 26
net.sf.ehcache.constructs.web.filter.SimplePageCachingFilter
indexCacheFilterfilter-name>
*index.actionurl-pattern>
就只需要这么多步骤,我们就可以给某个页面做一个缓存的,把上面这段配置放到你的web.xml 中,那么当你打开首页的时候,你会发现,2 分钟才会有一堆 sql 语句出现在控制台上。当然你也可以调成5 分钟,总之一切都在控制中。好了,缓存整个页面看上去是非常的简单,甚至都不需要写一行代码,只需要几行配置就行了,够简单吧,虽然看上去简单,但是事实上内部实现却不简单哦,有兴趣的话,大家可以看看SimplePageCachingFilter 继承体系的源代码。
上面的配置针对的情况是缓存首页的全部,如果你只想缓存首页的部分内容时,你需要使用SimplePageFragmentCachingFilter 这个 filter。我们看一下如下片断:
XML/HTML 代码
indexCacheFilterfilter-name>
net.sf.ehcache.constructs.web.filter.SimplePageFragmentCachingFilter
filter>
indexCacheFilterfilter-name>
*/index_right.jspurl-pattern>
这个 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
@EnableCaching
public 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
*/
@Service
public 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 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 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 中缓存的原理,使用范围,适用场景等等都需要有比较深刻的理解,这样才能用好缓存,用对缓存。