本文是基于Ehcache 3.7官方文档,总结的自己比较在意的内容。详细内容可以参看官方文档:Ehcache 3.7 Documentation Overview.
一、介绍
1、什么是Ehcache
Ehcache 是一个开源的、基于标准的,健壮、可靠、快速、简单、轻量级的java分布式缓存,支持与其他框架的集成,是Hibernate默认的CacheProvider。同时也实现了JSR107的规范,是Jcache的一种实现。
Ehcache 目前提供四层模型,支持四种级别:
- heap
- off-heap
- disk
- clustered
同时提供了java内配置和xml配置,提供线程池、事务、管理器等,并支持扩展和监听事件,提供了disk持久化以及clustered持久化。
2、依赖引入
Ehcache非常轻量级,唯一需要的依赖是slf4j。
maven使用:
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.7.1</version>
</dependency>
如果与其他框架集成,请相应的引入对应的依赖。
如果需要使用事务,从3.1版本,应该就单独剥离出去了,所以需要引入:
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache-transactions</artifactId>
<version>3.7.1</version>
</dependency>
3、分层架构
Ehcache3.7 支持堆、堆外、磁盘以及集群缓存;但是除了堆之外外的三种缓存,缓存的键值对必须支持序列化和反序列化。
我们在使用的时候,可以单独使用任意一个,比如:
CacheConfigurationBuilder.newCacheConfigurationBuilder(String.class, Integer.class,
ResourcePoolsBuilder.newResourcePoolsBuilder().offheap(1, MemoryUnit.GB));
- heap:堆缓存,受到jvm的管控,可以不序列化,速度最快;默认使用的是引用传递,也可以使用复制器来进行值传递。可以设置缓存条数或者缓存的大小来进行堆缓存大小的设置。尽量不要设置的过大,否则容易引起GC.(如果设置缓存的大小,则计算比较麻烦,同时又多了一个计算缓存大小的过程)。
- off-heap:堆外缓存,不受jvm的管控,受到RAM的限制,在ehcache中配置,至少1M。通过-XX:MaxDirectMemorySize限制ehcache可分配的最大堆外内存,但是实际使用发现,不配置也能使用。如果不配置,使用堆外缓存时,ehcache将会使用jvm的内存,最大值为堆内存,但实际比-Xmx要小,可以通过Runtime,getRuntime().maxMemory()获取。因此,对于堆内对垃圾收集的影响过于严重的大量数据,应该选择堆外。
- disk:磁盘存储,尽量使用高性能的SSD。这一层的存储,不能在不同的CacheManager之间共享!
- clustered:集群
以上只有 heap 支持 运行时改变大小!
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(true);
Cache<Long, String> cache = cacheManager.createCache("runforupdate", CacheConfigurationBuilder
.newCacheConfigurationBuilder(Long.class, String.class, ResourcePoolsBuilder.heap(2)).build());
//do something
//change the heap
cache.getRuntimeConfiguration()
.updateResourcePools(ResourcePoolsBuilder.heap(10).build());
4、层组合
如果需要使用多个层,则需要遵循一定的规则。
规则:
- 必须有heap层
- disk和clusterd不能同时存在
- 层的大小应该采用金字塔的方式,即金字塔上的层比下的层使用更少的内存
根据规则进行以下有效配置:
- heap + offheap
- heap + offheap + disk
- heap + offheap + clustered
- heap + disk
- heap + clustered
对于多层,put、get的顺序:
- 当将一个值放入缓存时,它直接最低层。比如heap + offheap + disk,直接会存储在disk层。
- 当获取一个值,从最高层获取,如果没有继续向下一层获取,一旦获取到,会向上层推送,同时上层存储该值。
5、访问模式
Ehcache支持以下几种模式:
- Cache-aside
- Cache-as-SoR
- Read-through
- Write-through
- Write-behind
六、缓存配置
配置方式有两种:
- java配置
- XML配置
二、使用
1、缓存过期和淘汰策略
(1) 过期策略
在缓存级别配置,有三种策略:
- no expiry : 永不过期
- time-to-live :创建后一段时间过期
- time-to-idle : 访问后一段时间过期
示例:
- java
CacheConfiguration<String, String> configuration = CacheConfigurationBuilder
.newCacheConfigurationBuilder(String.class, String.class, ResourcePoolsBuilder.heap(10))
.withExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(Duration.ofSeconds(300))).build();
- xml
<cache alias="test">
<expiry>
<ttl unit="seconds">300</ttl>
</expiry>
<heap>10</heap>
</cache>
支持实现ExpiryPolicy接口来自定义过期策略。
(2) 淘汰策略
- FIFO
- LRU 默认策略
- LFU
2、自定义Serializer
除了heap层,可以存储对象;其他所有层都只能通过二进制形式表示,所以需要对对象进行序列化和反序列化。Ehcache提供了Serializer来,所以我们除了可以直接实现Serializable,也可以通过Ehcache的Serializer来自定义。这里使用Kryo(比java的Serializable更快更小)结合Ehcache提供的Serizlizer来进行序列化和反序列化。
Ehcache默认给基本的数据类型都提供了优化的序列化器,如下:
- java.io.Serializable
- java.lang.Long
- java.lang.Integer
- java.lang.Float
- java.lang.Double
- java.lang.Character
- java.lang.String
- byte[]
(1) Serializer接口
package org.ehcache.spi.serialization;
import java.nio.ByteBuffer;
/**
* Defines the contract used to transform type instances to and from a serial form.
* <p>
* Implementations must be thread-safe.
* <p>
* When used within the default serialization provider, there is an additional requirement.
* The implementations must define a constructor that takes in a {@code ClassLoader}.
* The {@code ClassLoader} value may be {@code null}. If not {@code null}, the class loader
* instance provided should be used during deserialization to load classes needed by the deserialized objects.
* <p>
* The serialized object's class must be preserved; deserialization of the serial form of an object must
* return an object of the same class. The following contract must always be true:
* <p>
* {@code object.getClass().equals( mySerializer.read(mySerializer.serialize(object)).getClass())}
* @param <T> the type of the instances to serialize
* @see SerializationProvider
*/
public interface Serializer<T> {
/**
* Transforms the given instance into its serial form.
* @param object the instance to serialize
* @return the binary representation of the serial form
* @throws SerializerException if serialization fails
*/
ByteBuffer serialize(T object) throws SerializerException;
/**
* Reconstructs an instance from the given serial form.
* @param binary the binary representation of the serial form
* @return the de-serialized instance
* @throws SerializerException if reading the byte buffer fails
* @throws ClassNotFoundException if the type to de-serialize to cannot be found
*/
T read(ByteBuffer binary) throws ClassNotFoundException, SerializerException;
/**
* Checks if the given instance and serial form {@link Object#equals(Object) represent} the same instance.
* @param object the instance to check
* @param binary the serial form to check
* @return {@code true} if both parameters represent equal instances, {@code false} otherwise
* @throws SerializerException if reading the byte buffer fails
* @throws ClassNotFoundException if the type to de-serialize to cannot be found
*/
boolean equals(T object, ByteBuffer binary) throws ClassNotFoundException, SerializerException;
}
该接口只有三个方法,但是需要注意以下几点:
- A. 实现类必须提供一个带有ClassLoader参数的构造器,比如:
public MySerializer(ClassLoader classLoader) {
}
- B. 实现类必须线程安全
- C. 被序列化对象的class必须被保存;被序列化对象的class与反序列化后的对象的class必须相等。(如果在构造CacheManager时没有指定classLoader,则使用ehcache的默认classLoader)
同时如果实现 java.io.Closeable 接口,当关闭缓存管理器时,将调用该序列化器的close方法。
(2) 通用序列化器
Ehcache不建议使用通用序列化器,建议将序列化器注册限制在具体的类中,可以根据实际来进行改变。
使用Kryo作为序列化/反序列化工具,
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import org.objenesis.strategy.StdInstantiatorStrategy;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import de.javakaffee.kryoserializers.ArraysAsListSerializer;
public class KryoUtil {
private static ThreadLocal<Kryo> kryos = new ThreadLocal<Kryo>() {
@Override
protected Kryo initialValue() {
Kryo kryo = new Kryo();
// 支持对象循环引用,默认 true
kryo.setReferences(true);
// 是否需要强制注册,默认 false
kryo.setRegistrationRequired(false);
// 支持无参构造器等(推荐每个类都有自己的无参构造器,可以为private)
((Kryo.DefaultInstantiatorStrategy) kryo.getInstantiatorStrategy())
.setFallbackInstantiatorStrategy(new StdInstantiatorStrategy());
// 支持Arrays.asList()
kryo.register(Arrays.asList("").getClass(), new ArraysAsListSerializer());
return kryo;
}
};
private static ThreadLocal<Output> outs = ThreadLocal.withInitial(() -> new Output(1024, 1024 * 1024));
private static ThreadLocal<Input> ins = ThreadLocal.withInitial(() -> {
return new Input();
});
/**
*
* @return
*/
public static Kryo get() {
return kryos.get();
}
/**
* Serializer
*
* @return
*/
public static byte[] serializer(Object object) {
try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); Output out = outs.get();) {
out.setOutputStream(bos);
kryos.get().writeClassAndObject(out, object);
out.flush();
return bos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 序列化
*
* @return
*/
public static ByteBuffer serializerByteBuffer(Object object) {
return ByteBuffer.wrap(serializer(object));
}
/**
* 反序列化
*
* @return
*/
@SuppressWarnings("unchecked")
public static <T> T deserializer(byte[] b) {
try (Input in = ins.get();) {
in.setBuffer(b, 0, b.length);
return (T) KryoUtil.get().readClassAndObject(in);
}
}
/**
* 反序列化
*
* @return
*/
public static <T> T deserializerByteBuffer(ByteBuffer binary) {
byte[] bytes = new byte[binary.capacity()];
binary.get(bytes);
return deserializer(bytes);
}
}
序列化器:
import java.io.Closeable;
import java.io.IOException;
import java.nio.ByteBuffer;
import org.ehcache.spi.serialization.Serializer;
import org.ehcache.spi.serialization.SerializerException;
import com.wzy.map.util.serializer.KryoUtil;
public class EhcacheSerializer<T> implements Serializer<T>, Closeable {
public EhcacheSerializer() {
}
public EhcacheSerializer(ClassLoader loader) {
}
@Override
public ByteBuffer serialize(T object) throws SerializerException {
return KryoUtil.serializerByteBuffer(object);
}
@Override
public T read(ByteBuffer binary) throws ClassNotFoundException, SerializerException {
return KryoUtil.deserializerByteBuffer(binary);
}
@Override
public boolean equals(T object, ByteBuffer binary) throws ClassNotFoundException, SerializerException {
return object.equals(read(binary));
}
@Override
public void close() throws IOException {
System.out.println("close...");
}
}
3、自定义Copier
官网定义,Copier is the Ehcache abstraction solving this: it is specific to the on-heap store.
默认情况下,堆上存储,保存的是对象的引用,可以通过key和value定义对应的Copier来自定义是存储引用还是重新创建一个对象。
public interface Copier<T> {
/**
* Creates a copy of the instance passed in.
* <p>
* This method is invoked as a value is read from the cache.
*
* @param obj the instance to copy
* @return the copy of the {@code obj} instance
*/
T copyForRead(T obj);
/**
* Creates a copy of the instance passed in.
* <p>
* This method is invoked as a value is written to the cache.
*
* @param obj the instance to copy
* @return the copy of the {@code obj} instance
*/
T copyForWrite(T obj);
}
可以使用Kryo来进行复制。
4、Cache as SOR
Ehcache 支持Read Through、Write Through、Write Behind;在3.7中,提供了CacheLoaderWriter接口。
需要注意的是,实现类必须是线程安全的,同时write behind级别不支持重试失败的写操作,需要我们自己实现。
这里业务代码不再同时维护cache和sor,业务代码只操作cache,把cache当作sor;而cache再对真实的数据库等进行读写操作!
这里以Write Behind为例子来简单模拟一下场景:
import java.util.concurrent.ConcurrentMap;
import org.ehcache.impl.internal.concurrent.ConcurrentHashMap;
import org.ehcache.spi.loaderwriter.CacheLoaderWriter;
public class SampleLoaderWriter<K, V> implements CacheLoaderWriter<K, V> {
// sor
private ConcurrentMap<K, V> map;
public SampleLoaderWriter() {
map = new ConcurrentHashMap<K, V>();
// do something
}
@Override
public V load(K key) throws Exception {
return (V) map.get(key);
}
@Override
public void write(K key, V value) throws Exception {
map.putIfAbsent(key, value);
}
@Override
public void delete(K key) throws Exception {
map.remove(key);
}
}
配置CacheLoaderWriter:
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(true);
Cache<Long, String> cache = cacheManager.createCache("cache",
CacheConfigurationBuilder
.newCacheConfigurationBuilder(Long.class, String.class, ResourcePoolsBuilder.heap(10).offhea)
.withLoaderWriter(new SampleLoaderWriter<Long, String>())
.add(WriteBehindConfigurationBuilder
.newBatchedWriteBehindConfiguration(10, TimeUnit.SECONDS, 3).queueSize(10)
.concurrencyLevel(1).enableCoalescing())
.build());
业务代码只需要处理cache就可以!
5、监听器
Ehcache 提供 CacheEventListerner 来监听缓存事件。
/**
* Definition of the contract for implementing listeners to receive {@link CacheEvent}s from a
* {@link org.ehcache.Cache Cache}.
*
* @param <K> the key type for the observed cache
* @param <V> the value type for the observed cache
*/
public interface CacheEventListener<K, V> {
/**
* Invoked on {@link org.ehcache.event.CacheEvent CacheEvent} firing.
* <p>
* This method is invoked according to the {@link EventOrdering}, {@link EventFiring} and
* {@link EventType} requirements provided at listener registration time.
* <p>
* Any exception thrown from this listener will be swallowed and logged but will not prevent other listeners to run.
*
* @param event the actual {@code CacheEvent}
*/
void onEvent(CacheEvent<? extends K, ? extends V> event);
}
实现 onEvent() 方法即可。因为监听器是在缓存级别注册的,因此只接收已注册的缓存的事件。可以在运行时注册监听器。
注册示例:
CacheEventListenerConfigurationBuilder cacheEventListenerConfiguration = CacheEventListenerConfigurationBuilder
.newEventListenerConfiguration(new ListenerObject(), EventType.CREATED, EventType.UPDATED,
EventType.REMOVED)
.unordered().asynchronous();
final CacheManager manager = CacheManagerBuilder.newCacheManagerBuilder()
.withCache("foo",
CacheConfigurationBuilder
.newCacheConfigurationBuilder(String.class, String.class, ResourcePoolsBuilder.heap(10))
.add(cacheEventListenerConfiguration))
.build(true);
6、Eviction Advisor
Ehcache 提供EvictionAdvisor 接口,当ehcache试图从缓存中删除数据时,将调用该接口以确定给定的数据是否该清除。