互联网软件神速发展,用户的体验度是判断一个软件好坏的重要原因,所以缓存就是必不可少的一个神器。在多线程高并发场景中往往是离不开cache的,需要根据不同的应用场景来需要选择不同的cache,比如分布式缓存如redis、memcached,还有本地(进程内)缓存如ehcache、GuavaCache、Caffeine。
说起Guava Cache,很多人都不会陌生,它是Google Guava工具包中的一个非常方便易用的本地化缓存实现,基于LRU算法实现,支持多种缓存过期策略。由于Guava的大量使用,Guava Cache也得到了大量的应用。但是,Guava Cache的性能一定是最好的吗?也许,曾经,它的性能是非常不错的。但所谓长江后浪推前浪,总会有更加优秀的技术出现。今天,我就来介绍一个比Guava Cache性能更高的缓存框架:Caffeine。
官方性能比较
Google Guava工具包中的一个非常方便易用的本地化缓存实现,基于LRU算法实现,支持多种缓存过期策略。
EhCache 是一个纯Java的进程内缓存框架,具有快速、精干等特点,是Hibernate中默认的CacheProvider。
Caffeine是使用Java8对Guava缓存的重写版本,在Spring Boot 2.0中将取代,基于LRU算法实现,支持多种缓存过期策略。
场景二:6个线程读,2个线程写,也就是75%的读操作,25%的写操作。
可以清楚地看到Caffeine效率明显高于其他缓存。
如何使用?
< span class ="" > 1 < /span > public < span class ="" > static < /span > < span class ="" > void < /span > main ( < span class ="" > String < /span > [ ] args ) {
< span class ="" > 2 < /span > LoadingCache < < span class ="" > String < /span > , < span class ="" > String < /span > > build = CacheBuilder . newBuilder ( ) . initialCapacity ( < span class ="" > 1 < /span > ) . maximumSize ( < span class ="" > 100 < /span > ) . expireAfterWrite ( < span class ="" > 1 < /span > , TimeUnit . DAYS )
< span class ="" > 3 < /span > . build ( < span class ="" > new < /span > CacheLoader < < span class ="" > String < /span > , < span class ="" > String < /span > > ( ) {
< span class ="" > 4 < /span > < span class ="" > //默认的数据加载实现,当调用get取值的时候,如果key没有对应的值,就调用这个方法进行加载</span>
< span class ="" > 5 < /span > < span class ="" > @ Override < /span >
< span class ="" > 6 < /span > public < span class ="" > String < /span > load ( < span class ="" > String < /span > key ) {
< span class ="" > 7 < /span > < span class ="" > return < /span > < span class ="" > "" < /span > ;
< span class ="" > 8 < /span > }
< span class ="" > 9 < /span > } ) ;
< span class ="" > 10 < /span > }
< span class ="" > 11 < /span > }
参数方法:
initialCapacity(1) 初始缓存长度为1;
maximumSize(100) 最大长度为100;
expireAfterWrite(1, TimeUnit.DAYS) 设置缓存策略在1天未写入过期缓存(后面讲缓存策略)。
过期策略
在Caffeine中分为两种缓存,一个是有界缓存,一个是无界缓存,无界缓存不需要过期并且没有界限。
在有界缓存中提供了三个过期API:
expireAfterWrite:代表着写了之后多久过期。(上面列子就是这种方式)
expireAfterAccess:代表着最后一次访问了之后多久过期。
expireAfter:在expireAfter中需要自己实现Expiry接口,这个接口支持create、update、以及access了之后多久过期。注意这个API和前面两个API是互斥的。这里和前面两个API不同的是,需要你告诉缓存框架,它应该在具体的某个时间过期,也就是通过前面的重写create、update、以及access的方法,获取具体的过期时间。
更新策略
何为更新策略?就是在设定多长时间后会自动刷新缓存。
Caffeine提供了refreshAfterWrite()方法来让我们进行写后多久更新策略:
< span class ="" > 1 < /span > LoadingCache < < span class ="" > String < /span > , < span class ="" > String < /span > > build = CacheBuilder . newBuilder ( ) . refreshAfterWrite ( < span class ="" > 1 < /span > , TimeUnit . DAYS )
< span class ="" > 2 < /span > . build ( < span class ="" > new < /span > CacheLoader < < span class ="" > String < /span > , < span class ="" > String < /span > > ( ) {
< span class ="" > 3 < /span > < span class ="" > @ Override < /span >
< span class ="" > 4 < /span > public < span class ="" > String < /span > load ( < span class ="" > String < /span > key ) {
< span class ="" > 5 < /span > < span class ="" > return < /span > < span class ="" > "" < /span > ;
< span class ="" > 6 < /span > }
< span class ="" > 7 < /span > } ) ;
< span class ="" > 8 < /span > }
上面的代码我们需要建立一个CacheLodaer来进行刷新,这里是同步进行的,可以通过buildAsync方法进行异步构建。在实际业务中这里可以把我们代码中的mapper传入进去,进行数据源的刷新。
但是实际使用中,你设置了一天刷新,但是一天后你发现缓存并没有刷新。这是因为只有在1天后这个缓存再次访问后才能刷新,如果没人访问,那么永远也不会刷新。
我们来看看自动刷新是怎么做的呢?自动刷新只存在读操作之后,也就是我们的afterRead()这个方法,其中有个方法叫refreshIfNeeded,它会根据你是同步还是异步然后进行刷新处理。
填充策略(Population)
Caffeine 为我们提供了三种填充策略:手动、同步和异步。
手动加载(Manual)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
< span class ="" > 1 < /span > Cache < < span class ="" > String < /span > , < span class ="" > Object < /span > > manualCache = Caffeine . newBuilder ( )
< span class ="" > 2 < /span > . expireAfterWrite ( < span class ="" > 10 < /span > , TimeUnit . MINUTES )
< span class ="" > 3 < /span > . maximumSize ( < span class ="" > 10 < /span > _000 )
< span class ="" > 4 < /span > . build ( ) ;
< span class ="" > 5 < /span >
< span class ="" > 6 < /span > < span class ="" > String < /span > key = < span class ="" > "name1" < /span > ;
< span class ="" > 7 < /span > < span class ="" > // 根据key查询一个缓存,如果没有返回NULL</span>
< span class ="" > 8 < /span > graph = manualCache . getIfPresent ( key ) ;
< span class ="" > 9 < /span > < span class ="" > // 根据Key查询一个缓存,如果没有调用createExpensiveGraph方法,并将返回值保存到缓存。</span>
< span class ="" > 10 < /span > < span class ="" > // 如果该方法返回Null则manualCache.get返回null,如果该方法抛出异常则manualCache.get抛出异常</span>
< span class ="" > 11 < /span > graph = manualCache . < span class ="" > get < /span > ( key , k -> createExpensiveGraph ( k ) ) ;
< span class ="" > 12 < /span > < span class ="" > // 将一个值放入缓存,如果以前有值就覆盖以前的值</span>
< span class ="" > 13 < /span > manualCache . put ( key , graph ) ;
< span class ="" > 14 < /span > < span class ="" > // 删除一个缓存</span>
< span class ="" > 15 < /span > manualCache . invalidate ( key ) ;
< span class ="" > 16 < /span >
< span class ="" > 17 < /span > ConcurrentMap < < span class ="" > String < /span > , < span class ="" > Object < /span > > map = manualCache . asMap ( ) ;
< span class ="" > 18 < /span > cache . invalidate ( key ) ;
Cache接口允许显式的去控制缓存的检索、更新和删除。我们可以通过cache.getIfPresent(key) 方法来获取一个key的值,通过cache.put(key, value)方法显示的将数控放入缓存,但是这样子会覆盖缓原来key的数据。更加建议使用cache.get(key,k – > value) 的方式,get 方法将一个参数为 key 的 Function (createExpensiveGraph) 作为参数传入。
如果缓存中不存在该键,则调用这个 Function 函数,并将返回值作为该缓存的值插入缓存中。get 方法是以阻塞方式执行调用,即使多个线程同时请求该值也只会调用一次Function方法。这样可以避免与其他线程的写入竞争,这也是为什么使用 get 优于 getIfPresent 的原因。
注意:如果调用该方法返回NULL(如上面的 createExpensiveGraph 方法),则cache.get返回null。如果调用该方法抛出异常,则get方法也会抛出异常。
可以使用Cache.asMap() 方法获取ConcurrentMap进而对缓存进行一些更改。
同步加载(Loading)
< span class ="" > 1 < /span > LoadingCache < < span class ="" > String < /span > , < span class ="" > Object < /span > > loadingCache = Caffeine . newBuilder ( )
< span class ="" > 2 < /span > . maximumSize ( < span class ="" > 10 < /span > _000 )
< span class ="" > 3 < /span > . expireAfterWrite ( < span class ="" > 10 < /span > , TimeUnit . MINUTES )
< span class ="" > 4 < /span > . build ( key -> createExpensiveGraph ( key ) ) ;
< span class ="" > 5 < /span >
< span class ="" > 6 < /span > < span class ="" > String < /span > key = < span class ="" > "name1" < /span > ;
< span class ="" > 7 < /span > < span class ="" > // 采用同步方式去获取一个缓存和上面的手动方式是一个原理。在build Cache的时候会提供一个createExpensiveGraph函数。</span>
< span class ="" > 8 < /span > < span class ="" > // 查询并在缺失的情况下使用同步的方式来构建一个缓存</span>
< span class ="" > 9 < /span > < span class ="" > Object < /span > graph = loadingCache . < span class ="" > get < /span > ( key ) ;
< span class ="" > 10 < /span >
< span class ="" > 11 < /span > < span class ="" > // 获取组key的值返回一个Map</span>
< span class ="" > 12 < /span > < span class ="" > List < /span > < < span class ="" > String < /span > > keys = < span class ="" > new < /span > ArrayList < > ( ) ;
< span class ="" > 13 < /span > keys . add ( key ) ;
< span class ="" > 14 < /span > < span class ="" > Map < /span > < < span class ="" > String < /span > , < span class ="" > Object < /span > > graphs = loadingCache . getAll ( keys ) ;
LoadingCache是使用CacheLoader来构建的缓存的值。批量查找可以使用getAll方法。默认情况下,getAll将会对缓存中没有值的key分别调用CacheLoader.load方法来构建缓存的值。我们可以重写CacheLoader.loadAll方法来提高getAll的效率。
注意:可以编写一个CacheLoader.loadAll来实现为特别请求的key加载值。例如,如果计算某个组中的任何键的值将为该组中的所有键提供值,则loadAll可能会同时加载该组的其余部分。
异步加载(Asynchronously Loading)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
< span class ="" > 1 < /span > AsyncLoadingCache < < span class ="" > String < /span > , < span class ="" > Object < /span > > asyncLoadingCache = Caffeine . newBuilder ( )
< span class ="" > 2 < /span > . maximumSize ( < span class ="" > 10 < /span > _000 )
< span class ="" > 3 < /span > . expireAfterWrite ( < span class ="" > 10 < /span > , TimeUnit . MINUTES )
< span class ="" > 4 < /span > < span class ="" > // Either: Build with a synchronous computation that is wrapped as asynchronous</span>
< span class ="" > 5 < /span > . buildAsync ( key -> createExpensiveGraph ( key ) ) ;
< span class ="" > 6 < /span > < span class ="" > // Or: Build with a asynchronous computation that returns a future</span>
< span class ="" > 7 < /span > < span class ="" > // .buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor));</span>
< span class ="" > 8 < /span >
< span class ="" > 9 < /span > < span class ="" > String < /span > key = < span class ="" > "name1" < /span > ;
< span class ="" > 10 < /span >
< span class ="" > 11 < /span > < span class ="" > // 查询并在缺失的情况下使用异步的方式来构建缓存</span>
< span class ="" > 12 < /span > CompletableFuture < < span class ="" > Object < /span > > graph = asyncLoadingCache . < span class ="" > get < /span > ( key ) ;
< span class ="" > 13 < /span > < span class ="" > // 查询一组缓存并在缺失的情况下使用异步的方式来构建缓存</span>
< span class ="" > 14 < /span > < span class ="" > List < /span > < < span class ="" > String < /span > > keys = < span class ="" > new < /span > ArrayList < > ( ) ;
< span class ="" > 15 < /span > keys . add ( key ) ;
< span class ="" > 16 < /span > CompletableFuture < < span class ="" > Map < /span > < < span class ="" > String < /span > , < span class ="" > Object < /span > > > graphs = asyncLoadingCache . getAll ( keys ) ;
< span class ="" > 17 < /span > < span class ="" > // 异步转同步</span>
< span class ="" > 18 < /span > loadingCache = asyncLoadingCache . synchronous ( ) ;
AsyncLoadingCache是继承自LoadingCache类的,异步加载使用Executor去调用方法并返回一个CompletableFuture。异步加载缓存使用了响应式编程模型。
如果要以同步方式调用时,应提供CacheLoader。要以异步表示时,应该提供一个AsyncCacheLoader,并返回一个CompletableFuture。
synchronous()这个方法返回了一个LoadingCacheView视图,LoadingCacheView也继承自LoadingCache。调用该方法后就相当于你将一个异步加载的缓存AsyncLoadingCache转换成了一个同步加载的缓存LoadingCache。
默认使用ForkJoinPool.commonPool()来执行异步线程,但是我们可以通过Caffeine.executor(Executor) 方法来替换线程池。
驱逐策略(eviction)
Caffeine提供三类驱逐策略:基于大小(size-based),基于时间(time-based)和基于引用(reference-based)。
基于大小(size-based)
基于大小驱逐,有两种方式:一种是基于缓存大小,一种是基于权重。
< span class ="" > 1 < /span > // Evict based on the number of entries in the <span class="">cache</span>
< span class ="" > 2 < /span > // 根据缓存的计数进行驱逐
< span class ="" > 3 < /span > LoadingCache < < span class ="" > Key < /span > , Graph > graphs = Caffeine . newBuilder ( )
< span class ="" > 4 < /span > . maximumSize ( < span class ="" > 10 < /span > _000 )
< span class ="" > 5 < /span > . build ( < span class ="" > key < /span > -> createExpensiveGraph ( < span class ="" > key < /span > ) ) ;
< span class ="" > 6 < /span >
< span class ="" > 7 < /span > // Evict based on the number of vertices in the <span class="">cache</span>
< span class ="" > 8 < /span > // 根据缓存的权重来进行驱逐(权重只是用于确定缓存大小,不会用于决定该缓存是否被驱逐)
< span class ="" > 9 < /span > LoadingCache < < span class ="" > Key < /span > , Graph > graphs = Caffeine . newBuilder ( )
< span class ="" > 10 < /span > . maximumWeight ( < span class ="" > 10 < /span > _000 )
< span class ="" > 11 < /span > . weigher ( ( < span class ="" > Key < /span > < span class ="" > key < /span > , Graph graph ) -> graph . vertices ( ) . size ( ) )
< span class ="" > 12 < /span > . build ( < span class ="" > key < /span > -> createExpensiveGraph ( < span class ="" > key < /span > ) ) ;
我们可以使用Caffeine.maximumSize(long)方法来指定缓存的最大容量。当缓存超出这个容量的时候,会使用Window TinyLfu策略来删除缓存。我们也可以使用权重的策略来进行驱逐,可以使用Caffeine.weigher(Weigher) 函数来指定权重,使用Caffeine.maximumWeight(long) 函数来指定缓存最大权重值。
注意:maximumWeight与maximumSize不可以同时使用。
基于时间(Time-based)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
< span class ="" > 1 < /span > < span class ="" > // Evict based on a fixed expiration policy</span>
< span class ="" > 2 < /span > < span class ="" > // 基于固定的到期策略进行退出</span>
< span class ="" > 3 < /span > LoadingCache < Key , Graph > graphs = Caffeine . newBuilder ( )
< span class ="" > 4 < /span > . expireAfterAccess ( < span class ="" > 5 < /span > , TimeUnit . MINUTES )
< span class ="" > 5 < /span > . build ( key -> createExpensiveGraph ( key ) ) ;
< span class ="" > 6 < /span > LoadingCache < Key , Graph > graphs = Caffeine . newBuilder ( )
< span class ="" > 7 < /span > . expireAfterWrite ( < span class ="" > 10 < /span > , TimeUnit . MINUTES )
< span class ="" > 8 < /span > . build ( key -> createExpensiveGraph ( key ) ) ;
< span class ="" > 9 < /span >
< span class ="" > 10 < /span > < span class ="" > // Evict based on a varying expiration policy</span>
< span class ="" > 11 < /span > < span class ="" > // 基于不同的到期策略进行退出</span>
< span class ="" > 12 < /span > LoadingCache < Key , Graph > graphs = Caffeine . newBuilder ( )
< span class ="" > 13 < /span > . expireAfter ( < span class ="" > new < /span > Expiry < Key , Graph > ( ) {
< span class ="" > 14 < /span > < span class ="" > @ Override < /span >
< span class ="" > 15 < /span > < span class ="" > public long expireAfterCreate ( Key key , Graph graph , long currentTime ) < /span > {
< span class ="" > 16 < /span > < span class ="" > // Use wall clock time, rather than nanotime, if from an external resource</span>
< span class ="" > 17 < /span > < span class ="" > long < /span > seconds = graph . creationDate ( ) . plusHours ( < span class ="" > 5 < /span > )
< span class ="" > 18 < /span > . minus ( System . currentTimeMillis ( ) , MILLIS )
< span class ="" > 19 < /span > . toEpochSecond ( ) ;
< span class ="" > 20 < /span > < span class ="" > return < /span > TimeUnit . SECONDS . toNanos ( seconds ) ;
< span class ="" > 21 < /span > }
< span class ="" > 22 < /span >
< span class ="" > 23 < /span > < span class ="" > @ Override < /span >
< span class ="" > 24 < /span > < span class ="" > public long expireAfterUpdate ( Key key , Graph graph ,
25 long currentTime , long currentDuration ) < /span > {
< span class ="" > 26 < /span > < span class ="" > return < /span > currentDuration ;
< span class ="" > 27 < /span > }
< span class ="" > 28 < /span >
< span class ="" > 29 < /span > < span class ="" > @ Override < /span >
< span class ="" > 30 < /span > < span class ="" > public long expireAfterRead ( Key key , Graph graph ,
31 long currentTime , long currentDuration ) < /span > {
< span class ="" > 32 < /span > < span class ="" > return < /span > currentDuration ;
< span class ="" > 33 < /span > }
< span class ="" > 34 < /span > } )
< span class ="" > 35 < /span > . build ( key -> createExpensiveGraph ( key ) ) ;
基于引用(reference-based)
Java 4种引用的级别由高到低依次为:强引用 > 软引用 > 弱引用 > 虚引用。
< span class ="" > 1 < /span > < span class ="" > // Evict when neither the key nor value are strongly reachable</span>
< span class ="" > 2 < /span > < span class ="" > // 当key和value都没有引用时驱逐缓存</span>
< span class ="" > 3 < /span > LoadingCache < Key , Graph > graphs = Caffeine . newBuilder ( )
< span class ="" > 4 < /span > . weakKeys ( )
< span class ="" > 5 < /span > . weakValues ( )
< span class ="" > 6 < /span > . build ( key -> createExpensiveGraph ( key ) ) ;
< span class ="" > 7 < /span >
< span class ="" > 8 < /span > < span class ="" > // Evict when the garbage collector needs to free memory</span>
< span class ="" > 9 < /span > < span class ="" > // 当垃圾收集器需要释放内存时驱逐</span>
< span class ="" > 10 < /span > LoadingCache < Key , Graph > graphs = Caffeine . newBuilder ( )
< span class ="" > 11 < /span > . softValues ( )
< span class ="" > 12 < /span > . build ( key -> createExpensiveGraph ( key ) ) ;
我们可以将缓存的驱逐配置成基于垃圾回收器。为此,我们可以将key和 value配置为弱引用或只将值配置成软引用。
注意:AsyncLoadingCache不支持弱引用和软引用。
移除监听器(Removal)
驱逐(eviction):由于满足了某种驱逐策略,后台自动进行的删除操作;
无效(invalidation):表示由调用方手动删除缓存;
移除(removal):监听驱逐或无效操作的监听器。
手动删除缓存:
在任何时候,都可能明确地使缓存无效,而不用等待缓存被驱逐。
< span class ="" > 1 < /span > // individual key
< span class ="" > 2 < /span > cache . invalidate ( < span class ="" > key < /span > )
< span class ="" > 3 < /span > // <span class="">bulk</span> <span class="">keys</span>
< span class ="" > 4 < /span > cache . invalidateAll ( < span class ="" > keys < /span > )
< span class ="" > 5 < /span > // all <span class="">keys</span>
< span class ="" > 6 < /span > cache . invalidateAll ( )
Removal 监听器:
可以通过Caffeine.removalListener(RemovalListener) 为缓存指定一个删除侦听器,以便在删除数据时执行某些操作。 RemovalListener可以获取到key、value和RemovalCause(删除的原因)。
删除侦听器的里面的操作是使用Executor来异步执行的。默认执行程序是ForkJoinPool.commonPool(),可以通过Caffeine.executor(Executor)覆盖。当操作必须与删除同步执行时,请改为使用CacheWrite,CacheWrite将在下面说明。
注意:由RemovalListener抛出的任何异常都会被记录(使用Logger)并不会抛出。
统计(Statistics)
< span class ="" > Cache < /span > < < span class ="" > Key < /span > , Graph > graphs = Caffeine . newBuilder ( )
< span class ="" > 2 < /span > . maximumSize ( < span class ="" > 10 < /span > _000 )
< span class ="" > 3 < /span > . recordStats ( )
< span class ="" > 4 < /span > . build ( ) ;
使用Caffeine.recordStats(),可以打开统计信息收集。Cache.stats() 方法返回提供统计信息的CacheStats,如:
hitRate():返回命中与请求的比率;
hitCount(): 返回命中缓存的总数;
evictionCount():缓存逐出的数量;
averageLoadPenalty():加载新值所花费的平均时间。
总结
Caffeine的调整不只有算法上面的调整,内存方面的优化也有很大进步,Caffeine的API的操作功能和Guava是基本保持一致的,并且Caffeine为了兼容之前是Guava的用户,所以使用或者重写缓存到Caffeine应该没什么问题,但是也要看项目情况,不要盲目使用。
[转]如何解决多线程高并发场景下的 Java 缓存问题?