写点什么

自定义对象池在 Caffeine 框架中实践

作者:FunTester
  • 2024-04-11
    河北
  • 本文字数:3224 字

    阅读完需:约 11 分钟

前文讲到自定义对象池的实现,通常来说都是获取到对象,使用完之后要主动归还对象。但是在某些场景下,并不能轻易在代码中调用 returnObject 方法归还。此时就需要 Case by Case 具体情况具体分析,解决了。


今天分享一下自定义对象池在本地高性能缓存框架 Caffeine 中的使用。从对象池中借出的对象会存放在 Caffeine 缓存当中,然后就需要依赖 Caffeine 自己的过期和资源回收策略,决定何时回收对象。


Caffeine 框架提供了一个 API 用于处理对象过期或者被淘汰时业务逻辑,就是 RemovalListenerRemovalListener 可以监视缓存中的条目移除,并在移除时执行自定义的逻辑。


下面是使用案例:


public static void main(String[] args) {        // 创建一个带有RemovalListener的缓存        Cache<String, String> cache = Caffeine.newBuilder()                .maximumSize(100)                .removalListener(new MyRemovalListener())                .build();
// 将键值对放入缓存 cache.put("tester1", "FunTester001"); cache.put("tester2", "FunTester002");
// 从缓存中移除一个条目 cache.invalidate("key1"); }
// 自定义的RemovalListener实现 static class MyRemovalListener implements RemovalListener<String, String> { @Override public void onRemoval(String key, String value, RemovalCause cause) { System.out.println("Key: " + key + ", Value: " + value + " has been removed from the cache."); } }
复制代码


但是在我在业务中使用下面的代码时,却发生意外的情况。


        Caffeine.newBuilder()                .expireAfterWrite(1, TimeUnit.MINUTES)                .build()
复制代码


当对象过期以后,并没有出发 RemovalListener 执行。所以我查询官方资源,Caffeine 的回收策略,有三种:


  1. 定时清理:在固定的时间间隔内执行清理操作。

  2. 延迟清理:在缓存中的条目过期后一定时间内执行清理操作。

  3. 手动清理:在特定的事件触发时手动执行清理操作。


我们需要手动指定 Caffeine 回收策略,也就是调度器 schedule 。它负责在缓存中管理定期清理过期条目的操作。Caffeine 允许你自定义调度器的实现,以便更灵活地控制缓存的行为。


在 Caffeine 中,有四种不同类型的调度器(Scheduler)可供选择,它们分别是:SystemSchedulerGuardedSchedulerDisabledSchedulerExecutorServiceScheduler。下面我将解释它们之间的差异:


  1. SystemScheduler

  2. SystemScheduler是 Caffeine 的默认调度器。

  3. 它使用系统级的调度机制来执行定期清理任务。

  4. 这种调度器适用于大多数场景,并且通常表现良好。

  5. GuardedScheduler

  6. GuardedScheduler是一个具有保护机制的调度器。

  7. 它可以确保任务不会重复执行,即使调度器被多次触发。

  8. 这种调度器适用于需要保证任务不会被重复执行的场景。

  9. DisabledScheduler

  10. DisabledScheduler是一个禁用调度器,它不会执行任何清理任务。

  11. 当你不希望缓存自动执行清理操作时,可以使用这个调度器。

  12. 这种调度器适用于不需要自动清理的场景,或者你希望手动控制清理的时机。

  13. ExecutorServiceScheduler

  14. ExecutorServiceScheduler是一个基于ScheduledExecutorService的调度器。

  15. 它使用ScheduledExecutorService来执行定期清理任务。

  16. 这种调度器适用于需要更精细控制清理任务的执行方式,比如使用自定义的线程池、调整执行频率等。


这些调度器提供了不同的选择,以满足不同的需求和场景。你可以根据自己的需求来选择适合的调度器类型,以确保缓存的清理操作能够按照预期的方式执行。


那么问题来了,Caffeine 默认的调度器是那个呢?为什么没有及时回收掉过期的资源呢?


Caffeine 源码中,我得到了答案,位于 com.github.benmanes.caffeine.cache.Caffeine#getScheduler,内容如下:


    Scheduler getScheduler() {        if (this.scheduler != null && this.scheduler != Scheduler.disabledScheduler()) {            return this.scheduler == Scheduler.systemScheduler() ? this.scheduler : Scheduler.guardedScheduler(this.scheduler);        } else {            return Scheduler.disabledScheduler();        }    }
复制代码


默认的是 disabledScheduler() ,顾名思义,看起来是关闭了调度器的功能。下一步我们看看实际代码 com.github.benmanes.caffeine.cache.DisabledScheduler 内容:


package com.github.benmanes.caffeine.cache;
import java.util.Objects;import java.util.concurrent.Executor;import java.util.concurrent.Future;import java.util.concurrent.TimeUnit;
enum DisabledScheduler implements Scheduler { INSTANCE;
private DisabledScheduler() { }
public Future<Void> schedule(Executor executor, Runnable command, long delay, TimeUnit unit) { Objects.requireNonNull(executor); Objects.requireNonNull(command); Objects.requireNonNull(unit); return DisabledFuture.INSTANCE; }}
复制代码


我们看看 schedule() 方法,除了验证了一下参数意外,好像什么都没做啊,事实上就是什么都没做。实际上依靠 Caffeine 的一些驱逐策略完成的回收,也就是当我们访问过期资源、手动置无效、手动调用清理资源方法才能触发。


作为对比,我们看一下另外一个调度器的实现 com.github.benmanes.caffeine.cache.SystemScheduler


package com.github.benmanes.caffeine.cache;
import java.util.concurrent.CompletableFuture;import java.util.concurrent.Executor;import java.util.concurrent.Future;import java.util.concurrent.TimeUnit;
enum SystemScheduler implements Scheduler { INSTANCE;
private SystemScheduler() { }
public Future<?> schedule(Executor executor, Runnable command, long delay, TimeUnit unit) { Executor delayedExecutor = CompletableFuture.delayedExecutor(delay, unit, executor); return CompletableFuture.runAsync(command, delayedExecutor); }}
复制代码


看到是使用了线程池实现异步功能,其中涉及到了一个具有延迟涉及的线程池,绕的有点远了,这里不再详细说明。


解决办法也有了,就是设置有用的调度器,我选择了 com.github.benmanes.caffeine.cache.SystemScheduler ,实测也是足够满足需求的。代码如下:


static void main(String[] args) {      def pool = new FunPool<String>(new FunPooledFactory<String>() {// 创建对象池          @Override          String newInstance() {              return "FunTester" + getRandomInt(Integer.MAX_VALUE)// 创建对象          }      })      def listener = new RemovalListener<String, String>() {          @Override          void onRemoval(String key, String value, RemovalCause removalCause) {              output("Key: $key , Value: $value cause: $removalCause")// 打印移除原因              pool.back(value)// 回收对象          }      }        def build = Caffeine<String, String>.newBuilder()              .expireAfterWrite(1, TimeUnit.SECONDS)// 设置写入后过期时间              .maximumSize(100)   // 设置缓存的最大容量              .removalListener(listener)  // 设置缓存移除监听器              .scheduler(Scheduler.systemScheduler())              .build()      build.put("tester", pool.borrow())// 放入缓存      sleep(2.0)// 等待缓存过期  }
复制代码


控制台输出:


12:29:15:054 ForkJoinPool.commonPool-worker-2 Key: tester , Value: FunTester208146637 cause: EXPIRED
复制代码


成功触发 RemovalListener ,且将对象归还给对象池。

发布于: 刚刚阅读数: 4
用户头像

FunTester

关注

公众号:FunTester,800篇原创,欢迎关注 2020-10-20 加入

Fun·BUG挖掘机·性能征服者·头顶锅盖·Tester

评论

发布
暂无评论
自定义对象池在Caffeine框架中实践_FunTester_InfoQ写作社区