EhCache简单入门
一 介绍
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
依赖
<?xml version="1.0" encoding="UTF-8"?>
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
path="java.io.tmpdir/ehcache"/>
maxEntriesLocalHeap="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
maxEntriesLocalDisk="10000000"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
strategy="localTempSwap"/>
>
name="HelloWorldCache"
maxElementsInMemory="1000"
eternal="false"
timeToIdleSeconds="5"
timeToLiveSeconds="5"
overflowToDisk="false"
memoryStoreEvictionPolicy="LRU"/>
>
测试类
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 的监听.
@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配置为强一致性
模型:
Cache cache = cacheManager.getEhcache("myCache");
UnlockedReadsView unlockedReadsView = new UnlockedReadsView(cache, "myUnlockedCache"); //代码上设置
3、原子方法(Atomic methods
):方法执行是原子化
的,即CAS
操作(Compare and Swap)。CAS最终
也实现了强一致性的效果,但不同的是,它是采用乐观锁而不是悲观锁来实现的。在乐观锁机制下,更新的操作可能不成功,因为在这过程中可能会有其他线程对同一条数据进行变更,那么在失败后需要重新执行更新操作。现代的CPU都支持CAS原语了
。
// 将缓存保存到名称为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
xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
>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")