Java程序中的常见的四种缓存类型及代码实现
在Java程序中,有的时候需要根据不同的场景来使⽤不同的缓存类型。在Java中主要分别有堆缓存、堆外缓存、磁盘缓存、分布式缓存等。
堆缓存
java线程池创建的四种使⽤Java堆内存来存储缓存对象。使⽤堆缓存的好处是没有序列化/反序列化,是最快的缓存。缺点也很明显,当缓存的数据量很⼤
时,GC(垃圾回收)暂停时间会变长,存储容量受限于堆空间⼤⼩。⼀般通过软引⽤/弱引⽤来存储缓存对象,即当堆内存不⾜时,可以强制回收这部分内存释放堆内存空间。⼀般使⽤堆缓存存储较热的数据。可以使⽤Guava Cache、Ehcache 3.x、MapDB实现。
1.Gauva Cache实现
Cache<String,String> cache = wBuilder()
.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 = wCacheManagerBuilder().build(true);
CacheConfigurationBuilder<String, String> cacheConfig = wCacheConfigurationBuilder(
String.class,String.class,
.
withDispatcherConcurrency(4)
.withExpiry(Expirations.timeToLiveExpiration(Duration.of(10,TimeUnit.SECONDS)));
Cache<String,String> cache = ateCache("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 = wCacheConfigurationBuilder(
String.class,String.wResourcePoolsBuilder().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 = DirectDB().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 = wCacheManagerBuilder()
.using(PoolExecutionServiceConfigurationBuilder
.newPooledExecutionServiceCoonfigurationBuilder()
.defaultPool("default",1,10)
.build())
.with(new CacheManagerPersistenceConfiguration(new File("home\back")))
.build(true);
CacheConfigurationBuilder<String,String> cacheConfig =
String.class,
String.calss,
.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。另外,操作完缓存后记得调⽤dbmit⽅法提交事务。
cache.put("key" + counterWriter, "value" + counterWriter);
db mit();
分布式缓存:
上⽂提到的缓存是进程内缓存和磁盘缓存,在多JVM实例的情况下,会存在两个问题:1.单机容量问题;2.数据⼀致性问题(多台JVM实例的缓存数据不⼀致怎么办?),不过,这个问题不⽤太纠结,既然数据允许缓存,则表⽰允许⼀定时间内的不⼀致,因此可以设置缓存数据的过期时间来定期更新数据;3.缓存不命中时,需要回源到DB/服务请求多变问题:每个实例在缓存不命中的情况下都会回源到DB加载数据,因此,多实例后DB整体的访问量就变多了,解决办法是可以使⽤如⼀致性哈希分⽚算法。因此,这些情况可以考虑使⽤分布式缓存来解决。可以使⽤ehcache-clustered(配合Terracotta server)实现Java进程间分布式缓存。当然也可以使⽤如Redis实现分布式缓存。
两种模式如下。
· 单机时: 存储最热的数据到堆缓存,相对热的数据到堆外缓存,不热的数据到磁盘缓存。
· 集时: 存储最热的数据到堆缓存,相对热的数据到堆外缓存,全量数据到分布式缓存。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论