目前最常用的三种缓存方案:
- ehcache
- redis
- memcached
其中的redis和memcached都是以TCP/IP的形式实现分布式缓存,而ehcache需要集成到项目中,与应用共享JVM环境,属于嵌入式组件。因为不需要额外的网络开销,因此在这三者中的运行效率最高,常被用于实现一级缓存。
添加ehcache依赖
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>2.10.4</version>
</dependency>
这个版本是发布到 net.sf.ehcache 的最后一个版本,也是 2.x 的最后一个版本。
在 org.ehcache 上有更新的 3.x 版本,功能更强大,但写法差异也挺大。
由于 2.x 的核心功能已经非常稳定,已经完全满足系统需求,因此一般选择 2.10.4 这个版本就可以。
配置文件:ehcache.xml
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../config/ehcache.xsd"
updateCheck="false"
dynamicConfig="false">
<diskStore path="java.io.tmpdir/myApp"/>
<!--
默认缓存
属性说明:
maxElementsInMemory:内存中可保存的最大数量
eternal:缓存中对象是否为永久的。如果是,超时设置将被忽略
timeToIdleSeconds:对象最后一次访问之后的存活时间
timeToLiveSeconds:对象创建后的存活时间
memoryStoreEvictionPolicy:内存缓存的超期清理策略
maxElementsOnDisk:硬盘中可保存的最大数量
diskExpiryThreadIntervalSeconds:磁盘超期监控线程扫描时间间隔
overflowToDisk:内存不足时,是否启用磁盘缓存
-->
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="1200"
maxElementsOnDisk="10000000"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"
overflowToDisk="true">
</defaultCache>
</ehcache>
保存位置:src/main/resource
实现动态创建Cache
在ehcache中,有两个最基本的对象:
- Cache
- Element
其中,Cache 指 ehcache 的分区,可以看成 memcache 中的 Slab,上面配置文件中的 defaultCache 就是一个默认分区
每个 Cache(分区)都类似一个 Map<K, V>,可以通过 Key 来从 Cache 中返回缓存的 Value。
每个 KV 键值对,在 ehcache 中,被称为 Element(元素)。
要将任何一个对象添加到 ehcache 中,都需要事先指定分区
但在配置文件中创建分区很麻烦,通常只创建一个默认分区(必须存在)
可通过以下方法来动态创建分区:
/**
* 获取Cache,当Cache不存在时自动创建
*
* @param cacheName
* @return Cache
* @author netwild@qq.com
*/
public Cache getOrAddCache(String cacheName) {
Cache cache = cacheManager.getCache(cacheName);
if (cache == null) {
synchronized (locker) {
cache = cacheManager.getCache(cacheName);
if (cache == null) {
cacheManager.addCacheIfAbsent(cacheName);
cache = cacheManager.getCache(cacheName);
}
}
}
return cache;
}
这样的话,只需要像下面的用法,就可以很方便的把对象添加到缓存中:
String cacheName = "article";
String atricleId = "A00428";
Atricle article = AtricleService.findById(atricleId);
Element element = new Element(atricleId, article);
getOrAddCache(cacheName).put(element);
动态创建的 Cache 并不会出现在 ehcache.xml 配置文件中。
值得注意的是,上面动态创建 Cache 的方法中,并没有为新的 Cache 指定任何参数,那这些参数的默认值是多少呢?
其实,当创建 Cache 时,如果未传入参数默认值,将自动拷贝 defaultCache 的参数设置。
就是说,配置文件中 defaultCache 的超期时间等属性将直接被应用到所有动态创建的 Cache。
ehcache关于元素超期的判断逻辑
在ehcache.xml配置文件中,有两个关于元素超期的参数:
- timeToLiveSeconds:对象创建后的存活时间
- timeToIdleSeconds:对象最后一次访问之后的存活时间
这两个参数我忽略了将近一年的时间,一直在配置文件中对他们都设置了同样的参数值,比如:1200(20分钟)
刚才反复实验多次,终于将这两个参数搞清楚,才明白以前的做法是错误的。
首先,这两个参数都可以单独设置而省略另一个,也可以分别设置成不同的值,当然也可以设置成相同的值,就像我以前做的那样。
下面分别对这几种进行说明:
单独设置 timeToLiveSeconds:
该对象的超期时间 = 初始创建时间 + timeToLiveSeconds
因为初始创建时间是固定的,因此不管这个对象在有效期内被命中了多少次,一旦满足超期条件,该对象将被移除。单独设置 timeToIdleSeconds:
该对象的超时时间 = 最近访问时间 + timeToIdleSeconds
注意与上面的区别:不再根据创建时间,而是根据最近访问时间来确定超期时间。
所以这是一种动态的超期模式,即使这个参数设置为1(秒),只要保证每秒内都能get一次,那么对象也将永远不会超期。分别设置 timeToLiveSeconds 及 timeToIdleSeconds :
那么将分别计算以上两种模式的超期时间,会得出两个结果,再从两个结果里找到最小的一个做为超期时间,相当于“严苛模式”
但事实上,创建时间肯定会小于最近访问时间,那如果两者都设置同样的参数值,相当于 timeToIdleSeconds 永远也不会起到作用。
如果设置不同的参数值,根据具体的业务需求,可能会出现一些意料之中或者意料之外的情况。
综上所述,我的建议是,单独设置 timeToIdleSeconds 更恰当一些,对于在有效期内被频繁命中的缓存对象,可以自动“续期”。
最常用的操作之一:判断缓存中是否存在对象
在应用缓存的开发过程中,这是最常用的操作,目的是想要知道:目标对象是否已经被缓存过
通常下面的逻辑是:
如果已被缓存过,那么直接拿出来使用;
否则自力更生,完事之后再添加到缓存,下次就省事了
很多人是这样判断的:
return getOrAddCache(cacheName).get(key) != null;
但这种方式存在个问题:当缓存对象实际上存在,但值就是Null,这时就相当于忽略了缓存。
所以我开始时是这样判断的:
return getOrAddCache(cacheName).isKeyInCache(key);
后来发现这种方式不会进行超期验证,就是说即使对象已经超期,只要当初被创建过,也会返回 true
调整之后:
Cache cache = getOrAddCache(cacheName);
if(cache.isKeyInCache(key) && cache.getQuiet(key) != null){
return true;
}
return false;
这样就准确了!