Caffeine与Guava对比

Caffeine/Guava性能测试

处于性能优化考虑,项目准备将本地缓存从guava cache 转到caffeine cache,于是着手对caffeine进行了一波调研,首先通过一系列测试,通过caffeine和guava从结果来看,在相同cpu负载下,Caffeine Cache的读取和写入速度优于Guava Cache,差距在4倍以上。

但在内存占用方面来看,两者无明显区别。

测试环境:

  • CPU:i7-8700 3.20GHz 6核
  • 内存:16g
  • 系统:Windows 10
  • JDK版本:8
  • IDE:IDEA
  • 内存监控工具:JProfiler

一、速度测试:

测试逻辑:

  • 构建Cache,load方法为简单的字符串拼接
  • 将250000个字符串加载到cache后,启动任务线程,预热10s后,开始计时,统计每10秒的count,共6轮,最后统计每轮中的每秒平均值

测试结果:

image-20200813143631711

(1) 6个线程纯读:

CEB58087-97C6-49B4-9E51-359CBDB199F7

(2) 4个线程读+2个线程写:

7A64933F-034B-42CC-B317-4E3E4CCF5788

二、内存占用测试

测试逻辑:

  • 基于项目中使用内存缓存需求最大的数据,构建缓存
  • 先初始化缓存对象,10s后将数据库中的20w+条数据存入缓存中,并主动触发一次gc,对比剩余内存占用

测试结果:

Guava和Caffeine在加载完24w条柜机数据后,通过GC清理掉临时占用的内存,最后都保持了420M的内存占用,无明显区别,整个内存变化如下:

(1)Guava:

1F084945-84E5-43B5-9F18-08959453A005

(2)Caffeine:

B4F8F7BC-684F-44A5-ACC7-08CC0B6BD314

三、源码分析

Caffeine是在guava基础上进行优化的产物,也是带着替代guava的目的而来的,因而在使用上差别不大,但是通过测试可以明显看到Caffeine在性能上的优势,进而,通过源码,进一步探究了一下Caffeine和guava的区别

一、初始化

Caffeine、Guava都通过builder的方式进行初始化操作,生成缓存对象,通过builder方式可以生成两种缓存对象LoadingCache(同步填充)和Cache(手动填充),LoadingCache继承Cache,相比于Cache,提供了get获取值时,如果不存在值,自动通过CacheLoader的load方法下载数据并返回的功能,此处load方法在初始化时通过重写进行定义,项目中基本通过同步填充的方式,从数据库中加载数据,需要提的是,通过手动加载的方式,也可以在put时传递可执行函数

方式一 cache:

//guava
Cache cache = CacheBuilder.newBuilder()
        .maximumSize(maximumSize).
        expireAfterWrite(expireAfterWriteDuration, timeUnit)
        .recordStats().build();
//caffeine
Cache cache = Caffeine.newBuilder()
        .maximumSize(maximumSize).
        expireAfterWrite(expireAfterWriteDuration, timeUnit)
        .recordStats().build();

方式二 LoadingCache:

//guava
LoadingCache<K, V> cache = CacheBuilder.newBuilder().maximumSize(maximumSize)
    .expireAfterWrite(expireAfterWriteDuration, timeUnit)
    .recordStats().build(new CacheLoader<K, V>() {
                        @Override
                        public V load(K key) throws Exception {
                            return loadData(key);
                        }
                    });

//caffeine
LoadingCache<K, V> cache = Caffeine.newBuilder().maximumSize(maximumSize)
    .expireAfterWrite(expireAfterWriteDuration, timeUnit)
    .recordStats().build(new CacheLoader<K, V>() {
                        @Override
                        public V load(K key) throws Exception {
                            return loadData(key);
                        }
                    });

初始化过程,guava 和Caffeine基本上没有太大区别,都是通过builder方式进行构建,设置过期方式,刷新时间,统计信息等。

虽然两者初始化方式大致一致,但有个问题需要注意,guava初始化时重写的load()方法,不能返回null值,但caffeine可以。

guava 调用get()时,load方法返回null时的报错代码:

image-20200817101440560

从caffeine的doComputeIfAbsent()方法可以看出,在load返回null时,get()调用直接返回null

image-20200817101257916

guava 和Caffeine 都可以通过这两种方式初始化缓存,代码几乎完全一样,除了这两种初始化方式外,caffeine cache还提供了第三种初始化方式,异步加载方式

caffeine 异步加载方式:

AsyncLoadingCache<K, V> cache = Caffeine.newBuilder()
                                .recordStats()
                                .maximumSize(maximumSize)
                                .expireAfterWrite(expireAfterWriteDuration, timeUnit)
                                .buildAsync((CacheLoader<K, V>) key -> {
                                    return loadData(key);
                                });

AsyncLoadingCache是继承自LoadingCache类的,异步加载使用Executor去调用方法并返回一个CompletableFuture,相比于同步填充模式,在load数据时,使用异步线程来执行load方法,默认使用ForkJoinPool.commonPool()来执行异步线程,我们可以通过Caffeine.executor(Executor) 方法来替换线程池。

通过asyncLoad对load方法进行了异步执行封装

image-20200817101539499

生成executor

image-20200817101809402

从初始化方式看不出两者太大的区别,caffeine提供的异步加载方式,在某些特定场景应该可以进一步提升数据加载的性能

二、基于时间的过期驱逐策略

guava 和caffeine都支持通过两种策略来进行数据的回收策略,分别是expireAfterWrite、expireAfterAccess,此外caffeine还支持通过expireAfter来通过重新相关方法自定义过期策略,这些过期策略都是初始化时进行指定

guava:

Cache guavaCache = CacheBuilder.newBuilder()
                            .expireAfterWrite(expireAfterWriteDuration, timeUnit)
                            .expireAfterAccess(expireAfterAccessDuration, timeUnit);

guava提供了两种回收策略,但是他并不是通过后台监听线程进行数据的清除工作,而是在获取值时进行值回收,所以如果一个key一直不被访问,虽然设置了过期策略,他依然会一直存在

1597307801074

通过get方法可以看出,guava在每次get时,通过getLiveValue方法去判断数据是否过期,并对过期数据进行清除,然后返回null,由判断条件可以看到在过期数据被驱逐后又会去调用lockedGetOrLoad进行数据加载,也就是说,对于数据过期,并不会导致数据直接失效,而是在get时,去load新的值,这就导致了一个问题,一旦一个key value存入缓存中,通过设置过期时间无法将它真正的从缓存中去除,如果没有设置maximumSize的话,就可能出现内存泄漏的情况

caffeine:

Cache guavaCache = Caffeine.newBuilder()
                            .expireAfterWrite(expireAfterWriteDuration, timeUnit)
                            .expireAfterAccess(expireAfterAccessDuration,timeUnit)
    						.expireAfter(...);

caffine和guava相同的两种过期策略也是惰性删除,在get时去进行过期判断过期

image-20200819114451679

并且从代码可以看出,和guava一样,如果数据过期,也会通过load方法去重新加载数据,这也导致caffine在设置过期策略时,会有和guava相同的问题,即如果没有设置maximumSize的话,就可能出现内存泄漏的情况

相比于guava使用队列的方式进行过期数据的处理,caffeine使用ConcurrentHashMap的compute进行旧值替换,并且在返回前使用生成异步任务的方式进行旧数据的清除,这里可以看出,caffeine相比于guava,在过期处理逻辑上减少了对get操作的影响

image-20200817102208106

image-20200817102257129

caffeine的自定义过期策略expireAfter也是在进行特定操作是进行过期校验,并进行过期的,一般情况下caffeine提供的两种方式就已经够用了,所以不做深究

通过对guava和caffeine时间过期策略的比较,可以看出,caffine通过异步删除旧值,优化了guava通过队列同步移除旧值,减少了过期处理对get性能的影响,并且caffeine使用面向JDK8的ConcurrentHashMap进行数据存储,由于在JDK8中ConcurrentHashMap增加了红黑树,在hash冲突严重时也有良好的可读性

三、基于大小的驱逐策略

无论是caffeine还是guava,通过设置过期时间,是无法使缓存值从缓存中驱逐出去的,只会在指定时间后被新值替代,所以,在使用caffeine或者guava的时候,设置maximumSize是很有必要的caffeine和guava也是在get或者put操作的时候根据设置的大小进行清除的,但是两者的清除算法存在区别,guava使用LRU算法进行实现,而caffeine使用综合LFU和LRU优点产生的W-TinyLFU进行数据清除,改良的算法可以更科学的进行非热点数据的驱逐,较大程度的增加缓存的命中率。

guava chache通过LRU(Least Recently Used)算法进行数据驱逐:

guava 在每次调用get方法时,如果获取到了值,会调用recordRead方法,来利用recencyQueue队列记录访问的信息

image-20200819151437488

在storeLoadedValue()方法中插入新值时,会调用evictEntries()方法来判断是否超过设定的最大值

image-20200819153145908

首先是调用drainRecencyQueue()方法,通过recencyQueue队列存储信息的顺序来移动accessQueue中的数据对象

image-20200819153308365

如果超过了设置的容量大小,就会调用getNextEvictable()方法从accessQueue中获取需要被驱逐的数据,然后调用removeEntry(e, e.getHash(), RemovalCause.SIZE)方法进行数据驱逐

image-20200819153333629

这里,guava cache主要通过recencyQueue、accessQueue两个队列来实现LRU算法对数据进行驱逐,但是LRU算法的缺点是对偶发性、周期性的批量操作会导致LRU命中率急剧下,降缓存污染情况比较严重。

caffeine chache通过W-TinyLFU算法进行数据驱逐:

caffeine chache主要通过accessOrderWindowDeque、accessOrderProbationDeque、accessOrderProtectedDeque三个队列来实现W-TinyLFU算法,首先,前面说过,caffeine是通过异步方式执行过期任务的,在执行任务中,会将数据放到对应的accessOrderWindowDeque队列中

image-20200819161311142

在maintenance()方法中调用evictEntries()进行数据驱逐,maintenance()在scheduleDrainBuffers(),performCleanUp()等方法中被调用

image-20200819161549890

在evictEntries()方法中,首先会通过evictFromWindow()方法,将可能被驱逐的数据从accessOrderWindowDeque队列区转入到accessOrderProbationDeque队列

image-20200819161825695

在新增更新任务时,任务执行时,调用onAccess方法,会将accessOrderProbationDeque转移到accessOrderProtectedDeque中

image-20200819164853299

通过这三个地方完成了数据在accessOrderWindowDeque、accessOrderProbationDeque、accessOrderProtectedDeque三个队列中的转化,实现W-TinyLFU算法,完成对guava cache算法的优化。

总结

通过对guava cache 和caffeine 从性能到算法及使用的对比中,可以发现Caffeine基本是在Guava的基础上进行优化而来的,提供的功能基本一致,但是通过对算法和部分逻辑的优化,完成了对性能极大的提升,而且我们可以发现,两者切换几乎没有成本,毕竟caffeine就是以替换guava cache为目的而来的。

  • 9
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值