Guava Cache 緩存數據被移除後的監聽器RemovalListener

之前文章已經介紹了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);
    }

}



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章