一 介绍
EhCache 是一个纯Java
的进程内缓存框架
,具有快速、精干等特点,是Hibernate中默认CacheProvider。Ehcache是一种广泛使用的开源Java分布式缓存。主要面向通用缓存,Java EE和轻量级容器。它具有内存
和磁盘
存储,缓存加载器,缓存扩展,缓存异常处理程序,一个gzip缓存servlet过滤器,支持REST和SOAP api等特点。
特性
- 快速、简单
- 多种
缓存策略
- 缓存数据有两级:
内存和磁盘
,因此无需担心容量问题
- 缓存数据会在虚拟机
重启
的过程中写入磁盘
- 可以通过
RMI
、可插入API等方式进行分布式缓存
- 具有缓存和缓存管理器的侦听接口
- 支持
多
缓存管理器实例
,以及一个实例的多个缓存区域
- 提供
Hibernate
的缓存实现
集成
可以单独使用,一般在第三方库中被用到的比较多(如mybatis、shiro等)ehcache 对分布式支持不够好
,多个节点不能同步
,通常和redis一块使用
灵活性
ehcache具备对象api接口
和可序列化api接口
不能序列化的对象
可以使用出磁盘存储外ehcache
的所有功能
支持基于Cache和基于Element的过期策略,每个Cache的存活时间都是可以设置和控制的。
提供了LRU、LFU和FIFO缓存淘汰算法,Ehcache 1.2引入了最少使用和先进先出缓存淘汰算法,构成了完整的缓存淘汰算法。
提供内存和磁盘存储,Ehcache和大多数缓存解决方案一样,提供高性能的内存和磁盘存储。
动态、运行时缓存配置,存活时间、空闲时间、内存和磁盘存放缓存的最大数目都是可以在运行时修改的。
应用持久化
在vm重启
后,持久化到磁盘的存储可以复原数据
Ehache是第一个引入缓存数据持久化存储的开源java缓存框架,缓存的数据可以在机器重启后从磁盘上重新获得
根据需要将缓存刷到磁盘。将缓存条目刷到磁盘
的操作可以通过cache.fiush
方法执行,这大大方便了ehcache的使用
ehcache 和 redis 比较
- ehcache直接在jvm虚拟机中缓存,
速度快
,效率高;但是缓存共享麻烦
,集群分布式应用不方便。 - redis是通过socket访问到缓存服务,效率比ecache低,比数据库要快很多,
处理集群和分布式缓存方便,有成熟的方案。如果是单个应用
或者对缓存访问要求很高
的应用,用ehcache。如果是大型系统,存在缓存共享、分布式部署、缓存内容很大
的,建议用redis。
二 Hello World
依赖
net.sf.ehcache
ehcache
2.10.2
junit
junit
4.12
test
配置文件
测试类
package com.zyc;
import org.junit.Test;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
public class Test1 {
@Test
public void test1() {
// 1. 创建缓存管理器
CacheManager cacheManager = CacheManager.create("./src/main/resources/ehcache.xml");
// 2. 获取缓存对象
Cache cache = cacheManager.getCache("HelloWorldCache");
// 3. 创建元素
Element element = new Element("key1", "value1");
// 4. 将元素添加到缓存
cache.put(element);
// 5. 获取缓存
Element value = cache.get("key1");
System.out.println(value);
System.out.println(value.getObjectValue());
// 6. 删除元素
cache.remove("key1");
Person p1 = new Person("小明",18,"杭州");
Element pelement = new Element("xm", p1);
cache.put(pelement);
Element pelement2 = cache.get("xm");
System.out.println(pelement2.getObjectValue());
System.out.println(cache.getSize());
// 7. 刷新缓存
cache.flush();
// 8. 关闭缓存管理器
cacheManager.shutdown();
}
}
三 配置文件说明
diskStore
- path :指定磁盘存储的位置
defaultCache
默认的缓存
cache
自定的缓存,当自定的配置不满足实际情况时可以通过自定义(可以包含多个cache节点
)
-
name
: 缓存的名称,可以通过指定名称
获取指定的某个Cache对象
-
maxElementsInMemory
:内存中允许存储的最大的元素个数
,0代表无限
个 -
clearOnFlush
:内存数量最大时是否清除
。 -
eternal
:设置缓存中对象是否为永久
的,如果是,超时设置将被忽略
,对象从不过期。根据存储数据的不同,例如一些静态不变的数据如省市区等可以设置为永不过时 -
timeToIdleSeconds
: 设置对象在失效前
的允许闲置
时间(单位:秒)。仅当eternal=false
对象不是
永久有效时使用
,可选属性,默认值是0,也就是可闲置时间无穷大
。 -
timeToLiveSeconds
:缓存数据的生存时间
(TTL),也就是一个元素从构建到消亡的最大时间间隔值,这只能在元素不是永久驻留时有效,如果该值是0就意味着元素可以停顿无穷长的时间。(和上面的两者取最小值
) -
overflowToDisk
:内存不足时,是否启用磁盘
缓存。 -
maxEntriesLocalDisk
:当内存中对象数量达到maxElementsInMemory时,Ehcache将会对象写到磁盘中。 -
maxElementsOnDisk
:硬盘最大缓存个数。 -
diskSpoolBufferSizeMB
:这个参数设置DiskStore(磁盘缓存
)的缓存区大小
。默认是30MB。每个Cache都应该有自己的一个缓冲区。 -
diskPersistent
:是否在VM重启时存储硬盘的缓存数据
。默认值是false。
-
diskExpiryThreadIntervalSeconds
:磁盘失效线程
运行时间间隔,默认是120秒。 -
memoryStoreEvictionPolicy
:当达到maxElementsInMemory
限制时,Ehcache将会根据指定的策略
去清理内存。默认策略是LRU(最近最少使用)
。你可以设置为FIFO(先进先出
)或是LFU(较少使用)
。这里比较遗憾,Ehcache并没有提供一个用户定制策略的接口,仅仅支持三种指定策略,感觉做的不够理想。
编程方式配置
Cache cache = manager.getCache("mycache");
CacheConfiguration config = cache.getCacheConfiguration();
config.setTimeToIdleSeconds(60);
config.setTimeToLiveSeconds(120);
config.setmaxEntriesLocalHeap(10000);
config.setmaxEntriesLocalDisk(1000000);
持久化配置
类必须实现序列化接口,不需要的属性用transient
x修饰
这种是所有数据都放到磁盘里去了
自己决定说明时候持久化
测试得出以下两个方法在配置持久化环境的情况下
都会将内存中的数据放到磁盘上
cache.flush();
// 8. 关闭缓存管理器
cacheManager.shutdown()
自动持久化
想利用spring 的注解,不想手动shutdown ,因此web.xml 配置listener 监听,在销毁的时候进行shutdown,这里利用ehcache 的监听.
net.sf.ehcache.constructs.web.ShutdownListener
直接杀死线程,这肯定监听不到。
更多资料(未测试过):记一次,ehcache缓存到磁盘,再恢复的过程
spring持久化测试情况
通过注解调用发现,每次test结束后,都会自动持久化
@Test
public void testPersist(){
System.out.println(ehcacheService.getDataFromDB("tt1"));
System.out.println(ehcacheService.getDataFromDB("tt1"));
}
@Cacheable(value="HelloWorldCache", key="#key")
@Override
public String getDataFromDB(String key) {
System.out.println("从数据库中获取数据...");
return key + ":" + String.valueOf(Math.round(Math.random()*1000000));
}
四 一致性模型
说到一致性,数据库的一致性
是怎样的?不妨先来回顾一下数据库的几个隔离级别:
未提交读(Read Uncommitted
):在读数据时不会检查或使用任何锁。因此,在这种隔离级别中可能读取到没有提交的数据。会出现脏读、不可重复读、幻象读。
已提交读(Read Committed
):只读取提交的数据并等待其他事务释放排他锁。读数据的共享锁在读操作完成后立即释放。已提交读是数据库的默认隔离级别。会出现不可重复读、幻象读。
可重复读(Repeatable Read
):像已提交读级别那样读数据,但会保持共享锁直到事务结束。会出现幻象读。
可序列化(Serializable
):工作方式类似于可重复读。但它不仅会锁定受影响的数据,还会锁定这个范围,这就阻止了新数据插入查询所涉及的范围
。
模型分类
-
强一致性模型
:系统中的某个数据被成功更新(事务成功返回)后,后续任何对该数据的读取操作都得到到
更新后的值。这是传统关系数据库提供的一致性模型,也是关系数据库
深受人们喜爱的原因之一。强一致性模型下的性能消耗
通常是最大
的 -
弱一致性模型
:系统中的某个数据被更新后,后续对该数据的读取操作得到的不一定
是更新后的值,这种情况下通常有个“不一致性时间窗口
”存在:即数据更新完成后在经过
这个时间窗口
,后续读取操作就能够得到更新后的值。 -
最终一致性模型
:属于弱一致性的一种,即某个数据被更新后,如果该数据后续没有被再次更新,那么最终
(没有时间窗口)所有的读取操作都会返回更新后的值 -
Bulk Load
:这种模型是基于批量加载数据到缓存
里面的场景而优化的,没有引入锁和常规的淘汰算法
这些降低性能的东西
,它和最终一致性模型很像,但是有批量、高速写和弱一致性保证
的机制。
最终一致性模型包含如下几个必要属性
-
读写一致
:某线程A,更新某条数据以后,后续的访问全部都能取得更新后的数据。 -
会话内一致
:它本质上和上面那一条是一致的,某用户更改了数据,只要会话还存在,后续他取得的所有数据都必须是更改后的数据。 -
单调读一致
:如果一个进程可以看到当前的值,那么后续的访问不能返回之前的值。 -
单调写一致
:对同一进程内的写行为必须是保序的,否则,写完毕的结果就是不可预期的了。·
API
1、显式锁
(Explicit Locking ):如果我们本身就配置为强一致性
,那么自然所有的缓存操作都具备事务性质。而如果我们配置成最终一致性时,再在外部使用显式锁API,也可以达到事务
的效果。当然这样的锁可以控制得更细粒度,但是依然可能存在竞争和线程阻塞。
2、无锁可读取视图(UnlockedReadsView)
:一个允许脏读
的decorator,它只能用在强一致性的配置下
,它通过申请一个特殊的写锁来比完全的强一致性配置提升性能
。
举例如下,xml配置为强一致性
模型:
但是使用UnlockedReadsView:
Cache cache = cacheManager.getEhcache("myCache");
UnlockedReadsView unlockedReadsView = new UnlockedReadsView(cache, "myUnlockedCache"); //代码上设置
3、原子方法(Atomic methods
):方法执行是原子化
的,即CAS
操作(Compare and Swap)。CAS最终
也实现了强一致性的效果,但不同的是,它是采用乐观锁而不是悲观锁来实现的。在乐观锁机制下,更新的操作可能不成功,因为在这过程中可能会有其他线程对同一条数据进行变更,那么在失败后需要重新执行更新操作。现代的CPU都支持CAS原语了
。
cache.putIfAbsent(Element element);
cache.replace(Element oldOne, Element newOne);
cache.remove(Element);
五 Spring整合
spring注解
Spring对缓存的支持类似于对事务的支持。
首先使用注解标记方法
,相当于定义了切点
,然后使用Aop
技术在这个方法的调用前
、调用后
获取方法的入参和返回值,进而实现了缓存的逻辑
。
@Cacheable
表明所修饰的方法是可以缓存的:当第一次
调用这个方法时,它的结果会被缓存下来,在缓存的有效时间内,以后访问这个方法都直接返回缓存结果
,不再执行
方法中的代码段
。
- 这个注解可以用
condition
属性来设置条件,如果不满足条件,就不使用缓存能力,直接执行方法。 - 可以使用
key
属性来指定key的生成规则
。
参数
-
value
:缓存位置名称,不能为空,如果使用EHCache,就是ehcache.xml中声明的cache的name
, 指明将值缓存到哪个Cache中 -
key
:缓存的key,默认为空
,既表示使用方法的参数类型
及参数值
作为key,支持SpEL
,如果要引用参数值
使用井号加参数名,如:#userId
,
一般来说,我们的更新操作只需要刷新缓存中某一个值,所以定义缓存的key值的方式就很重要,最好是能够唯一,因为这样可以准确的清除掉特定的缓存,而不会影响到其它缓存值 ,
本例子中使用实体加冒号再加ID组合成键的名称,如"user:1"、"order:223123"等 -
condition
:触发条件,只有满足条件的情况才会加入缓存,默认为空
,既表示全部都加入缓存
,支持SpEL
// 将缓存保存到名称为UserCache中,键为"user:"字符串加上userId值,如 'user:1'
@Cacheable(value="UserCache", key="'user:' + #userId")
public User findById(String userId) {
return (User) new User("1", "mengdee");
}
// 将缓存保存进UserCache中,并当参数userId的长度小于12时才保存进缓存,默认使用参数值及类型作为缓存的key
// 保存缓存需要指定key,value, value的数据类型,不指定key默认和参数名一样如:"1"
@Cacheable(value="UserCache", condition="#userId.length() < 12")
public boolean isReserved(String userId) {
System.out.println("UserCache:"+userId);
return false;
}
@CachePut
与@Cacheable不同,@CachePut不仅会缓存方法的结果
,还会执行
方法的代码段。它支持的属性和用法都与@Cacheable
一致。一个缓存后就不执行代码了,一个还要执行)
@CacheEvict
与@Cacheable功能相反,@CacheEvict表明所修饰的方法是用来删除失效
或无用
的缓存数据。
参数
- value:缓存位置名称,不能为空,同上
- key:缓存的key,默认为空,同上
- condition:触发条件,只有满足条件的情况才会清除缓存,默认为空,支持SpEL
-
allEntries
:true表示清除value中的全部缓存
,默认为false
//清除掉UserCache中某个指定key的缓存
@CacheEvict(value="UserCache",key="'user:' + #userId")
public void removeUser(User user) {
System.out.println("UserCache"+user.getUserId());
}
//清除掉UserCache中全部的缓存
@CacheEvict(value="UserCache", allEntries=true)
public final void setReservedUsers(String[] reservedUsers) {
System.out.println("UserCache deleteall");
}
代码测试
目录结构
pom
4.0.0
com.zyc
ehcache1
0.0.1-SNAPSHOT
UTF-8
4.10
4.2.3.RELEASE
junit
junit
${junit.version}
test
org.springframework
spring-test
${spring.version}
test
org.springframework
spring-webmvc
${spring.version}
org.springframework
spring-core
${spring.version}
org.springframework
spring-context
${spring.version}
org.springframework
spring-context-support
${spring.version}
net.sf.ehcache
ehcache
2.10.3
接口实现
package com.zyc;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import com.zyc.service.EhcacheService;
public class EhcacheServiceTest extends Test2 {
@Autowired
private EhcacheService ehcacheService;
/*
* 有效时间是5秒,第一次和第二次获取的值是一样的,因第三次是5秒之后所以会获取新的值
*/
@Test
public void testTimestamp() throws InterruptedException{
System.out.println("第一次调用:" + ehcacheService.getTimestamp("param"));
Thread.sleep(2000);
System.out.println("2秒之后调用:" + ehcacheService.getTimestamp("param"));
Thread.sleep(4000);
System.out.println("再过4秒之后调用:" + ehcacheService.getTimestamp("param"));
}
// 执行结果
// 第一次调用:1562460396352
// 2秒之后调用:1562460396352
// 再过4秒之后调用:1562460402359
@Test
public void testCache(){
String key = "zhangsan";
String value = ehcacheService.getDataFromDB(key); // 从数据库中获取数据...
ehcacheService.getDataFromDB(key); // 从缓存中获取数据,所以不执行该方法体
ehcacheService.removeDataAtDB(key); // 从数据库中删除数据
ehcacheService.getDataFromDB(key); // 从数据库中获取数据...(缓存数据删除了,所以要重新获取,执行方法体)
}
// 第二次调用已经用到了缓存
// 从数据库中获取数据...
// 从数据库中删除数据
// 从数据库中获取数据...
@Test
public void testPut(){
String key = "mengdee";
ehcacheService.refreshData(key); // 模拟从数据库中加载数据
String data = ehcacheService.getDataFromDB(key);//这个调用不会执行
System.out.println("data:" + data); // data:mengdee::103385
ehcacheService.refreshData(key); // 模拟从数据库中加载数据
String data2 = ehcacheService.getDataFromDB(key);
System.out.println("data2:" + data2); // data2:mengdee::180538
}
@Test
public void testFindById(){
ehcacheService.findById("1"); // 模拟从数据库中查询数据
ehcacheService.findById("1");
}
@Test
public void testIsReserved(){
ehcacheService.isReserved("123");
ehcacheService.isReserved("123");//会缓存
ehcacheService.isReserved("1234567890123");
ehcacheService.isReserved("1234567890123");//不会用到缓存
}
@Test
public void testRemoveUser(){
// 线添加到缓存
ehcacheService.findById("1");
// 再删除
ehcacheService.removeUser("1");
// 如果不存在会执行方法体
ehcacheService.findById("1");
}
@Test
public void testRemoveAllUser(){
ehcacheService.findById("1");
ehcacheService.findById("2");
ehcacheService.removeAllUser();
ehcacheService.findById("1");
ehcacheService.findById("2");
// 模拟从数据库中查询数据
// 模拟从数据库中查询数据
// UserCache delete all
// 模拟从数据库中查询数据
// 模拟从数据库中查询数据
}
}
参考
- ehcache入门基础示例
- ehcache详细解读
- Ehcache配置持久化到硬盘
- spring +ehcache 持久化数据,重启恢复