缓存事件过期监听机制
前言:
设计到缓存事件监听机制,一般应用场景是某一个任务下达后服务端在对应时间后进行后续操作,类似订单过期、消警等使用场景。
问题:
最近公司的一个业务是,当推送触发一条告警时,如果当前告警是误报上来的,则需要手动去消除警报,此业务相当于是消除当前警报,不会影响其他告警。但由于业务上种种原因,最终落实到我们服务端去操作此逻辑,主要逻辑是,告警上报后,确认消除警报后,关闭当前告警配置,待固定时间后服务端再后端自动开启相关配置
分析:
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值。