1. 前言
缓存(Cache)在代码世界中无处不在。从底层的CPU多级缓存,到客户端的页面缓存,处处都存在着缓存的身影。缓存从本质上来说,是一种空间换时间的手段,通过对数据进行一定的空间安排,使得下次进行数据访问时起到加速的效果。
就Java而言,其常用的缓存解决方案有很多,例如数据库缓存框架EhCache,分布式缓存Memcached等,这些缓存方案实际上都是为了提升吞吐效率,避免持久层压力过大。
对于常见缓存类型而言,可以分为本地缓存以及分布式缓存两种
分布式缓存: redis、memcached等
本地缓存: ehcache、GuavaCache、Caffeine等
1.1 官方性能比较
说起Guava Cache,很多人都不会陌生,它是Google Guava
工具包中的一个非常方便易用的本地化缓存实现,基于LRU算法实现,支持多种缓存过期策略。由于Guava的大量使用,Guava Cache也得到了大量的应用。但是,Guava Cache的性能一定是最好的吗?也许,曾经它的性能是非常不错的。正所谓长江后浪推前浪,前浪被拍在沙滩上,目前Spring5(SpringBoot2)开始用Caffeine取代Guava,详见官方信息:SPR-13797
下面就来对比一下各个缓存框架的性能,以下测试都是基于jmh测试的:
8个线程读,100%的读操作
6个线程读,2个线程写
- 8个线程写,100%的写操作
可以从数据看出来Caffeine的性能都比Guava要好。然后Caffeine的API的操作功能和Guava是基本保持一致的,并且 Caffeine为了兼容之前是Guava的用户,做了一个Guava的Adapter给大家使用也是十分的贴心
2. 什么是Caffeine
Caffeine是基于Java1.8的高性能本地缓存库,由Guava改进而来,官方说明指出,其缓存命中率已经接近最优值。实际上Caffeine这样的本地缓存和ConcurrentMap
很像,即支持并发,并且支持O(1)时间复杂度的数据存取。二者的区别主要在于:
ConcurrentMap
: 将存储所有存入的数据,直到显式将其移除
Caffeine
: 将通过给定的配置,自动移除”不常用”的数据,以保持内存的合理占用。
2. Caffeine基础
使用Caffeine
需要在工程中引入如下依赖
1 | <dependency> |
2.1 缓存加载策略
Cache手动创建
最普通的一种缓存,无需指定加载方式,需要手动调用put()
进行加载。需要注意的是put()
方法对已经存在的key将进行覆盖,这点和Map的表现是一致的。
在获取缓存值的时候,如果想要在缓存值不存在时,原子地将值写入缓存,则可以调用get(key, k->value)
方法,该方法将避免写入竞争,调用invalidate()
方法,将手动移除缓存
在多线程的情况下,当时用get(key, k->value)
时,如果有另一个线程同时调用本方法进行竞争,则后一个线程会被阻塞,直到前一个线程更新缓存完成;若另一个线程调用getIfPresent()
方法,则会立即返回null,不会被阻塞。
1 | public static void main(String[] args) { |
Loading Cache自动创建
LoadingCache是一种自动加载的缓存。其和普通缓存不同的地方在于,党缓存不存在/缓存已过期时,若调用get()
方法,则自动调用CacheLoader.load()
方法加载最新值。调用getAll()
方法将遍历所有的key调用get()
,除非实现了CacheLoader.loadAll()
方法。
使用LoadingCache时,需要指定CacheLoader,并实现其中的load()
方法供缓存缺失时自动加载。
在使用多线程情况下,当两个线程同时调用get()
,则后一线程将被阻塞,直至前一线程更新完成
1 | public static void main(String[] args) { |
Async Cache异步获取
AsyncCache是Cache的一个变体,其响应结果均为CompletableFuture
,通过这种方式,AsyncCache对异步编程模式进行了适配。默认情况下,缓存计算使用ForkJoinPool.commonPool()
作为线程池,如果想要指定线程池,则可以覆盖并实现Caffeine.excutor(Executor)
方法。synchronous()
提供了阻塞直到异步缓存生成完毕的能力,它将以Cache进行返回。
在多线程情况下,当两个线程同时调用get(key, k->value)
,则会返回同一个CompletableFuture
对象。由于返回结果本身不进行阻塞,可以根据业务设计自行选择阻塞带灯或者非阻塞。
1 | public static void main(String[] args) { |
2.2 驱逐策略
驱逐策略在创建缓存的时候进行指定。常用的有基于容量的驱逐和基于时间的驱逐
- 基于容量的驱逐需要指定缓存容量的最大值,当缓存容量达到最大时,Caffeine将使用LRU策略对缓存进行淘汰;
- 基于时间的驱逐如字面意思,可以设置在最后访问/写入一个缓存经过指定时间后,自动进行淘汰
驱逐策略可以组合使用,任意驱逐策略生效后,该缓存条目级被驱逐:
LRU 最近最少使用: 淘汰最长时间没有被使用的页面
LFU 最不经常使用:淘汰一段时间内使用次数最小的页面
FIFO先进显出
Caffeine有4种缓存淘汰设置:
大小(LFU算法进行淘汰)
权重(大小与权重,只能二选一)
时间
引用(不常用,本文不介绍)