前言:
設計到緩存事件監聽機制,一般應用場景是某一個任務下達后服務端在對應時間后進行后續操作,類似訂單過期、消警等使用場景。
問題:
最近公司的一個業務是,當推送觸發一條告警時,如果當前告警是誤報上來的,則需要手動去消除警報,此業務相當於是消除當前警報,不會影響其他告警。但由於業務上種種原因,最終落實到我們服務端去操作此邏輯,主要邏輯是,告警上報后,確認消除警報后,關閉當前告警配置,待固定時間后服務端再后端自動開啟相關配置
分析:
1、首先,我們能想到的是事件監聽機制,總不能讓當前線程休眠對應3分鍾后再往下執行任務,這顯然不行。
2、其次,我們服務端是作為服務通訊層,不涉及任何中間件,所以redis的”KeyExpirationEventMessageListener“也無法使用
3、最后,我們鎖定了谷歌的 Guava Cache 緩存數據被移除后的監聽器 RemovalListener。依賴包
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>18.0</version> </dependency>
一、guava RemovalListener 邏輯代碼:
1、緩存配置類
package com.ghh.websocketRecive.config; import com.google.common.cache.*; import com.google.common.util.concurrent.ListenableFuture; import org.springframework.util.StringUtils; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; /** * @program: mainproject * @@Description: 監聽器 * @Author: GHH * @Date: 2021-11-10 15:38 **/ public class MyRemovalListener extends Thread{ // 存放自己的業務邏輯值 public static Map<String,Long> map = new HashMap<>(); // 線程啟用狀態 public static String isStart = "0"; public static RemovalListener<String, String> myRemovalListener = new RemovalListener<String, String>(){ @Override public void onRemoval(RemovalNotification<String, String> 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 RemovalCause cause = notification.getCause(); if ("EXPLICIT".equals(cause.toString())){ System.out.println("開啟行為分析全局配置"); } System.out.printf("Remove %s in cacheConnection", notification.getKey()); } }; // 單例模式獲取緩存 private static Cache<String, String> cacheConnection; public static synchronized Cache<String, String> getWebSocketClientHandler() { if (cacheConnection == null) { cacheConnection = CacheBuilder.newBuilder() //設置cache中的數據在600秒沒有被讀寫將自動刪除 .expireAfterWrite(600, TimeUnit.SECONDS) //設置cache的初始大小為20000,要合理設置該值 .initialCapacity(20000) //設置並發數為5,即同一時間最多只能有5個線程往cache執行寫入操作 .concurrencyLevel(100) //設置監聽,當出現自動刪除時的回調 .removalListener(myRemovalListener) //構建cache實例 .build(); } return cacheConnection; } // 線程獲取過期的數據並執行清除操作 @Override public void run() { isStart = "1"; while (true){ try { // 當前時間 long l = System.currentTimeMillis(); for (String key : map.keySet()) { Long aLong = map.get(key); Cache<String, String> webSocketClientHandler = MyRemovalListener.getWebSocketClientHandler(); // 如果超過10則清除 if ((l-aLong)>10000 || StringUtils.isEmpty(webSocketClientHandler.getIfPresent(key))){ // 主動清除緩存數據 webSocketClientHandler.invalidate(key); map.remove(key); } } }catch (Exception e){ System.out.println("循環失敗"); isStart = "0"; } } } }
提示:關於上面為什么要用線程去獲取map中數據再下面說明
2、調用類
// 創建一個帶有RemovalListener監聽的緩存 Cache<String, String> cache = MyRemovalListener.getWebSocketClientHandler(); System.out.println("獲取行為分析全局參數"); cache.put(s, "獲取到的行為分析全局參數"); System.out.println("消警操作"); Map<String, Long> map = MyRemovalListener.map; map.put(s,System.currentTimeMillis()); if ("0".equals(MyRemovalListener.isStart)) { MyRemovalListener removalListener = new MyRemovalListener(); removalListener.start(); }
描述:
1、根據上面的RemovalListener配置我們可以看到,可以配置對應的緩存大小,線程數、以及多少秒失效等,根據業務上自定義一個map存放對應key,value,key是業務唯一值,value為當前時間戳。
2、業務上先獲取到緩存對象,然后將后續緩存過期的業務所需要的值存放到緩存中
3、業務處理時,根據線程獲取map中的數據,判斷時間戳是否過期,如果過期則主動清除當前緩存中的數據,並觸發 RemovalListener的 onRemoval的方法, RemovalNotification這個對象里面有緩存中存放的key和value值以及當前數據清除的原因
答疑:
RemovalListener會失效,也不算是會失效,主要是因為 guava內機制如此,guava中
1、使用CacheBuilder構建的Cahe不會“自動”執行清理數據,或者在數據過期后,立即執行清除操作。相反,它在寫操作期間或偶爾讀操作期間執行少量維護(如果寫很少)。
2、這樣做的原因如下:
如果我們想要連續地執行緩存維護,我們需要創建一個線程,它的操作將與共享鎖的用戶操作發生競爭。此外,一些環境限制了線程的創建,這會使CacheBuilder在該環境中不可用。
簡單來說,GuavaCache 並不保證在過期時間到了之后立刻刪除該 <Key,Value>,如果你此時去訪問了這個 Key,它會檢測是不是已經過期,過期就刪除它,所以過期時間到了之后你去訪問這個 Key 會顯示這個 Key 已經被刪除,但是如果你不做任何操作,那么在 過期時間到了之后也許這個<Key,Value> 還在內存中。所以我們業務中目前只能使用線程去判斷管理當前緩存數據
二、Redis的 KeyExpirationEventMessageListener
其實這個比較好理解,我們使用redis的時候設置對應的過期時間即可,創建一個類繼承 KeyExpirationEventMessageListener,重寫里面的 onMessage方法,在里面做對應的業務邏輯即可,入參的Message方法為業務上redis存放的value值。