缓存技术内部交流_01_Ehcache3简介
参考资料:
http://www.ehcache.org/documentation/3.2/getting-started.html
http://www.ehcache.org/documentation/3.2/eviction-advisor.html
示例代码:
https://github.com/gordonklg/study,cache module
A. HelloWorld
gordon.study.cache.ehcache3.basic.HelloWorld.java
public class HelloWorld {
public static void main(String[] args) {
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build();
cacheManager.init();
Cache<Long, String> myCache = cacheManager.createCache("myCache",
CacheConfigurationBuilder.newCacheConfigurationBuilder(Long.class, String.class, ResourcePoolsBuilder.heap(10)));
myCache.put(1L, "Hello World!");
String value = myCache.get(1L);
System.out.println(value);
cacheManager.close();
}
}
代码第4行通过 Builder 模式创建了一个不可变的 CacheManager,用于管理所有的 Cache 及相关 Service。接着调用 init() 方法初始化 CacheManager。
代码第6行创建了一个 Cache,名字为 myCache。通过 CacheConfigurationBuilder 构建出的 CacheConfiguration 限制 myCache 只能接受 key 类型为 Long,value 类型为 String 的数据,同时 ResourcePoolsBuilder 构建出的 ResourcePools 决定了 myCache 可以使用 JVM 堆内存缓存最多10条数据。
代码第8行使用 myCache 缓存了一条数据,第9行从 myCache 中读取缓存数据。
代码第10行关闭 CacheManager,这会顺带着关闭所有的 Cache。
B. Cache 配置项
Cache 的特性通过 CacheConfigurationBuilder 创建出的 CacheConfiguration 决定。通过 CacheConfiguration 接口定义,可以看出 Cache 支持以下特性:
- key type,设定允许的 key 类型,默认为 Object, 类型不匹配会抛出 ClassCastException
- value type,设定允许的 value 类型,默认为 Object, 类型不匹配会抛出 ClassCastException
- resource pools,设定缓存区的空间大小
- expiry,设定过期策略
- eviction advisor,调整回收策略
resource pools 设定缓存区的空间大小,Ehcache 总共有4种缓存区,分别为:
- heap,堆内存,最常用,目前团队只要掌握这种就可以了
- offheap,堆外内存,优势是不用GC
- disk,磁盘空间,可以持久化
- cluster,Ehcahce 已经支持集群了,具体内容不详
对于堆内存,resource pools 指定了 Cache 实例可以存多少条目以及最大占用多大内存空间。
过期策略有三种:
- 不过期,条目一直存在于缓存中,除非显式从缓存中 remove,或者因回收策略被移除
- TTL,time to live,指定条目的存活时间
- TTI,time to idle,条目在指定时间段内未被使用,则过期
默认回收策略比较复杂,大致可视为 LRU 算法(最长时间未被访问的条目优先被回收)。可以通过 EvictionAdvisor 接口调整回收策略
示例代码如下:
gordon.study.cache.ehcache3.basic.CacheFeatures.java
public class CacheFeatures {
@SuppressWarnings({ "rawtypes", "unchecked" })
public static void main(String[] args) throws Exception {
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(true);
CacheConfiguration config = CacheConfigurationBuilder
.newCacheConfigurationBuilder(Integer.class, String.class, ResourcePoolsBuilder.heap(5))
.withEvictionAdvisor(new TopThreeAreImportantEvictionAdvisor())
.withExpiry(Expirations.timeToIdleExpiration(Duration.of(3, TimeUnit.SECONDS))).build();
Cache myCache = cacheManager.createCache("myCache", config);
try { // key type 错误
myCache.put("Invalid key type", "sad");
} catch (Exception e) {
System.out.println("Invalid key type");
}
try { // value type 错误
myCache.put(1L, 9527L);
} catch (Exception e) {
System.out.println("Invalid value type");
}
for (int i = 1; i <= 10; i++) { // 超出数量上限,回收策略开始生效:key为1、2、3的条目不被回收
myCache.put(i, "No. " + i);
}
printAllCacheEntries(myCache);
System.out.println("-------------------------------");
for (int i = 0; i < 3; i++) { // 等待3秒,3秒内一直没被使用的条目全部过期移除。只有key为1的条目还在。
myCache.get(1);
Thread.sleep(1050);
}
printAllCacheEntries(myCache);
}
private static void printAllCacheEntries(Cache<Integer, String> cache) {
cache.forEach(new Consumer<Cache.Entry<Integer, String>>() {
@Override
public void accept(Cache.Entry<Integer, String> entry) {
System.out.println(entry.getKey());
}
});
}
private static class TopThreeAreImportantEvictionAdvisor implements EvictionAdvisor<Integer, String> {
@Override
public boolean adviseAgainstEviction(Integer key, String value) {
return (key.intValue() < 4);
}
}
}
代码第8行将回收策略设定为自定义的 TopThreeAreImportantEvictionAdvisor 回收策略,即 key 值小于4的条目尽量不回收。第24行打印的所有 Cache 条目证明了key为1、2、3的条目没有被回收。EvictionAdvisor 实现类的 adviseAgainstEviction 回调方法返回 true 表示不建议回收该条目,返回 false 表示可以回收。
代码第9行将过期策略设定为 TTI,3秒未被访问的条目会过期。第31行的打印结果证明了只有key为1的条目由于一直被访问还存在于 Cache 中。
C. 回收算法略读
Ehcache 默认回收算法是一种近似 LRU 算法,Ehcache 并没有将所有的条目按照最后访问时间放在一个有序的Map中,或者利用额外的数据结构来完成排序,而是通过遍历的方式在一个小范围内寻找 LRU 条目。回收的触发时机是在新的条目需要放置到 Cache 中时。
Cache 类的 Store store 属性用于 Cache 的后端存储实现。在本例中,store 类型为 OnHeapStore。对 Cache 类的 put 操作实际上就是对 OnHeapStore 的 put 操作。
OnHeapStore 的 put 操作会先创建条目,放入内部的 Map 中(本质上是 ConcurrentHashMap),然后判断当前条目数量是否超过允许的条目上限。如果超过上限,则先按照指定的 EvictionAdvisor 寻找一条可回收条目,如果找不到,则无视 EvictionAdvisor 再次寻找一条可回收条目(因为可能 Cache 中所有条目都置为不建议回收)。这段逻辑由 OnHeapStore 的 evict 方法实现(Line 1605)
@SuppressWarnings("unchecked")
Map.Entry<K, OnHeapValueHolder<V>> candidate = map.getEvictionCandidate(random, SAMPLE_SIZE, EVICTION_PRIORITIZER, EVICTION_ADVISOR);
if (candidate == null) {
// 2nd attempt without any advisor
candidate = map.getEvictionCandidate(random, SAMPLE_SIZE, EVICTION_PRIORITIZER, noAdvice());
}
寻找可回收条目的逻辑略复杂。宏观上说,是遍历 Cache 中的条目,分析最多8条可以回收的条目,选中 lastAccessTime 值最小的那个条目。时间对比通过 OnHeapStore 类中预先定义好的 Comparator EVICTION_PRIORITIZER 完成。显然,这并不是 LRU,只是小范围中的 LRU。
private static final Comparator<ValueHolder<?>> EVICTION_PRIORITIZER = new Comparator<ValueHolder<?>>() {
@Override
public int compare(ValueHolder<?> t, ValueHolder<?> u) {
if (t instanceof Fault) {
return -1;
} else if (u instanceof Fault) {
return 1;
} else {
return Long.signum(u.lastAccessTime(TimeUnit.NANOSECONDS) - t.lastAccessTime(TimeUnit.NANOSECONDS));
}
}
};
由于算法的特性,所以遍历 Cache 条目需要从一个随机点开始以优化算法的整体准确性。getEvictionCandidate 方法从一个随机起始点开始遍历 ConcurrentHashMap,分析可回收条目,如果分析完8个条目,则返回最长时间未被访问的条目;如果从随机起始点到最后一个条目之间不满8个可回收条目,则调用 getEvictionCandidateWrap 方法从0开始遍历 ConcurrentHashMap,直到分析完8个条目(包含getEvictionCandidate 方法中分析的条目)或到达上面的随机开始点为止(相当于遍历了全部的 Cache 条目),最终返回最长时间未被访问的条目。
按照这个逻辑,如果 Cache 空间已满且全是不建议回收条目,这时如果插入可回收条目,该条目立即会被回收;如果插入不建议回收条目,则随机回收一条。见下例:
gordon.study.cache.ehcache3.basic.CacheFeatures_Eviction.java
public class CacheFeatures_Eviction {
private static final int CACHE_SIZE = 50;
public static void main(String[] args) throws Exception {
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(true);
CacheConfiguration<Integer, String> config = CacheConfigurationBuilder
.newCacheConfigurationBuilder(Integer.class, String.class, ResourcePoolsBuilder.heap(50))
.withEvictionAdvisor(new MyEvictionAdvisor()).build();
Cache<Integer, String> myCache = cacheManager.createCache("myCache", config);
for (int i = 1; i <= CACHE_SIZE; i++) {
myCache.put(i, "No. " + i);
Thread.sleep(10);
}
myCache.put(CACHE_SIZE + 1, "No. " + (CACHE_SIZE + 1));
BitSet bitSet = new BitSet(CACHE_SIZE + 1);
for (Cache.Entry<Integer, String> entry : myCache) {
bitSet.set(entry.getKey() - 1);
}
System.out.println("Eviction number: " + (bitSet.nextClearBit(0) + 1));
}
private static class MyEvictionAdvisor implements EvictionAdvisor<Integer, String> {
@Override
public boolean adviseAgainstEviction(Integer key, String value) {
return (key.intValue() <= CACHE_SIZE); // 最后一条是可回收条目,则必定回收这一条
// return (key.intValue() <= (CACHE_SIZE + 1)); // 最后一条是不建议回收条目,则随机回收一条
}
}
}