借鉴环形队列实现的设备离线监测

背景

在物联网场景中,经常会涉及到设备在线和离线监测,在线监测相对比较容易实现,监听设备消息或心跳,更新设备状态,但是一旦设备离线了,是不会向应用层发送离线消息的,这时候就需要解决如何判定设备离线的问题。

方案

关于离线的判定,任何方案都是基于心跳周期或是keepalive的,目前通常的方案有两种:

  1. 在接收到设备心跳时,启动一个延时keepalive时长的延时任务,并维护一个任务列表,任务执行时去检查设备是否离线,若期间设备上发了消息,那么更新该任务。这个方案由于对每个设备都要启动一个线程去执行检测任务,相当占用资源。
  2. 启动一个线程,去扫描所有设备列表,检查该设备是否离线并更新其状态。这个方案在设备数量很大的情况下,每一次扫描都会耗费大量的时间。

偶然间,在网上看到了一位同学的方案,是使用环形队列来实现低消耗、低延时的离线监测,原文在这里:如何高效的触发设备离线

初见很欣喜,但仔细阅读后,发现该同学的业务场景是所有设备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);
    }
}

废话

这个方案也是刚构思出来,还要经过业务的检验,定期更新

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值