在Java的世界中,缓存技术被广泛用于提升应用性能,主要分为两大类:远程缓存和本地缓存。远程缓存,以其卓越的性能和灵活性,常通过如Redis和Memcached这样的流行解决方案来实现。而本地缓存,则以其轻量级和快速访问的特点,由HashMap、Guava Cache、Caffeine和Ehcache等技术代表。
在未来的博文中,我们将深入探讨远程缓存的奥秘,但今天,让我们聚焦于本地缓存。本文将首先带您领略本地缓存技术的风采,为您提供一个全面的概览。紧接着,我们将深入探讨被誉为性能之王的缓存技术,探索其背后的原理和实现方式。最后,通过一系列实际案例,我们将展示如何在日常工作中有效利用这些高性能的本地缓存技术,以提升您的开发效率和应用性能。
一、 Java本地缓存
1.1 HashMap
利用Map的底层实现,我们可以将待缓存的对象直接存储在内存中,这是一种直接而高效的方法。
优势:这种方法简单直接,无需依赖外部库,非常适合那些对缓存需求不复杂、场景较为简单的应用。
劣势:然而,这种方法缺乏自动的缓存淘汰机制,如果需要实现更高级的缓存策略,可能需要较高的定制化开发成本。
public class LRUCache extends LinkedHashMap {
/**
* 可重入读写锁,保证并发读写安全性
*/
private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private Lock readLock = readWriteLock.readLock();
private Lock writeLock = readWriteLock.writeLock();
/**
* 缓存大小限制
*/
private int maxSize;
public LRUCache(int maxSize) {
super(maxSize + 1, 1.0f, true);
this.maxSize = maxSize;
}
@Override
public Object get(Object key) {
readLock.lock();
try {
return super.get(key);
} finally {
readLock.unlock();
}
}
@Override
public Object put(Object key, Object value) {
writeLock.lock();
try {
return super.put(key, value);
} finally {
writeLock.unlock();
}
}
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return this.size() > maxSize;
}
}
1.2 Guava Cache
Guava Cache是由Google开发的一种缓存技术,它基于LRU(最近最少使用)替换算法。然而,随着Caffeine的崛起,Guava Cache逐渐淡出了人们的视野。Caffeine不仅继承了Guava Cache的优点,还在多个方面实现了超越。虽然在此不提供具体的示例代码,但对Guava Cache感兴趣的读者可以访问其官方网站了解更多信息。
优势:Guava Cache支持设置最大容量限制,并提供了两种过期策略:基于插入时间和访问时间,同时还支持一些基础的统计功能。
劣势:随着Spring Boot 2和Spring 5的发布,它们都不再推荐使用Guava Cache。
1.3 Caffeine
Caffeine是一种开源的缓存技术,它采用了W-TinyLFU算法,这是一种结合了LRU和LFU(最少使用频率)优点的缓存淘汰策略。Caffeine的缓存性能接近理论上的最优解,可以看作是Guava Cache的升级版。
public class CaffeineCacheTest {
public static void main(String[] args) throws Exception {
//创建guava cache
Cache<String, String> loadingCache = Caffeine.newBuilder()
//cache的初始容量
.initialCapacity(5)
//cache最大缓存数
.maximumSize(10)
//设置写缓存后n秒钟过期
.expireAfterWrite(17, TimeUnit.SECONDS)
//设置读写缓存后n秒钟过期,实际很少用到,类似于expireAfterWrite
//.expireAfterAccess(17, TimeUnit.SECONDS)
.build();
String key = "key";
// 往缓存写数据
loadingCache.put(key, "v");
// 获取value的值,如果key不存在,获取value后再返回
String value = loadingCache.get(key, CaffeineCacheTest::getValueFromDB);
// 删除key
loadingCache.invalidate(key);
}
private static String getValueFromDB(String key) {
return "v";
}
}
1.4 Ehcache
Ehcache是一个纯粹的Java进程内缓存框架,以其快速和高效的性能而著称。它被广泛认可,并作为Hibernate的默认缓存提供者。
优势:Ehcache提供了广泛的缓存淘汰算法,包括LFU(最少使用频率)、LRU(最近最少使用)和FIFO(先进先出)。它支持不同类型的缓存存储,如堆内缓存、堆外缓存以及磁盘缓存,以适应不同的存储需求。此外,Ehcache还支持多种集群方案,有效解决了数据共享的问题。
劣势:尽管Ehcache在许多方面表现出色,但在性能方面,它与Caffeine相比略显不足。
public class EncacheTest {
public static void main(String[] args) throws Exception {
// 声明一个cacheBuilder
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
.withCache("encacheInstance", CacheConfigurationBuilder
//声明一个容量为20的堆内缓存
.newCacheConfigurationBuilder(String.class,String.class, ResourcePoolsBuilder.heap(20)))
.build(true);
// 获取Cache实例
Cache<String,String> myCache = cacheManager.getCache("encacheInstance", String.class, String.class);
// 写缓存
myCache.put("key","v");
// 读缓存
String value = myCache.get("key");
// 移除换粗
cacheManager.removeCache("myCache");
cacheManager.close();
}
}
二、高性能缓存Caffeine
2.1 缓存类型
2.1.1 Cache
Cache<Key, Graph> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10_000)
.build();
// 查找一个缓存元素, 没有查找到的时候返回null
Graph graph = cache.getIfPresent(key);
// 查找缓存,如果缓存不存在则生成缓存元素, 如果无法生成则返回null
graph = cache.get(key, k -> createExpensiveGraph(key));
// 添加或者更新一个缓存元素
cache.put(key, graph);
// 移除一个缓存元素
cache.invalidate(key);
Cache 接口提供了显式搜索查找、更新和移除缓存元素的能力。当缓存的元素无法生成或者在生成的过程中抛出异常而导致生成元素失败,cache.get 也许会返回 null 。
2.1.2 Loading Cache
LoadingCache<Key, Graph> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));
// 查找缓存,如果缓存不存在则生成缓存元素, 如果无法生成则返回null
Graph graph = cache.get(key);
// 批量查找缓存,如果缓存不存在则生成缓存元素
Map<Key, Graph> graphs = cache.getAll(keys);
一个LoadingCache是一个Cache 附加上 CacheLoader能力之后的缓存实现。
如果缓存不错在,则会通过CacheLoader.load来生成对应的缓存元素。
2.1.3 Async Cache
AsyncCache<Key, Graph> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10_000)
.buildAsync();
// 查找一个缓存元素, 没有查找到的时候返回null
CompletableFuture<Graph> graph = cache.getIfPresent(key);
// 查找缓存元素,如果不存在,则异步生成
graph = cache.get(key, k -> createExpensiveGraph(key));
// 添加或者更新一个缓存元素
cache.put(key, graph);
// 移除一个缓存元素
cache.synchronous().invalidate(key);
AsyncCache就是Cache的异步形式,提供了Executor生成缓存元素并返回CompletableFuture的能力。默认的线程池实现是 ForkJoinPool.commonPool() ,当然你也可以通过覆盖并实现 Caffeine.executor(Executor)方法来自定义你的线程池选择。
2.1.4 Async Loading Cache
AsyncLoadingCache<Key, Graph> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
// 你可以选择: 去异步的封装一段同步操作来生成缓存元素
.buildAsync(key -> createExpensiveGraph(key));
// 你也可以选择: 构建一个异步缓存元素操作并返回一个future
.buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor));
// 查找缓存元素,如果其不存在,将会异步进行生成
CompletableFuture<Graph> graph = cache.get(key);
// 批量查找缓存元素,如果其不存在,将会异步进行生成
CompletableFuture<Map<Key, Graph>> graphs = cache.getAll(keys);
AsyncLoadingCache就是LoadingCache的异步形式,提供了异步load生成缓存元素的功能。
2.2 驱逐策略
基于容量
// 基于缓存内的元素个数进行驱逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumSize(10_000)
.build(key -> createExpensiveGraph(key));
// 基于缓存内元素权重进行驱逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumWeight(10_000)
.weigher((Key key, Graph graph) -> graph.vertices().size())
.build(key -> createExpensiveGraph(key));
基于时间
// 基于固定的过期时间驱逐策略
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.expireAfterAccess(5, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));
// 基于不同的过期驱逐策略
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.expireAfter(new Expiry<Key, Graph>() {
public long expireAfterCreate(Key key, Graph graph, long currentTime) {
// Use wall clock time, rather than nanotime, if from an external resource
long seconds = graph.creationDate().plusHours(5)
.minus(System.currentTimeMillis(), MILLIS)
.toEpochSecond();
return TimeUnit.SECONDS.toNanos(seconds);
}
public long expireAfterUpdate(Key key, Graph graph,
long currentTime, long currentDuration) {
return currentDuration;
}
public long expireAfterRead(Key key, Graph graph,
long currentTime, long currentDuration) {
return currentDuration;
}
})
.build(key -> createExpensiveGraph(key));
基于引用
// 当key和缓存元素都不再存在其他强引用的时候驱逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.weakKeys()
.weakValues()
.build(key -> createExpensiveGraph(key));
// 当进行GC的时候进行驱逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.softValues()
.build(key -> createExpensiveGraph(key));
2.3 刷新机制
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumSize(10_000)
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));
只有在LoadingCache中可以使用刷新策略,与驱逐不同的是,在刷新的时候如果查询缓存元素,其旧值将仍被返回,直到该元素的刷新完毕后结束后才会返回刷新后的新值。
2.4 统计
Cache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumSize(10_000)
.recordStats()
.build();
通过使用Caffeine.recordStats()方法可以打开数据收集功能。Cache.stats()方法将会返回一个CacheStats对象,其将会含有一些统计指标,比如:
hitRate(): 查询缓存的命中率
evictionCount(): 被驱逐的缓存数量
averageLoadPenalty(): 新值被载入的平均耗时
配合SpringBoot提供的RESTful Controller,能很方便的查询Cache的使用情况。