java最强本地缓存-Caffeine
前言:
👏作者简介:我是笑霸final,一名热爱技术的在校学生。
📝个人主页:个人主页1 || 笑霸final的主页2
📕系列专栏:java专栏
📧如果文章知识点有错误的地方,请指正!和大家一起学习,一起进步👀
🔥如果感觉博主的文章还不错的话,👍点赞👍 + 👀关注👀 + 🤏收藏🤏
目录
- 一 简单使用
- 二、内存驱逐策略
- 三、存储空间
一 简单使用
添加依赖
<dependency><groupId>com.github.ben-manes.caffeine</groupId><artifactId>caffeine</artifactId><version>2.5.5</version>
</dependency>
添加策略
Caffeine提供了四种缓存添加策略:
● 手动加载cache
● 自动加载LoadingCache
● 手动异步加载AsyncCache
● 自动异步加载AsyncLoadingCache
- 手动加载
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);
- 自动加载
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);
- 手动异步
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);
- 自动异步
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);
二、内存驱逐策略
Caffeine 提供了三种驱逐策略,分别是基于容量,基于时间和基于引用三种类型
2.1基于内容
缓存将会尝试通过基于【Window TinyLfu】驱逐掉不会再被使用到的元素
内容也有两种实现:
● 基于个数:如果你的缓存容量不希望超过某个特定的大小,那么记得使用Caffeine.maximumSize(long)
● 基于权重:你的缓存可能中的元素可能存在不同的“权重”–打个比方,你的缓存中的元素可能有不同的内存占用–你也许需要借助Caffeine.weigher(Weigher) 方法来界定每个元素的权重并通过 Caffeine.maximumWeight(long)方法来界定缓存中元素的总权重来实现上述的场景。
2.2基于时间
基于时间也有三种实现:
● expireAfterAccess(long, TimeUnit): 一个元素在上一次读写操作后一段时间之后,在指定的时间后没有被再次访问将会被认定为过期项。理想选择: 当session因为不活跃而使元素过期的情况下使用此】● expireAfterWrite(long, TimeUnit): 一个元素将会在其创建或者最近一次被更新之后的一段时间后被认定为过期项。理想选择: 对被缓存的元素的时效性存在要求的场景下使用此】
● expireAfter(Expiry): 一个元素将会在指定的时间后被认定为过期项。理想选择: 当被缓存的元素过期时间收到外部资源影响的时候使用此】。
2.3基于引用
注意:
● 异步不支持
● key支持:弱引用
● value支持:软引用、弱引用。
基于引用也有三种实现:
● Caffeine.weakKeys() 在保存key的时候将会进行弱引用。这允许在GC的过程中,当key没有被任何强引用指向的时候去将缓存元素回收
● Caffeine.weakValues()在保存value的时候将会使用弱引用。这允许在GC的过程中,当value没有被任何强引用指向的时候去将缓存元素回收
● Caffeine.softValues()在保存value的时候将会使用软引用。为了相应内存的需要,在GC过程中被软引用的对象将会被通过LRU算法回收。由于使用软引用可能会影响整体性能,我们还是建议通过使用基于缓存容量的驱逐策略代替软引用的使用
三、存储空间
3.1构成
W-TinyLFU将缓存存储空间分为两个大的区域
:Window Cache(1%)
和Main Cache(99%)
.
● Main Cache[SLRU(Segmented LRU,即分段 LRU)]进一步划分为Protected Cache(保护区 80%)和Probation Cache(考察区 20%)两个区域,这两个区域都是基于LRU的Cache。
● 猜测:1%、99%(20%、80%)这样的配置应该是实验得来。不过这个比例 Caffeine 会在运行时根据统计数据(statistics)去动态调整
3.2写入机制
● 当有新的缓存项写(item)入缓存时,会先写入Window Cache区域,当Window Cache空间满时,最旧的缓存项(根据LRU) 会被移出Window Cache,放到 probation(观察) 区。
● 如果 probation 区也满了,就把 item 和 probation 将要淘汰的元素 victim,两个进行“PK”【TinyLFU算法】,胜者留在 probation,输者就要被淘汰了。
● Probation(观察区) 中的缓存项如果访问频率达到一定次数,会提升到Protected(保护区);
● 如果Protected也满了,最旧的缓存项也会移出Protected Cache,然后根据TinyLFU算法确定是丢弃(淘汰)还是写入Probation Cache。
- 【tips】每一个缓存项,都由 2 部分组成
● 数据:缓存数据本身;
● 访问频次:被访问的次数
3.3 TinyLFU淘汰算法
【 win Cache和protected cache淘汰出来的数据称为 candidate。Probation淘汰出来的数据称为victim】
这 3 个区域移出的缓存项都是各自区域中最久未被使用的数据,在 TinyLFU 过滤器中进行竞争,竞争算法如下:
- 如果 Candidate 的访问频率 > Victim 的访问频率,则直接淘汰 Victim ;
- 如果 Candidate 的访问频率 <= Victim 的访问频率,此时分为两种情况::
》 如果 Candidate 的访问频率 < 5 ,则淘汰 Candidate ;
》 如果 Candidate 的访问频率 >= 5 ,则在 Candidate 和 Victim 中随机淘汰一个。
频率计算:使用 Count-Min Sketch 算法存储访问频率,极大地节省了空间。
● 类似于布隆过滤器,多次hash,但是数值不仅仅是0 1了,多次hash取值最低的当做访问频率。
● 值最大为4位,也就是15。
● 频度也不是一直不变,为了让缓存降低新鲜度。当整体的统计计数(当前所有记录的频率统计之和,这个数值内部维护)达到某一个值时,那么所有记录的频率统计除以 2。