Guava Cache 數據變化實現回調的監聽器RemovalListener

上一篇介紹了guava的使用,實現了項目第一個需求定期清理cache數據,第二個需求,我們需要在緩存被移除的時候,得到通知產生回調,並做一些額外處理工作。這個時候RemovalListener就派上用場了。

下面是獲得所有數據改變的監聽

[java] view plain copy
  1. public class Main {  
  2.   
  3.     // 創建一個監聽器  
  4.     private static class MyRemovalListener implements RemovalListener<Integer, Integer> {  
  5.     @Override  
  6.     public void onRemoval(RemovalNotification<Integer, Integer> notification) {  
  7.         String tips = String.format("key=%s,value=%s,reason=%s", notification.getKey(), notification.getValue(), notification.getCause());  
  8.         System.out.println(tips);  
  9.     }  
  10.     }  
  11.   
  12.     public static void main(String[] args) {  
  13.   
  14.     // 創建一個帶有RemovalListener監聽的緩存  
  15.     Cache<Integer, Integer> cache = CacheBuilder.newBuilder().removalListener(new MyRemovalListener()).build();  
  16.   
  17.     cache.put(11);  
  18.   
  19.     // 手動清除  
  20.     cache.invalidate(1);  
  21.   
  22.     System.out.println(cache.getIfPresent(1)); // null  
  23.     }  
  24.   
  25. }  

使用invalidate()清除緩存數據之後,註冊的回調被觸發了。



下面是隻有主動刪除數據使的回調

public class CacheConnection {
  
  public static RemovalListener<String, Connection> myRemovalListener = new RemovalListener<String, Connection>(){
    @Override  
    public void onRemoval(RemovalNotification<String, Connection> notification) {  
        String tips = String.format("key=%s,value=%s,reason=%s in myRemovalListener", notification.getKey(), notification.getValue(), notification.getCause());  
        System.out.println(tips);
        //when expireAfterAccess to do
        if (notification.getCause().equals("EXPIRED") && notification.getValue() != null) {
          try {
            notification.getValue().close();
          } catch (SQLException e) {
            System.out.printf("Exception in myRemovalListener:\n");
            e.printStackTrace();
          }
          
          System.out.printf("Remove %s in cacheConnection", notification.getKey());
        }
          
    }
  };
  
  public static Cache<String, Connection> cacheConnection = CacheBuilder.newBuilder()  
      //設置cache的初始大小爲20000,要合理設置該值  
      .initialCapacity(20000)  
      //設置併發數爲5,即同一時間最多隻能有5個線程往cache執行寫入操作  
      .concurrencyLevel(100)  
      //設置cache中的數據在600秒沒有被讀寫將自動刪除  
      .expireAfterAccess(600, TimeUnit.SECONDS) 
      //設置監聽,當出現自動刪除時的回調
      .removalListener(myRemovalListener)
      //構建cache實例  
      .build();  
  
  public static Connection getCache(String key)  {
    try {
      Connection var = cacheConnection.getIfPresent(key);
      return var;
    } catch (Exception e) {
      // TODO: handle exception
      System.out.println("the value of cacheConnection is null");
      e.printStackTrace();
      return null;
    }
  }
     
   public static void putCache(String key, Connection value) {
     cacheConnection.put(key, value);
   }
   

}


RemovalNotification中包含了緩存的key、value以及被移除的原因RemovalCause。通過源碼可以看出,移除原因與容量管理方式是相對應的。下面是具體的消息

[java] view plain copy
  1. public enum RemovalCause {  
  2.   /** 
  3.    * The entry was manually removed by the user. This can result from the user invoking 
  4.    * {@link Cache#invalidate}, {@link Cache#invalidateAll(Iterable)}, {@link Cache#invalidateAll()}, 
  5.    * {@link Map#remove}, {@link ConcurrentMap#remove}, or {@link Iterator#remove}. 
  6.    */  
  7.   EXPLICIT {  
  8.     @Override  
  9.     boolean wasEvicted() {  
  10.       return false;  
  11.     }  
  12.   },  
  13.   
  14.   /** 
  15.    * The entry itself was not actually removed, but its value was replaced by the user. This can 
  16.    * result from the user invoking {@link Cache#put}, {@link LoadingCache#refresh}, {@link Map#put}, 
  17.    * {@link Map#putAll}, {@link ConcurrentMap#replace(Object, Object)}, or 
  18.    * {@link ConcurrentMap#replace(Object, Object, Object)}. 
  19.    */  
  20.   REPLACED {  
  21.     @Override  
  22.     boolean wasEvicted() {  
  23.       return false;  
  24.     }  
  25.   },  
  26.   
  27.   /** 
  28.    * The entry was removed automatically because its key or value was garbage-collected. This 
  29.    * can occur when using {@link CacheBuilder#weakKeys}, {@link CacheBuilder#weakValues}, or 
  30.    * {@link CacheBuilder#softValues}. 
  31.    */  
  32.   COLLECTED {  
  33.     @Override  
  34.     boolean wasEvicted() {  
  35.       return true;  
  36.     }  
  37.   },  
  38.   
  39.   /** 
  40.    * The entry's expiration timestamp has passed. This can occur when using 
  41.    * {@link CacheBuilder#expireAfterWrite} or {@link CacheBuilder#expireAfterAccess}. 
  42.    */  
  43.   EXPIRED {  
  44.     @Override  
  45.     boolean wasEvicted() {  
  46.       return true;  
  47.     }  
  48.   },  
  49.   
  50.   /** 
  51.    * The entry was evicted due to size constraints. This can occur when using 
  52.    * {@link CacheBuilder#maximumSize} or {@link CacheBuilder#maximumWeight}. 
  53.    */  
  54.   SIZE {  
  55.     @Override  
  56.     boolean wasEvicted() {  
  57.       return true;  
  58.     }  
  59.   };  
  60.   
  61.   /** 
  62.    * Returns {@code true} if there was an automatic removal due to eviction (the cause is neither 
  63.    * {@link #EXPLICIT} nor {@link #REPLACED}). 
  64.    */  
  65.   abstract boolean wasEvicted();  
  66. }  

監聽器使用很簡單,有幾個特點需要注意下:

1、默認情況下,監聽器方法是被同步調用的(在移除緩存的那個線程中執行)。如果監聽器方法比較耗時,會導致調用者線程阻塞時間變長。下面這段代碼,由於監聽器執行需要2s,所以main線程調用invalidate()要2s後才能返回。

[java] view plain copy
  1. public class Main {  
  2.   
  3.     // 創建一個監聽器  
  4.     private static class MyRemovalListener implements RemovalListener<Integer, Integer> {  
  5.         @Override  
  6.         public void onRemoval(RemovalNotification<Integer, Integer> notification) {  
  7.             String tips = String.format("key=%s,value=%s,reason=%s", notification.getKey(), notification.getValue(), notification.getCause());  
  8.             System.out.println(tips);  
  9.   
  10.             try {  
  11.                 // 模擬耗時  
  12.                 Thread.sleep(2000);  
  13.             } catch (InterruptedException e) {  
  14.                 e.printStackTrace();  
  15.             }  
  16.   
  17.         }  
  18.     }  
  19.   
  20.     public static void main(String[] args) {  
  21.   
  22.         // 創建一個帶有RemovalListener監聽的緩存  
  23.         final Cache<Integer, Integer> cache = CacheBuilder.newBuilder().removalListener(new MyRemovalListener()).build();  
  24.         cache.put(11);  
  25.         cache.put(22);  
  26.   
  27.         System.out.println("main...begin.");  
  28.         cache.invalidate(1);// 耗時2s  
  29.         System.out.println("main...over.");  
  30.     }  
  31.   
  32. }  

解決這個問題的方法是:使用異步監聽RemovalListeners.asynchronous(RemovalListener, Executor)。

[java] view plain copy
  1. public class Main {  
  2.   
  3.     // 創建一個監聽器  
  4.     private static class MyRemovalListener implements RemovalListener<Integer, Integer> {  
  5.         @Override  
  6.         public void onRemoval(RemovalNotification<Integer, Integer> notification) {  
  7.             String tips = String.format("key=%s,value=%s,reason=%s", notification.getKey(), notification.getValue(), notification.getCause());  
  8.             System.out.println(tips);  
  9.   
  10.             try {  
  11.                 // 模擬耗時  
  12.                 Thread.sleep(2000);  
  13.             } catch (InterruptedException e) {  
  14.                 e.printStackTrace();  
  15.             }  
  16.   
  17.         }  
  18.     }  
  19.   
  20.     public static void main(String[] args) {  
  21.   
  22.         RemovalListener<Integer, Integer> async = RemovalListeners.asynchronous(new MyRemovalListener(), Executors.newSingleThreadExecutor());  
  23.         // 創建一個帶有RemovalListener監聽的緩存  
  24.         final Cache<Integer, Integer> cache = CacheBuilder.newBuilder().removalListener(async).build();  
  25.         cache.put(11);  
  26.         cache.put(22);  
  27.   
  28.         System.out.println("main...begin.");  
  29.         cache.invalidate(1);// main線程立刻返回  
  30.         System.out.println("main...over.");  
  31.     }  
  32.   
  33. }  


2、創建cache的時候只能添加1個監聽器,這個監聽器對象會被多個線程共享,所以如果監聽器需要操作共享資源,那麼一定要做好同步控制。下面這段代碼可以看出:2個線程會交替執行監聽器的發方法。

[java] view plain copy
  1. public class Main {  
  2.   
  3.     // 創建一個監聽器  
  4.     private static class MyRemovalListener implements RemovalListener<Integer, Integer> {  
  5.         @Override  
  6.         public void onRemoval(RemovalNotification<Integer, Integer> notification) {  
  7.             String tips = String.format("key=%s,value=%s,reason=%s", notification.getKey(), notification.getValue(), notification.getCause());  
  8.             System.out.println(tips);  
  9.   
  10.             try {  
  11.                 // 模擬耗時  
  12.                 Thread.sleep(2000);  
  13.             } catch (InterruptedException e) {  
  14.                 e.printStackTrace();  
  15.             }  
  16.   
  17.             System.out.println("process over.");  
  18.         }  
  19.     }  
  20.   
  21.     public static void main(String[] args) {  
  22.   
  23.         // 創建一個帶有RemovalListener監聽的緩存  
  24.         final Cache<Integer, Integer> cache = CacheBuilder.newBuilder().removalListener(new MyRemovalListener()).build();  
  25.         cache.put(11);  
  26.         cache.put(22);  
  27.   
  28.         new Thread(new Runnable() {  
  29.             @Override  
  30.             public void run() {  
  31.                 System.out.println("thread1...trigger RemovalListener begin.");  
  32.                 cache.invalidate(1);  
  33.                 System.out.println("thread1...trigger RemovalListener over.");  
  34.             }  
  35.         }).start();  
  36.   
  37.         new Thread(new Runnable() {  
  38.             @Override  
  39.             public void run() {  
  40.                 System.out.println("thread2...trigger RemovalListener begin.");  
  41.                 cache.invalidate(2);  
  42.                 System.out.println("thread2...trigger RemovalListener over.");  
  43.             }  
  44.         }).start();  
  45.     }  
  46.   
  47. }  



3、監聽器中拋出的任何異常,在被記錄到日誌後,會被guava丟棄,不會導致監聽器不可用。下面這段代碼可以看到:監聽器中拋出的異常只是被記錄了(打印到了控制檯),並沒有導致JVM退出,之後緩存被移除一樣可以再次觸發。
[java] view plain copy
  1. public class Main {  
  2.   
  3.     // 創建一個監聽器  
  4.     private static class MyRemovalListener implements RemovalListener<Integer, Integer> {  
  5.         @Override  
  6.         public void onRemoval(RemovalNotification<Integer, Integer> notification) {  
  7.             String tips = String.format("key=%s,value=%s,reason=%s", notification.getKey(), notification.getValue(), notification.getCause());  
  8.             System.out.println(tips);  
  9.   
  10.             throw new RuntimeException();  
  11.         }  
  12.     }  
  13.   
  14.     public static void main(String[] args) {  
  15.   
  16.         // 創建一個帶有RemovalListener監聽的緩存  
  17.         final Cache<Integer, Integer> cache = CacheBuilder.newBuilder().removalListener(new MyRemovalListener()).build();  
  18.         cache.put(11);  
  19.         cache.put(22);  
  20.   
  21.         cache.invalidate(1);  
  22.         cache.invalidate(2);  
  23.     }  
  24.   
  25. }  
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章