Guava Cache 缓存数据被移除后的监听器RemovalListener

Guava Cache的RemovalListener允许在数据被移除时接收通知,便于进行额外处理。当使用invalidate()清除缓存时,注册的监听器会被触发,RemovalNotification携带key、value和移除原因。默认情况下,监听器同步执行可能导致调用者线程阻塞。为避免这种情况,可以使用异步监听器RemovalListeners.asynchronous()来提高效率。

之前文章已经介绍了guava的容量管理,有4种方式可以将数据从缓存中移除。有的时候,我们需要在缓存被移除的时候,得到这个通知,并做一些额外处理工作。这个时候RemovalListener就派上用场了。

public class Main {

	// 创建一个监听器
	private static class MyRemovalListener implements RemovalListener<Integer, Integer> {
	@Override
	public void onRemoval(RemovalNotification<Integer, Integer> notification) {
		String tips = String.format("key=%s,value=%s,reason=%s", notification.getKey(), notification.getValue(), notification.getCause());
		System.out.println(tips);
	}
	}

	public static void main(String[] args) {

	// 创建一个带有RemovalListener监听的缓存
	Cache<Integer, Integer> cache = CacheBuilder.newBuilder().removalListener(new MyRemovalListener()).build();

	cache.put(1, 1);

	// 手动清除
	cache.invalidate(1);

	System.out.println(cache.getIfPresent(1)); // null
	}

}


使用invalidate()清除缓存数据之后,注册的回调被触发了。



RemovalNotification中包含了缓存的key、value以及被移除的原因RemovalCause。通过源码可以看出,移除原因与容量管理方式是相对应的。

public enum RemovalCause {
  /**
   * The entry was manually removed by the user. This can result from the user invoking
   * {@link Cache#invalidate}, {@link Cache#invalidateAll(Iterable)}, {@link Cache#invalidateAll()},
   * {@link Map#remove}, {@link ConcurrentMap#remove}, or {@link Iterator#remove}.
   */
  EXPLICIT {
    @Override
    boolean wasEvicted() {
      return false;
    }
  },

  /**
   * The entry itself was not actually removed, but its value was replaced by the user. This can
   * result from the user invoking {@link Cache#put}, {@link LoadingCache#refresh}, {@link Map#put},
   * {@link Map#putAll}, {@link ConcurrentMap#replace(Object, Object)}, or
   * {@link ConcurrentMap#replace(Object, Object, Object)}.
   */
  REPLACED {
    @Override
    boolean wasEvicted() {
      return false;
    }
  },

  /**
   * The entry was removed automatically because its key or value was garbage-collected. This
   * can occur when using {@link CacheBuilder#weakKeys}, {@link CacheBuilder#weakValues}, or
   * {@link CacheBuilder#softValues}.
   */
  COLLECTED {
    @Override
    boolean wasEvicted() {
      return true;
    }
  },

  /**
   * The entry's expiration timestamp has passed. This can occur when using
   * {@link CacheBuilder#expireAfterWrite} or {@link CacheBuilder#expireAfterAccess}.
   */
  EXPIRED {
    @Override
    boolean wasEvicted() {
      return true;
    }
  },

  /**
   * The entry was evicted due to size constraints. This can occur when using
   * {@link CacheBuilder#maximumSize} or {@link CacheBuilder#maximumWeight}.
   */
  SIZE {
    @Override
    boolean wasEvicted() {
      return true;
    }
  };

  /**
   * Returns {@code true} if there was an automatic removal due to eviction (the cause is neither
   * {@link #EXPLICIT} nor {@link #REPLACED}).
   */
  abstract boolean wasEvicted();
}

监听器使用很简单,有几个特点需要注意下:

1、默认情况下,监听器方法是被同步调用的(在移除缓存的那个线程中执行)。如果监听器方法比较耗时,会导致调用者线程阻塞时间变长。下面这段代码,由于监听器执行需要2s,所以main线程调用invalidate()要2s后才能返回。

public class Main {

    // 创建一个监听器
    private static class MyRemovalListener implements RemovalListener<Integer, Integer> {
        @Override
        public void onRemoval(RemovalNotification<Integer, Integer> notification) {
            String tips = String.format("key=%s,value=%s,reason=%s", notification.getKey(), notification.getValue(), notification.getCause());
            System.out.println(tips);

            try {
                // 模拟耗时
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }

    public static void main(String[] args) {

        // 创建一个带有RemovalListener监听的缓存
        final Cache<Integer, Integer> cache = CacheBuilder.newBuilder().removalListener(new MyRemovalListener()).build();
        cache.put(1, 1);
        cache.put(2, 2);

        System.out.println("main...begin.");
        cache.invalidate(1);// 耗时2s
        System.out.println("main...over.");
    }

}

解决这个问题的方法是:使用异步监听RemovalListeners.asynchronous(RemovalListener, Executor)。

public class Main {

    // 创建一个监听器
    private static class MyRemovalListener implements RemovalListener<Integer, Integer> {
        @Override
        public void onRemoval(RemovalNotification<Integer, Integer> notification) {
            String tips = String.format("key=%s,value=%s,reason=%s", notification.getKey(), notification.getValue(), notification.getCause());
            System.out.println(tips);

            try {
                // 模拟耗时
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }

    public static void main(String[] args) {

        RemovalListener<Integer, Integer> async = RemovalListeners.asynchronous(new MyRemovalListener(), Executors.newSingleThreadExecutor());
        // 创建一个带有RemovalListener监听的缓存
        final Cache<Integer, Integer> cache = CacheBuilder.newBuilder().removalListener(async).build();
        cache.put(1, 1);
        cache.put(2, 2);

        System.out.println("main...begin.");
        cache.invalidate(1);// main线程立刻返回
        System.out.println("main...over.");
    }

}


2、创建cache的时候只能添加1个监听器,这个监听器对象会被多个线程共享,所以如果监听器需要操作共享资源,那么一定要做好同步控制。下面这段代码可以看出:2个线程会交替执行监听器的发方法。

public class Main {

    // 创建一个监听器
    private static class MyRemovalListener implements RemovalListener<Integer, Integer> {
        @Override
        public void onRemoval(RemovalNotification<Integer, Integer> notification) {
            String tips = String.format("key=%s,value=%s,reason=%s", notification.getKey(), notification.getValue(), notification.getCause());
            System.out.println(tips);

            try {
                // 模拟耗时
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("process over.");
        }
    }

    public static void main(String[] args) {

        // 创建一个带有RemovalListener监听的缓存
        final Cache<Integer, Integer> cache = CacheBuilder.newBuilder().removalListener(new MyRemovalListener()).build();
        cache.put(1, 1);
        cache.put(2, 2);

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread1...trigger RemovalListener begin.");
                cache.invalidate(1);
                System.out.println("thread1...trigger RemovalListener over.");
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread2...trigger RemovalListener begin.");
                cache.invalidate(2);
                System.out.println("thread2...trigger RemovalListener over.");
            }
        }).start();
    }

}



3、监听器中抛出的任何异常,在被记录到日志后,会被guava丢弃,不会导致监听器不可用。下面这段代码可以看到:监听器中抛出的异常只是被记录了(打印到了控制台),并没有导致JVM退出,之后缓存被移除一样可以再次触发。
public class Main {

    // 创建一个监听器
    private static class MyRemovalListener implements RemovalListener<Integer, Integer> {
        @Override
        public void onRemoval(RemovalNotification<Integer, Integer> notification) {
            String tips = String.format("key=%s,value=%s,reason=%s", notification.getKey(), notification.getValue(), notification.getCause());
            System.out.println(tips);

            throw new RuntimeException();
        }
    }

    public static void main(String[] args) {

        // 创建一个带有RemovalListener监听的缓存
        final Cache<Integer, Integer> cache = CacheBuilder.newBuilder().removalListener(new MyRemovalListener()).build();
        cache.put(1, 1);
        cache.put(2, 2);

        cache.invalidate(1);
        cache.invalidate(2);
    }

}



Guava Cache中,可以通过监听器(`RemovalListener`)来监控缓存元素的加入和移除操作。以下是一些实现元素加入和移除监听的最佳实践: 1. **使用RemovalListener处理缓存失效后的清理逻辑**:当某个缓存项因过期、大小限制或显式删除而被移除时,可以通过`RemovalListener`执行自定义的清理操作。例如,可以将缓存内容同步到数据库或其他持久化存储中。 ```java RemovalListener<String, String> removalListener = notification -> { System.out.println("Removed key: " + notification.getKey() + ", value: " + notification.getValue()); }; ``` 2. **通过CacheBuilder配置监听器**:在构建缓存时,使用`CacheBuilder`的`removalListener()`方法添加监听器[^1]。这样可以在缓存初始化阶段就完成监听器的注册。 ```java Cache<String, String> cache = CacheBuilder.newBuilder() .maximumSize(100) .removalListener(removalListener) .build(); ``` 3. **避免在监听器中执行耗时操作**:由于`RemovalListener`默认是在缓存操作的线程中执行,因此应避免在监听器中执行阻塞或耗时较长的操作,以免影响缓存性能。可以考虑将耗时任务提交到异步线程池中执行。 4. **结合自动加载机制使用监听器**:如果使用了`CacheLoader`进行缓存值的自动加载,则可以在监听器中记录或处理缓存项的更新行为,例如日志记录或统计信息的更新。 5. **利用RemovalCause分类处理不同类型的移除事件**:`RemovalNotification`对象提供了`getCause()`方法,可以区分缓存项是由于何种原因被移除(如`EXPLICIT`、`REPLACED`、`SIZE`等),从而实现更精细的控制逻辑。 ```java RemovalListener<String, String> removalListener = notification -> { if (notification.getCause() == RemovalCause.SIZE) { System.out.println("Entry was evicted due to size constraints."); } else if (notification.getCause() == RemovalCause.EXPIRED) { System.out.println("Entry expired."); } }; ``` 6. **确保监听器线程安全**:由于Guava Cache监听器可能在多个线程中并发执行,因此需要确保监听器内部的状态管理是线程安全的。 7. **合理设置缓存大小与过期时间**:结合业务需求合理配置缓存的最大条目数和过期时间,以减少不必要的缓存项频繁创建和销毁,降低监听器的触发频率,提升系统整体性能[^1]。 ### 缓存元素加入的额外注意事项 - **手动添加元素时避免null值**:Guava Cache不允许将`null`作为缓存值插入,否则会抛出`NullPointerException`。因此,在调用`put()`或`putAll()`方法前,应确保所有值不为`null`[^1]。 ```java try { cache.put("key", null); // This will throw NullPointerException } catch (NullPointerException e) { System.out.println("Cannot put null value into Guava Cache."); } ``` - **预热缓存**:为了防止系统启动后首次访问缓存未命中,可以在系统启动时手动填充一些常用数据到缓存中,提前触发监听器并建立初始缓存状态。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值