180621-一个简单的时间窗口设计与实现

logo

如何设计一个计数的时间窗口

时间窗口,一般对于一些实时信息展现中用得比较多,好比维持一个五分钟的交易明细时间窗口,就须要记录当前时间,到五分钟以前的全部交易明细,而五分钟以前的数据,则丢掉java

一个简单的实现就是用一个队列来作,新的数据在对头添加;同时起一个线程,不断的询问队尾的数据是否过时,若是过时则丢掉git

另一中场景须要利用到这个时间窗口内的数据进行计算,如计算着五分钟交易中资金的流入流出总和,若是依然用上面的这种方式,会有什么问题?github

  • 若是时间窗口大,须要存储大量的明细数据
  • 咱们主要关心的只有资金流入流出;存这些明细数据得不偿失
  • 每次新增or删除过时数据时,实时计算流入流出消耗性能

针对这种特殊的场景,是否有什么取巧的实现方式呢?数组

<!-- more -->性能

I. 方案设计

1. 基于队列的轮询删除方式

将时间窗口分割成一个一个的时间片,每一个时间片中记录资金的流入流出总数,而后总的流入流出就是全部时间片的流入流出的和学习

新增数据:测试

  • 若未跨时间片,则更新队头的值
  • 若跨时间片,新增一个队列头

删除数据:this

  • 轮询任务,判断队列尾是否过时
  • 队尾过时,则删除队尾,此时若队头数据未加入计算,也须要加入计算

image.png

2. 基于队列的新增时删除方式

相比较前面的轮询方式,这个的应用场景为另一种,只有在新增数据时,确保数据的准确性便可,不须要轮询的任务去删除过时的数据线程

简单来讲,某些场景下(好比能确保数据不会断续的进来,即每一个时间片都至少有一个数据过来),此时但愿个人时间窗口数据是由新增的数据来驱动并更新设计

新增数据:

  • 未跨时间片,则更新队头值
  • 跨时间片,新塞入一个,并删除旧的数据

image.png

II. 基于数组的时间窗口实现

针对上面第二种,基于数组给出一个简单的实现,本篇主要是给出一个基础的时间窗口的设计与实现方式,固然也须要有进阶的case,好比上面的资金流入流出中,我须要分别计算5min,10min,30min,1h,3h,6h,12h,24h的时间窗口,该怎么来实现呢?可否用一个队列就知足全部的时间窗口的计算呢?关于这些留待下一篇给出

1. 时间轮计算器

前面用队列的方式比较好理解,这里为何用数组方式来实现?

  • 固定长度,避免频繁的新增和删除对象
  • 定位和更新数据方便

首先是须要实现一个时间轮计算器,根据传入的时间,获取须要删除的过时数据

@Data
public class TimeWheelCalculate {
    private static final long START = 0;

    private int period;
    private int length;

    /**
     * 划分的时间片个数
     */
    private int cellNum;

    private void check() {
        if (length % period != 0) {
            throw new IllegalArgumentException(
                    "length % period should be zero but not! now length: " + length + " period: " + period);
        }
    }

    public TimeWheelCalculate(int period, int length) {
        this.period = period;
        this.length = length;

        check();

        this.cellNum = length / period;
    }

    public int calculateIndex(long time) {
        return (int) ((time - START) % length / period);
    }

    /**
     * 获取全部过时的时间片索引
     *
     * @param lastInsertTime 上次更新时间轮的时间戳
     * @param nowInsertTime  本次更新时间轮的时间戳
     * @return
     */
    public List<Integer> getExpireIndexes(long lastInsertTime, long nowInsertTime) {
        if (nowInsertTime - lastInsertTime >= length) {
            // 已通过了一轮,过去的数据所有丢掉
            return null;
        }

        List<Integer> removeIndexList = new ArrayList<>();
        int lastIndex = calculateIndex(lastInsertTime);
        int nowIndex = calculateIndex(nowInsertTime);
        if (lastIndex == nowIndex) {
            // 尚未跨过这个时间片,则不须要删除过时数据
            return Collections.emptyList();
        } else if (lastIndex < nowIndex) {
            for (int tmp = lastIndex; tmp < nowIndex; tmp++) {
                removeIndexList.add(tmp);
            }
        } else {
            for (int tmp = lastIndex; tmp < cellNum; tmp++) {
                removeIndexList.add(tmp);
            }

            for (int tmp = 0; tmp < nowIndex; tmp++) {
                removeIndexList.add(tmp);
            }
        }
        return removeIndexList;
    }
}

这个计算器的实现比较简单,首先是指定时间窗口的长度(length),时间片(period),其主要提供两个方法

  • calculateIndex 根据当前时间,肯定过时的数据在数组的索引
  • getExpireIndexes 根据上次插入的时间,和当前插入的时间,计算两次插入时间之间,全部的过时数据索引

2. 时间轮容器

容器内保存的时间窗口下的数据,包括实时数据,和过去n个时间片的数组,其主要的核心就是在新增数据时,须要判断

  • 若跨时间片,则删除过时数据,更新实时数据,更新总数
  • 若未跨时间片,则直接更新实时数据便可
@Data
public class TimeWheelContainer {
    private TimeWheelCalculate calculate;

    /**
     * 历史时间片计数,每一个时间片对应其中的一个元素
     */
    private int[] counts;

    /**
     * 实时的时间片计数
     */
    private int realTimeCount;

    /**
     * 整个时间轮计数
     */
    private int timeWheelCount;

    private Long lastInsertTime;


    public TimeWheelContainer(TimeWheelCalculate calculate) {
        this.counts = new int[calculate.getCellNum()];
        this.calculate = calculate;
        this.realTimeCount = 0;
        this.timeWheelCount = 0;
        this.lastInsertTime = null;
    }

    public void add(long now, int amount) {
        if (lastInsertTime == null) {
            realTimeCount = amount;
            lastInsertTime = now;
            return;
        }


        List<Integer> removeIndex = calculate.getExpireIndexes(lastInsertTime, now);
        if (removeIndex == null) {
            // 二者时间间隔超过一轮,则清空计数
            realTimeCount = amount;
            lastInsertTime = now;
            timeWheelCount = 0;
            clear();
            return;
        }

        if (removeIndex.isEmpty()) {
            // 没有跨过期间片,则只更新实时计数
            realTimeCount += amount;
            lastInsertTime = now;
            return;
        }

        // 跨过了时间片,则须要在总数中删除过时的数据,并追加新的数据
        for (int index : removeIndex) {
            timeWheelCount -= counts[index];
            counts[index] = 0;
        }
        timeWheelCount += realTimeCount;
        counts[calculate.calculateIndex(lastInsertTime)] = realTimeCount;
        lastInsertTime = now;
        realTimeCount = amount;
    }

    private void clear() {
        for (int i = 0; i < counts.length; i++) {
            counts[i] = 0;
        }
    }
}

3. 测试

主要就是验证上面的实现有没有明显的问题,为何是明显的问题?

  • 深层次的bug在实际的使用中,更容易暴露
public class CountTimeWindow {

    public static void main(String[] args) {
        TimeWheelContainer timeWheelContainer = new TimeWheelContainer(new TimeWheelCalculate(2, 20));

        timeWheelContainer.add(0, 1);
        Assert.isTrue(timeWheelContainer.getTimeWheelCount() == 0, "first");

        timeWheelContainer.add(1, 1);
        Assert.isTrue(timeWheelContainer.getTimeWheelCount() == 0, "first");

        timeWheelContainer.add(2, 1);
        Assert.isTrue(timeWheelContainer.getTimeWheelCount() == 2, "second");
        Assert.isTrue(timeWheelContainer.getCounts()[0] == 2, "second");

        for (int i = 3; i < 20; i++) {
            timeWheelContainer.add(i, 1);
            System.out.println("add index: " + i + " count: " + timeWheelContainer.getTimeWheelCount());
        }

        // 恰好一轮

        timeWheelContainer.add(20, 3);
        Assert.isTrue(timeWheelContainer.getTimeWheelCount() == 20, "third");
        timeWheelContainer.add(21, 3);
        Assert.isTrue(timeWheelContainer.getTimeWheelCount() == 20, "third");


        // 减去过时的那个数据
        timeWheelContainer.add(22, 3);
        Assert.isTrue(timeWheelContainer.getTimeWheelCount() == 26 - 2, "fourth");
        Assert.isTrue(timeWheelContainer.getCounts()[0] == 6, "fourth");


        timeWheelContainer.add(26, 3);
        Assert.isTrue(timeWheelContainer.getTimeWheelCount() == 24 - 2 - 2 + 3, "fifth");
        System.out.println(Arrays.toString(timeWheelContainer.getCounts()));


        timeWheelContainer.add(43, 3);
        System.out.println(Arrays.toString(timeWheelContainer.getCounts()));
        Assert.isTrue(timeWheelContainer.getTimeWheelCount() == 6, "six");
    }
}

III. 其余

1. 一灰灰Bloghttps://liuyueyi.github.io/hexblog

一灰灰的我的博客,记录全部学习和工做中的博文,欢迎你们前去逛逛

2. 声明

尽信书则不如,已上内容,纯属一家之言,因我的能力有限,不免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

3. 扫描关注

blogInfoV2.png

相关文章
相关标签/搜索