背景
在物联网场景中,经常会涉及到设备在线和离线监测,在线监测相对比较容易实现,监听设备消息或心跳,更新设备状态,但是一旦设备离线了,是不会向应用层发送离线消息的,这时候就需要解决如何判定设备离线的问题。
方案
关于离线的判定,任何方案都是基于心跳周期或是keepalive的,目前通常的方案有两种:
- 在接收到设备心跳时,启动一个延时keepalive时长的延时任务,并维护一个任务列表,任务执行时去检查设备是否离线,若期间设备上发了消息,那么更新该任务。这个方案由于对每个设备都要启动一个线程去执行检测任务,相当占用资源。
- 启动一个线程,去扫描所有设备列表,检查该设备是否离线并更新其状态。这个方案在设备数量很大的情况下,每一次扫描都会耗费大量的时间。
偶然间,在网上看到了一位同学的方案,是使用环形队列来实现低消耗、低延时的离线监测,原文在这里:如何高效的触发设备离线
初见很欣喜,但仔细阅读后,发现该同学的业务场景是所有设备30秒判断离线,这和实际的应用场景还是有点出入,仔细思考后,借鉴该方案设计了一个满足keepalive时间不等的场景的方案。
设计
方案的大致思路如下图所示,好处是只起了一个线程做监测,不用遍历整个数组,其次一旦设备离线了,基本1s内就能判定出来
实现
CircleQueue实现类
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author : zhiying
* @Date : 2022-11-22 15:17
* @Desc : 环形队列 - 设备离线判定
* 1、预设一个长度为10000的数组(按实际业务定义长度)
* 2、每个数组存放一个Set集合
* 3、维护一个游标cur,从0到9999递增,到达9999时,重置为0(启动一个线程执行)
* 4、维护一个map,记录所有设备ID存放的数组位置,方便查找
* 5、监听到设备心跳时,先将原来的数据从指定位置的集合中删除,通过计算当前游标位置和keepAlive寻找合适的位置将设备ID放入
* 6、当游标指向某个位置a时,a位置的集合中的所有设备全部判定为离线,并清空该位置的集合
**/
public class CircleQueue<T> {
//线程安全锁
Lock lock = new ReentrantLock();
//初始环形队列大小
private int capacity = 10000;
//当前环形队列所在节点
private volatile int currentIndex = 0;
//数据所在节点
private Map<T,Integer> dataIndex = new HashMap<>();
//环形队列
private Set<T>[] array;
public CircleQueue(){
array = new HashSet[capacity];
}
public CircleQueue(int capacity){
this.capacity = capacity;
array = new HashSet[capacity];
}
/**
* 向环形队列中添加元素
* @param t
* @param offset 偏移量,基于游标
*/
public void add(T t, int offset){
int index = currentIndex + offset;
if(index >= capacity){
index = index - capacity;
}
try {
lock.lock();
//判断数据是否存在
if(dataIndex.containsKey(t)){
Set<T> old = array[dataIndex.get(t)];
old.remove(t);
}
//获取当前节点的队列
Set<T> set = array[index];
if(null == set){
set = new HashSet<>();
array[index] = set;
}
set.add(t);
//更新新的节点位置
dataIndex.put(t,index);
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
/**
* 下移一格,到9999重新置为0
*/
public void next(){
int cur = currentIndex + 1;
if(cur >= capacity){
cur = cur - capacity;
}
currentIndex = cur;
System.out.println("当前游标位置:" + currentIndex);
}
/**
* 获取当前游标指向的元素集合
* @return
*/
public Set<T> getAndDeleteData(){
Set<T> set = null;
try {
lock.lock();
set = array[currentIndex];
return set;
}finally {
// 将集合中所有的元素移除
array[currentIndex] = new HashSet<>();
if(set != null && set.size()>0){
set.forEach(t -> {
dataIndex.remove(t);
});
}
lock.unlock();
}
}
public int getIndex(T t){
if(dataIndex.containsKey(t)){
return dataIndex.get(t);
}
return -1;
}
}
测试类
import com.google.common.base.Joiner;
import io.renren.modules.iot.utils.CircleQueue;
import org.junit.jupiter.api.Test;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class DeviceOnlineCheck {
ScheduledExecutorService service = Executors.newScheduledThreadPool(3);
@Test
public void circleTest(){
CircleQueue<String> circleQueue = new CircleQueue<>();
for (int i=0;i<1000;i++){
String uuid = String.valueOf(i+1);
int offset = (int) Math.round(Math.random()*10);
circleQueue.add(uuid, offset);
}
checkTimeout(circleQueue);
insertDataRandom(circleQueue);
try {
Thread.sleep(600000);
}catch (Exception e){
e.printStackTrace();
}
}
private void checkTimeout(CircleQueue<String> circleQueue){
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
Set<String> set = circleQueue.getAndDeleteData();
if(set == null || set.isEmpty()) {
System.out.println("本次没有设备离线");
}else{
System.out.println("这些设备离线啦:" + Joiner.on(",").join(set));
}
circleQueue.next();
}
},2,1, TimeUnit.SECONDS);
}
private void insertDataRandom(CircleQueue<String> circleQueue){
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
String deviceId = String.valueOf(Math.round(Math.random()*100000));
int offset = (int) Math.round(Math.random()*10);
circleQueue.add(deviceId, offset);
System.out.println("插入设备["+deviceId+"], " + offset + "秒后离线");
}
},3,3, TimeUnit.SECONDS);
}
}
废话
这个方案也是刚构思出来,还要经过业务的检验,定期更新