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程序中的常见的四种缓存类型及代码实现的主要内容,如果未能解决你的问题,请参考以下文章

Java数据结构及算法实战系列002:算法的四种描述方式

Java数据结构及算法实战系列002:算法的四种描述方式

Java数据结构及算法实战系列002:算法的四种描述方式

.NET导出Excel的四种方法及评测

聚类算法中的四种距离及其python实现

Java中的四种引用类型,强引用,软引用,弱引用,虚引用