Java程序中的常见的四种缓存类型及代码实现
Posted 沛沛老爹
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java程序中的常见的四种缓存类型及代码实现相关的知识,希望对你有一定的参考价值。
在Java程序中,有的时候需要根据不同的场景来使用不同的缓存类型。在Java中主要分别有堆缓存、堆外缓存、磁盘缓存、分布式缓存等。
堆缓存
使用Java堆内存来存储缓存对象。使用堆缓存的好处是没有序列化/反序列化,是最快的缓存。缺点也很明显,当缓存的数据量很大时,GC(垃圾回收)暂停时间会变长,存储容量受限于堆空间大小。一般通过软引用/弱引用来存储缓存对象,即当堆内存不足时,可以强制回收这部分内存释放堆内存空间。一般使用堆缓存存储较热的数据。可以使用Guava Cache、Ehcache 3.x、MapDB实现。
1.Gauva Cache实现
Cache<String,String> cache = CacheBuilder.newBuilder()
.concurrencyLevel(4)
.expireAfterWrite(10,TimeUnit.SECONDS)
.maximumSize(10000)
.build();
然后可以通过put、getIfPresent来读写缓存。CacheBuilder有几类参数:缓存回收策略、并发设置、统计命中率等。
maximumSize: 设置缓存的容量,当超出maximumSize时,按照LRU进行缓存回收。
expireAfterWrite: 设置TTL,缓存数据在给定的时间内没有写(创建/覆盖)时,则被回收,即定期会回收缓存数据。
expireAfterAccess: 设置TTI,缓存数据在给定的时间内没有被读/写时,则被回收。每次访问时,都会更新它的TTI,从而如果该缓存是非常热的数据,则将一直不过期,可能会导致脏数据存在很长时间(因此,建议设置expireAfterWrite)。
weakKeys/weakValues: 设置弱引用缓存。
softValues: 设置软引用缓存。
invalidate(Object key)/ invalidateAll(Iterable<?> keys)/invalidateAll(): 主动失效某些缓存数据。
什么时候触发失效呢?Guava Cache不会在缓存数据失效时立即触发回收操作,而在PUT时会主动进行一次缓存清理,当然读者也可以根据实际业务通过自己设计线程来调用cleanUp方法进行清理。
concurrencyLevel: Guava Cache重写了ConcurrentHashMap,concurrencyLevel用来设置Segment数量,concurrencyLevel越大并发能力越强。
recordStats: 启动记录统计信息,比如命中率等。
2.Ehcache 3.x实现
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(true);
CacheConfigurationBuilder<String, String> cacheConfig = CacheConfigurationBuilder.newCacheConfigurationBuilder(
String.class,String.class,
ResourcePoolsBuilder.newResourcePoolsBuilder().heap(100,EntryUnit.ENTRIES))
.withDispatcherConcurrency(4)
.withExpiry(Expirations.timeToLiveExpiration(Duration.of(10,TimeUnit.SECONDS)));
Cache<String,String> cache = cacheManager.createCache("cache",concheConfig);
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<? extends K> keys)/clear(): 主动失效某些缓存数据。
什么时候触发失效呢?Ehcache使用了类似于Guava Cache的机制。
withDispatcherConcurrency:是用来设置事件分发时的并发级别。
3.MapDB 3.x实现
HTreeMap cache = DBMark.heapDB()
.concurrencyScale(16)
.make()
.hashMap("cache")
.expireMaxSize(1000)
.expireAfterCreate(10,TimeUnit.SECONDS)
.expireAfterUpdate(10,TimeUnit.SECONDS)
.expireAfterGet(10,TimeUnit.SECONDS)
.create();
然后可以通过PUT、GET来读写缓存。其有几类参数:缓存回收策略、并发设置、统计命中率等。
expireMaxSize: 设置缓存的容量,当超出expireMaxSize时,按照LRU进行缓存回收。
expireAfterCreate/expireAfterUpdate: 设置TTL,缓存数据在给定的时间内没有写(创建/覆盖)时,则被回收,即定期地会回收缓存数据。
expireAfterGet: 设置TTI,缓存数据在给定的时间内没有被读/写时,则被回收。每次访问时都会更新它的TTI,从而如果该缓存是非常热的数据,则将一直不过期,可能会导致脏数据存在很长的时间(因此,建议要设置expireAfterCreate/expireAfterUpdate)。
remove(Object key) /clear(): 主动失效某些缓存数据。
什么时候触发失效呢?MapDB默认使用类似于Guava Cache的机制。不过,也支持通过如下配置使用线程池定期进行缓存失效。
.expireExecutor(scheduledExecutorService )
.expireExecutorPeriod(3000)
concurrencyScale: 类似于Guava Cache的配置。
堆外缓存
即缓存数据存储在堆外内存,可以减少GC暂停时间(堆对象转移到堆外,GC扫描和移动的对象变少了),可以支持更大的缓存空间(只受机器内存大小限制,不受堆空间的影响)。但是,读取数据时需要序列化/反序列化,因此会比堆缓存慢很多。可以使用Ehcache 3.x、MapDB实现。
1.EhCache 3.x实现
CacheConfigurationBuilder<String, String> 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);
堆外缓存不支持基于容量的缓存过期策略。
2.MapDB 3.x实现
HTreeMap cache = DBMark.memoryDirectDB().concurrencyScale(16).make().hashMap("cache")
.expireStoreSize(64*1024*1024)
.expireMaxSize(1000)
.expireAfterCreate(10,TimeUnit.SECONDS)
.expireAfterUpdate(10,TimeUnit.SECONDS)
.expireAfterGet(10,TimeUnit.SECONDS)
.create();
在使用堆外缓存时,请记得添加JVM启动参数,如-XX:MaxDirectMemorySize=6G。
磁盘缓存
即缓存数据存储在磁盘上,在JVM重启时数据还是存在的,而堆缓存/堆外缓存数据会丢失,需要重新加载。可以使用Ehcache 3.x、MapDB实现。
1.EhCache 3.x实现
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
.using(PoolExecutionServiceConfigurationBuilder
.newPooledExecutionServiceCoonfigurationBuilder()
.defaultPool("default",1,10)
.build())
.with(new CacheManagerPersistenceConfiguration(new File("home\\back")))
.build(true);
CacheConfigurationBuilder<String,String> cacheConfig =
CacheConfigurationBuilder.newCacheConfigurationBuilder(
String.class,
String.calss,
ResourcePoolsBuilder.newResourcePoolsBuilder()
.disk(100,MemoryUnit.MB,true))
.withDiskStoreThreadPool("default",5)
.withExpiry(Expirations.timeToLiveExpiration(Duration.of(30,TimeUnit.SECONDS)))
.withSizeOfMaxObjectGraph(3)
.withSizeOfMaxObjectSize(1,MemoryUnit.KB);
在JVM停止时,记得调用cacheManager .close(),从而保证内存数据能dump到磁盘。
2.MapDB 3.x实现
DB db = DBMark.fileDB("home\\back\\db.data")
//启用mmap
.fileMmapEnable()
.fileMmapEnableIfSupported()
.fileMmapPreclearDisable()
.cleanerHackEnable()
//启用事务
.transactionEnable()
.closeOnJvmShutdown()
.concurrencyScale(16)
.make();
HTreeMap cache = db.hashMap("cache")
.expireMaxSize(1000)
.expireAfterCreate(10,TimeUnit.SECONDS)
.expireAfterUpdate(10,TimeUnit.SECONDS)
.expireAfterGet(10,TimeUnit.SECONDS)
.createOrOpen();
因为开启了事务,MapDB则开启了WAL。另外,操作完缓存后记得调用db.commit方法提交事务。
cache.put("key" + counterWriter, "value" + counterWriter);
db .commit();
分布式缓存:
上文提到的缓存是进程内缓存和磁盘缓存,在多JVM实例的情况下,会存在两个问题:1.单机容量问题;2.数据一致性问题(多台JVM实例的缓存数据不一致怎么办?),不过,这个问题不用太纠结,既然数据允许缓存,则表示允许一定时间内的不一致,因此可以设置缓存数据的过期时间来定期更新数据;3.缓存不命中时,需要回源到DB/服务请求多变问题:每个实例在缓存不命中的情况下都会回源到DB加载数据,因此,多实例后DB整体的访问量就变多了,解决办法是可以使用如一致性哈希分片算法。因此,这些情况可以考虑使用分布式缓存来解决。可以使用ehcache-clustered(配合Terracotta server)实现Java进程间分布式缓存。当然也可以使用如Redis实现分布式缓存。
两种模式如下。
· 单机时: 存储最热的数据到堆缓存,相对热的数据到堆外缓存,不热的数据到磁盘缓存。
· 集群时: 存储最热的数据到堆缓存,相对热的数据到堆外缓存,全量数据到分布式缓存。
以上是关于Java程序中的常见的四种缓存类型及代码实现的主要内容,如果未能解决你的问题,请参考以下文章