1. 简介
Ehcache是一种广泛使用的开源Java分布式缓存。主要面向通用缓存、Java EE和轻量级容器。它具有堆内内存、堆外内存、磁盘存储,缓存加载器,缓存扩展,缓存异常处理程序等特点。
本文将介绍堆内内存、堆外内存、磁盘存储多种存储介质组合的缓存模式及原理分析。
2. 分级缓存设计
作为本地缓存框架, Ehcache支持多层缓存模式,常用的有三种数据存储介质:
- 堆内
直接在JVM堆中存储JAVA对象,优点是速度快;缺点是会增加GC的频次和GC时间。 - 堆外
在堆外内存中存储序列化的JAVA对象,优点是不会增加GC;缺点是存取速度较慢,需要额外的时间处理序列化和反序列化。 - 磁盘
缓存数据到磁盘,优点是掉电不会丢失数据,可用空间更大;缺点是存取速度比堆外慢很多。
对于这三种缓存存储介质,Ehcache支持三种组合模式:
- 堆内+堆外
- 堆内+磁盘
- 堆内+堆外+磁盘
典型的堆内+堆外+磁盘的结构图如下:
2.1 时序图
在Ehcache的多层缓存结构中,最底层被称为Authoritative Tier,其余的缓存层被称为Caching Tier。Authoritative Tier层数据是最全的,其余层的数据都是该层的数据子集,只是临时存储数据。
比如,堆内+堆外模式中,堆外为Authoritative Tier。堆内+堆外+磁盘模式中,磁盘为Authoritative Tier。
2.1.1 put
2.1.2 get
3. 代码分析
3.1 Cache的使用
CacheManagerConfiguration<PersistentCacheManager> persistentManagerConfig = CacheManagerBuilder
.persistence(new File("/tmp", "ehcache-junit"));
PersistentCacheManager persistentCacheManager = CacheManagerBuilder.newCacheManagerBuilder()
.with(persistentManagerConfig).build();
persistentCacheManager.init();
ResourcePoolsBuilder resource = ResourcePoolsBuilder.newResourcePoolsBuilder()
.heap(4, MemoryUnit.MB)
.offheap(16, MemoryUnit.MB)
.disk(256, MemoryUnit.MB, true);
CacheConfiguration<Long, String> config = CacheConfigurationBuilder
.newCacheConfigurationBuilder(Long.class, String.class, resource).build();
Cache<Long, String> cache = persistentCacheManager.createCache("test",
CacheConfigurationBuilder.newCacheConfigurationBuilder(config));
cache.put(100L, "abc");
System.out.println(cache.get(100L));
System.out.println(cache.get(101L));
空间大小必须heap小于offhead小于disk,否则抛出IllegalArgumentException
java.lang.IllegalArgumentException: Tiering Inversion: ‘Pool {4 MB heap}’ is not smaller than ‘Pool {4 MB disk(persistent)}’
程序运行后在/tmp/ehcache-junit/file目录下创建了test_a94a8fe5ccb19ba61c4c0873d391e987982fbbd3/offheap-disk-store目录,该目录下存在两个文件ehcache-disk-store.meta和ehcache-disk-store.data,记录缓存的元数据和缓存数据。
ehcache-disk-store.meta内容如下,明文记录了key和value的类型信息
#Key and value types
#Wed Jul 10 12:52:06 CST 2019
keyType=java.lang.Long
valueType=java.lang.String
ehcache-disk-store.data则记录了缓存的序列化数据。
3.2 序列化
堆外和磁盘存储时,必须先将对象序列化为java.nio.ByteBuffer,Ehcache允许用户按下面的代码传入自定义的序列化类。
CacheManagerBuilder.newCacheManagerBuilder().withSerializer(Employee.class,
EmployeeSerializer.class).withSerializer(Person.class, PersonSerializer.class)
Ehcache自带的序列化器支持如下的类型:
- java.io.Serializable
- java.lang.Long
- java.lang.Integer
- java.lang.Float
- java.lang.Double
- java.lang.Character
- java.lang.String
- byte[]
LongSerializer逻辑如下:
@Override
public ByteBuffer serialize(Long object) {
ByteBuffer byteBuffer = ByteBuffer.allocate(8);
byteBuffer.putLong(object).flip();
return byteBuffer;
}
@Override
public Long read(ByteBuffer binary) throws ClassNotFoundException {
return binary.getLong();
}
StringSerializer逻辑如下:
public ByteBuffer serialize(String object) {
int length = object.length();
try(ByteArrayOutputStream bout = new ByteArrayOutputStream(length)) {
int i = 0;
for (; i < length; i++) {
char c = object.charAt(i);
if (c == 0x0000 || c > 0x007f) {
break;
}
bout.write(c);
}
for (; i < length; i++) {
char c = object.charAt(i);
if (c == 0x0000) {
bout.write(0xc0);
bout.write(0x80);
} else if (c < 0x0080) {
bout.write(c);
} else if (c < 0x800) {
bout.write(0xc0 | ((c >>> 6) & 0x1f));
bout.write(0x80 | (c & 0x3f));
} else {
bout.write(0xe0 | ((c >>> 12) & 0x1f));
bout.write(0x80 | ((c >>> 6) & 0x3f));
bout.write(0x80 | (c & 0x3f));
}
}
return ByteBuffer.wrap(bout.toByteArray());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public String read(ByteBuffer binary) throws ClassNotFoundException {
StringBuilder sb = new StringBuilder(binary.remaining());
int i = binary.position();
int end = binary.limit();
for (; i < end; i++) {
byte a = binary.get(i);
if (((a & 0x80) != 0)) break;
sb.append((char) a);
}
for (; i < end; i++) {
byte a = binary.get(i);
if ((a & 0x80) == 0) {
sb.append((char) a);
} else if ((a & 0xe0) == 0xc0) {
sb.append((char) (((a & 0x1f) << 6) | ((binary.get(++i) & 0x3f))));
} else if ((a & 0xf0) == 0xe0) {
sb.append((char) (((a & 0x0f) << 12) | ((binary.get(++i) & 0x3f) << 6) | (binary.get(++i) & 0x3f)));
} else {
//these remaining stanzas are for compatibility with the previous regular UTF-8 codec
int codepoint;
if ((a & 0xf8) == 0xf0) {
codepoint = ((a & 0x7) << 18) | ((binary.get(++i) & 0x3f) << 12) | ((binary.get(++i) & 0x3f) << 6) | ((binary.get(++i) & 0x3f));
} else if ((a & 0xfc) == 0xf8) {
codepoint = ((a & 0x3) << 24) | ((binary.get(++i) & 0x3f) << 18) | ((binary.get(++i) & 0x3f) << 12) | ((binary.get(++i) & 0x3f) << 6) | ((binary.get(++i) & 0x3f));
} else if ((a & 0xfe) == 0xfc) {
codepoint = ((a & 0x1) << 30) | ((binary.get(++i) & 0x3f) << 24) | ((binary.get(++i) & 0x3f) << 18) | ((binary.get(++i) & 0x3f) << 12) | ((binary.get(++i) & 0x3f) << 6) | ((binary.get(++i) & 0x3f));
} else {
throw new SerializerException("Unexpected encoding");
}
sb.appendCodePoint(codepoint);
}
}
return sb.toString();
}
3.3 offheap的get和put
3.3.1 get
堆外缓存的读写逻辑主要在org.ehcache.impl.internal.store.offheap.AbstractOffHeapStore中
private Store.ValueHolder<V> internalGet(K key, final boolean updateAccess, final boolean touchValue) throws StoreAccessException {
final StoreEventSink<K, V> eventSink = eventDispatcher.eventSink();
final AtomicReference<OffHeapValueHolder<V>> heldValue = new AtomicReference<>();
try {
OffHeapValueHolder<V> result = backingMap().computeIfPresent(key, (mappedKey, mappedValue) -> {
long now = timeSource.getTimeMillis();
if (mappedValue.isExpired(now)) {
onExpiration(mappedKey, mappedValue, eventSink);
return null;
}
// 更新访问时间
if (updateAccess) {
mappedValue.forceDeserialization();
OffHeapValueHolder<V> valueHolder = setAccessTimeAndExpiryThenReturnMapping(mappedKey, mappedValue, now, eventSink);
if (valueHolder == null) {
heldValue.set(mappedValue);
}
return valueHolder;
} else if (touchValue) {
mappedValue.forceDeserialization();
}
return mappedValue;
});
if (result == null && heldValue.get() != null) {
result = heldValue.get();
}
eventDispatcher.releaseEventSink(eventSink);
return result;
} catch (RuntimeException re) {
eventDispatcher.releaseEventSinkAfterFailure(eventSink, re);
throw handleException(re);
}
}
4. 总结
本地缓存是微服务开发中经常使用的功能,开发者通常使用原生的ConcurrentHashmap、Guava、Ehcache等,经常上面的源码分析,我们可以理解Ehcache的分级缓存机制和使用场景。
5. 引用
[https://www.ehcache.org/documentation/2.8/get-started/storage-options.html]
[https://www.ehcache.org/documentation/3.4/tiering.html]