即设置缓存的存储空间,如设置为10MB,当达到存储空间时,按照一定的策略移除数据。
基于容量指缓存设置了最大大小,当缓存的条目超过最大大小,则按照一定的策略将旧数据移除。
TTL(Time To Live):存活期,即缓存数据从缓存中创建时间开始直到它到期的一个时间段(不管在这个时间段内有没有访问都将过期)。
TTI(Time To Idle):空闲期,即缓存数据多久没被访问过将从缓存中移除的时间。
软引用:如果一个对象是软引用,那么当JVM堆内存不足时,垃圾回收器可以回收这些对象。软引用适合用来做缓存,从而当JVM堆内存不足时,可以回收这些对象腾出一些空间供强引用对象使用,从而避免OOM。
弱引用:当垃圾回收器回收内存时,如果发现弱引用,则将立即回收它。相对于软引用有更短的生命周期。
注意:弱引用/软引用对象只有当没有其他强引用对象引用它时,垃圾回收时才回收该引用。
即如果有一个对象(不是弱引用/软引用)引用了弱引用/软引用对象,那么垃圾回收是不会回收该引用对象。
使用基于空间和基于容量的缓存会使用一定的策略移除旧数据,常见的如下:
实际应用中基于LRU的缓存较多,如Guava Cache、EhCache支持LRU。
使用Java堆内存来存储对象。可以使用Guava Cache、Ehcache 3.x、MapDB实现。
即缓存数据存储在堆外内存。可以使用Ehcache 3.x、MapDB实现。
即缓存数据的存储在磁盘上。当JVM重启时数据还是在的。而堆缓存/堆外缓存重启时数据会丢失,需要重新加载。可以使用Ehcache 3.x、MapDB实现。
在多JVM实例的情况时,进程内缓存和磁盘缓存会存在两个问题:1.单机容量问题; 2.数据一致性问题(既然数据允许缓存,则表示允许一定时间内的不一致,因此可以设置缓存数据的过期时间来定期更新数据); 3.缓存不命中时,需要回源到DB/服务查询变多:每个实例在缓存不命中情况下都会回源到DB加载数据,因此,多实例后DB整体的访问量就变多了。解决办法可以使用如一致性哈希分片算法来解决。因此,这些情况可以考虑使用分布式缓存来解决。可以使用ehcache-clustered(配合Terracotta server)实现Java进程间分布式缓存。当然也可以使用如Redis实现分布式缓存。
两种模式如下:
Guava Cache只提供堆缓存,小巧灵活,性能最好,如果只使用堆缓存,那么使用它就够了。
Cache myCache=
CacheBuilder.newBuilder()
.concurrencyLevel(4)
.expireAfterWrite(10, TimeUnit.SECONDS)
.maximumSize(10000)
.build();
然后可以通过put、getIfPresent 来读写缓存。CacheBuilder有几类参数:缓存回收策略、并发设置等。
maximumSize:设置缓存的容量,当超出maximumSize时,按照LRU进行缓存回收。
weakKeys/weakValues:设置弱引用缓存。
softValues:设置软引用缓存。
invalidate(Object key)/invalidateAll(Iterablekeys)/invalidateAll():主动失效某些缓存数据。
什么时候触发失效呢? Guava Cache不会在缓存数据失效时立即触发回收操作(如果要这么做,则需要有额外的线程来进行清理),是在PUT时会主动进行一次清理缓存,当然读者也可以根据实际业务通过自己设计线程来调用cleanUp方法进行清理。
concurrencyLevel:Guava Cache重写了ConcurrentHashMap,concurrencyLevel用来设置Segment数量,concurrencyLevel越大并发能力越强。
recordStats:启动记录统计信息,比如命中率等
CacheManager cacheManager = CacheManagerBuilder. newCacheManagerBuilder(). build(true);
CacheConfigurationBuilder cacheConfig= CacheConfigurationBuilder.newCacheConfigurationBuilder(
String.class,
String.class,
ResourcePoolsBuilder.newResourcePoolsBuilder()
.heap(100, EntryUnit.ENTRIES))
.withDispatcherConcurrency(4)
.withExpiry(Expirations.timeToLiveExpiration(Duration.of(10,TimeUnit.SECONDS)));
Cache myCache = cacheManager.createCache("myCache",cacheConfig);
CacheManager在JVM关闭时请调用CacheManager.close()方法。 可以通过PUT、GET来读写缓存。CacheConfigurationBuilder也有几类参数:缓存回收策略、并发设置、统计命中率等。
heap(100, EntryUnit.ENTRIES):设置缓存的条目数量,当超出此数量时按照LRU进行缓存回收。
heap(100, MemoryUnit.MB):设置缓存的内存空间,当超出此空间时按照LRU进行缓存回收。另外,应该设置withSizeOfMaxObjectGraph(2):统计对象大小时对象图遍历深度和withSizeOfMaxObjectSize(1, MemoryUnit.KB):可缓存的最大对象大小。
withExpiry(Expirations.timeToLiveExpiration(Duration.of(10,TimeUnit.SECONDS))):设置TTL,没有TTI。
withExpiry(Expirations.timeToIdleExpiration(Duration.of(10,TimeUnit.SECONDS))):同时设置TTL和TTI,且TTL和TTI值一样。
remove(K key)/ removeAll(Set keys)/clear():主动失效某些缓存数据。
什么时候触发失效呢?EhCache使用了类似于Guava Cache同样的机制。
目前还没有提供API来设置,EhCache内部使用ConcurrentHashMap作为缓存存储,默认并发级别16。withDispatcherConcurrency是用来设置事件分发时的并发级别。
HTreeMap myCache =
DBMaker.heapDB().concurrencyScale(16).make().hashMap("myCache")
.expireMaxSize(10000)
.expireAfterCreate(10, TimeUnit.SECONDS)
.expireAfterUpdate(10,TimeUnit.SECONDS)
.expireAfterGet(10, TimeUnit.SECONDS)
.create();
然后可以通过PUT、GET来读写缓存。其有几类参数:缓存回收策略、并发设置、统计命中率等。
expireMaxSize:设置缓存的容量,当超出expireMaxSize时,按照LRU进行缓存回收。
concurrencyScale:类似于Guava Cache配置。
还可以使用DBMaker.memoryDB()创建堆缓存,它将数据序列化并存储到1MB大小的byte[]数组中,从而减少垃圾回收的影响。
CacheConfigurationBuilder cacheConfig= CacheConfigurationBuilder.newCacheConfigurationBuilder(
String.class,
String.class,
ResourcePoolsBuilder.newResourcePoolsBuilder()
.offheap(100, MemoryUnit.MB))
.withDispatcherConcurrency(4)
.withExpiry(Expirations.timeToLiveExpiration(Duration.of(10,TimeUnit.SECONDS)))
.withSizeOfMaxObjectGraph(3)
.withSizeOfMaxObjectSize(1, MemoryUnit.KB);
堆外缓存不支持基于容量的缓存过期策略。
HTreeMap myCache =
DBMaker.memoryDirectDB().concurrencyScale(16).make().hashMap("myCache")
.expireStoreSize(64 * 1024 * 1024) //指定堆外缓存大小64MB
.expireMaxSize(10000)
.expireAfterCreate(10, TimeUnit.SECONDS)
.expireAfterUpdate(10, TimeUnit.SECONDS)
.expireAfterGet(10, TimeUnit.SECONDS)
.create();
在使用堆外缓存时,请记得添加JVM启动参数,如-XX:MaxDirectMemorySize=10G。
CacheManager cacheManager = CacheManagerBuilder. newCacheManagerBuilder()
//默认线程池
.using(PooledExecutionServiceConfigurationBuilder.newPooledExecutionServiceConfigurationBuilder().defaultPool("default",1, 10).build())
//磁盘文件存储位置
.with(new CacheManagerPersistenceConfiguration(newFile("D:\\bak")))
.build(true);
CacheConfigurationBuilder cacheConfig= CacheConfigurationBuilder. newCacheConfigurationBuilder(
String.class,
String.class,
ResourcePoolsBuilder.newResourcePoolsBuilder()
.disk(100, MemoryUnit.MB,true)) //磁盘缓存
.withDiskStoreThreadPool("default", 5) //使用"default"线程池进行dump文件到磁盘
.withExpiry(Expirations.timeToLiveExpiration(Duration.of(50,TimeUnit.SECONDS)))
.withSizeOfMaxObjectGraph(3)
.withSizeOfMaxObjectSize(1, MemoryUnit.KB);
在JVM停止时,记得调用cacheManager.close(),从而保证内存数据能dump到磁盘。
DB db = DBMaker
.fileDB("D:\\bak\\a.data")//数据存哪里
.fileMmapEnable() //启用mmap
.fileMmapEnableIfSupported() //在支持的平台上启用mmap
.fileMmapPreclearDisable() //让mmap文件更快
.cleanerHackEnable() //一些BUG处理
.transactionEnable() //启用事务
.closeOnJvmShutdown()
.concurrencyScale(16)
.make();
HTreeMap myCache = db.hashMap("myCache")
.expireMaxSize(10000)
.expireAfterCreate(10, TimeUnit.SECONDS)
.expireAfterUpdate(10, TimeUnit.SECONDS)
.expireAfterGet(10, TimeUnit.SECONDS)
.createOrOpen();
因为开启了事务,MapDB则开启了WAL。另外,操作完缓存后记得调用db.commit方法提交事务。
myCache.put("key" + counterWriter,"value" + counterWriter);
db.commit();
不建议使用。
性能非常好,有主从模式、集群模式。
如先查找堆缓存,如果没有查找磁盘缓存,则使用MapDB可以通过如下配置实现。
HTreeMap diskCache = db.hashMap("myCache")
.expireStoreSize(8 * 1024 * 1024 * 1024)
.expireMaxSize(10000)
.expireAfterCreate(10, TimeUnit.SECONDS)
.expireAfterUpdate(10, TimeUnit.SECONDS)
.expireAfterGet(10, TimeUnit.SECONDS)
.createOrOpen();
HTreeMap heapCache = db.hashMap("myCache")
.expireMaxSize(100)
.expireAfterCreate(10, TimeUnit.SECONDS)
.expireAfterUpdate(10, TimeUnit.SECONDS)
.expireAfterGet(10, TimeUnit.SECONDS)
.expireOverflow(diskCache) //当缓存溢出时存储到disk
.createOrOpen();
主要分两大类:Cache-Aside和Cache-As-SoR(Read-through、Write-through、Write-behind)
Cache-Aside 即业务代码围绕着Cache写,是由业务代码直接维护缓存,示例代码如下所示。
先从缓存获取数据,如果没有命中,则回源到SoR并将源数据放入缓存供下次读取使用。
//1、先从缓存中获取数据
value = myCache.getIfPresent(key);
if(value == null) {
//2.1、如果缓存没有命中,则回源到SoR获取源数据
value = loadFromSoR(key);
//2.2、将数据放入缓存,下次即可从缓存中获取数据
myCache.put(key, value);
}
先将数据写入SoR,写入成功后立即将数据同步写入缓存。
//1、先将数据写入SoR
writeToSoR(key,value);
//2、执行成功后立即同步写入缓存
myCache.put(key, value);
或者先将数据写入SoR,写入成功后将缓存数据过期,下次读取时再加载缓存。
//1、先将数据写入SoR
writeToSoR(key,value);
//2、失效缓存,然后下次读时再加载缓存
myCache.invalidate(key);
Cache-Aside适合使用AOP模式去实现
Cache-As-SoR即把Cache看作为SoR,所有操作都是对Cache进行,然后Cache再委托给SoR进行真实的读/写。即业务代码中只看到Cache的操作,看不到关于SoR相关的代码。有三种实现:read-through、write-through、write-behind。
Read-Through,业务代码首先调用Cache,如果Cache不命中由Cache回源到SoR,而不是业务代码(即由Cache读SoR)。使用Read-Through模式,需要配置一个CacheLoader组件用来回源到SoR加载源数据。Guava Cache和Ehcache 3.x都支持该模式。
LoadingCache> getCache =
CacheBuilder.newBuilder()
.softValues()
.maximumSize(5000).expireAfterWrite(2, TimeUnit.MINUTES)
.build(new CacheLoader>() {
@Override
public Result load(final Integer sortId) throwsException {
return categoryService.get(sortId);
}
});
在build Cache时,传入一个CacheLoader用来加载缓存,操作流程如下:
使用CacheLoader后有几个好处:
CacheManager cacheManager = CacheManagerBuilder. newCacheManagerBuilder(). build(true);
org.ehcache.Cache myCache =cacheManager. createCache ("myCache",
CacheConfigurationBuilder.newCacheConfigurationBuilder(String.class,String.class,
ResourcePoolsBuilder.newResourcePoolsBuilder().heap(100,MemoryUnit.MB))
.withDispatcherConcurrency(4)
.withExpiry(Expirations.timeToLiveExpiration(Duration.of(10,TimeUnit.SECONDS)))
.withLoaderWriter(newDefaultCacheLoaderWriter () {
@Override
public String load(String key) throws Exception {
return readDB(key);
}
@Override
public Map loadAll(Iterable extendsString> keys) throws BulkCacheLoadingException, Exception {
return null;
}
}));
Ehcache 3.1没有自己去解决Dog-pile effect。
Write-Through,称之为穿透写模式/直写模式,业务代码首先调用Cache写(新增/修改)数据,然后由Cache负责写缓存和写SoR,而不是业务代码。
使用Write-Through模式需要配置一个CacheWriter组件用来回写SoR。Guava Cache没有提供支持。Ehcache 3.x支持该模式。
Ehcache需要配置一个CacheLoaderWriter,CacheLoaderWriter知道如何去写SoR。当Cache需要写(新增/修改)数据时,首先调用CacheLoaderWriter来同步(立即)到SoR,成功后会更新缓存。
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(true);
Cache myCache =cacheManager.createCache ("myCache",
CacheConfigurationBuilder.newCacheConfigurationBuilder(String.class,String.class,
ResourcePoolsBuilder.newResourcePoolsBuilder().heap(100,MemoryUnit.MB))
.withDispatcherConcurrency(4)
.withExpiry(Expirations.timeToLiveExpiration(Duration.of(10,TimeUnit.SECONDS)))
.withLoaderWriter(newDefaultCacheLoaderWriter () {
@Override
public void write(String key, String value) throws Exception{
//write
}
@Override
public void writeAll(Iterable extends Map.Entry extendsString, ? extends String>> entries) throws BulkCacheWritingException,Exception {
for(Object entry: entries) {
//batch write
}
}
@Override
public void delete(Stringkey) throws Exception {
//delete
}
@Override
public void deleteAll(Iterable extends String>keys) throws BulkCacheWritingException, Exception {
for(Object key :keys) {
//batch delete
}
}
}).build());
Ehcache 3.x还是使用CacheLoaderWriter来实现,通过write(String key, String value)、writeAll(Iterable> entries)和delete(String key)、deleteAll(Iterable keys)分别来支持单个写、批量写和单个删除、批量删除操作。
操作流程如下:当我们调用myCache.put(“e”,”123”)或者myCache.putAll(map)时,写缓存。首先,Cache会将写操作立即委托给CacheLoaderWriter#write和#writeAll,然后由CacheLoaderWriter负责立即去写SoR。当写SoR成功后,再写入Cache。
Write-Behind,也叫Write-Back,称之为回写模式,不同于Write-Through是同步写SoR和Cache,Write-Behind是异步写。异步之后可以实现批量写、合并写、延时和限流。
略,可用EhCache实现
略,可用EhCache实现
有两种Copy Pattern, Copy-On-Read和Copy-On-Write。在Guava-Cache和EhCache中堆缓存都是基于引用的,这样如果哟人拿到缓存数据并修改了它,则可能发生不可预测的问题。Guava Cache没有提供支持,EhCache 3.x提供了支持。
public interface Copier<T> {
T copyForRead(T obj); //Copy-On-Read,比如myCache.get()
T copyForWrite(T obj); //Copy-On-Write,比如myCache.put()
}
参考来源:
[1] 亿级流量网站架构核心技术.张开涛著