起因

想要完成一个个人轻量级微服务框架,负载均衡和接口安全都需要一个这样的工具来统计访问频率,那么就选择了一种比较传统的方式来实现,其他博客中有提供一些方式,但设计较为简单,不能满足我的需求,所以再起炉灶

滑动窗口

概念上非常简单,就是给定一个数据结构列表,例如20个窗口,步长固定为1,长度为10,当index为11时,该窗口范围为[1-11]

代码实现

首先思考一下算法,我们大概需要几个参数【窗口个数、每个时间片时长、队列长度、每个时间片内允许授权个数(可选)】

/*** 时间窗滑块* since 2019/12/8** @author eddie*/
public class TimeWindowSliding {/** 队列的总长度  */private volatile int timeSliceSize;/** 每个时间片的时长,以毫秒为单位 */private volatile int timeMillisPerSlice;/** 当前所使用的时间片位置 */private AtomicInteger cursor = new AtomicInteger(0);/** 在一个完整窗口期内允许通过的最大阈值 */private int threshold;/** 窗口个数 */private int windowSize;/** 最小每个时间片的时长,以毫秒为单位 */private static final int MIN_TIME_MILLIS_PER_SLICE = 50;/** 最小窗口数量 */private static final int DEFAULT_WINDOW_SIZE = 5;/** 数据存储 */private TimeWindowSlidingDataSource timeWindowSlidingDataSource;public TimeWindowSliding(TimeWindowSlidingDataSource timeWindowSlidingDataSource, int windowSize, int timeMillisPerSlice, int threshold){this.timeWindowSlidingDataSource = timeWindowSlidingDataSource;this.timeMillisPerSlice = timeMillisPerSlice;this.threshold = threshold;/* 低于一定窗口个数会丢失精准度 */this.windowSize = Math.max(windowSize, DEFAULT_WINDOW_SIZE);/* 保证每个时间窗至少有2个窗口存储 不会重叠 */this.timeSliceSize = this.windowSize * 2 + 1;/* 可以忽略这个操作 数据存储结构中定义的生命周期函数 如果接口有实现会调用 没有实现走默认实现直接return */timeWindowSlidingDataSource.initTimeSlices();/* 初始化参数校验 */this.verifier();}

初始化函数中包含了一个TimeWindowSlidingDataSource,看下这个接口的定义


/*** 时间分配数据源* since 2019/12/8** @author eddie*/
public interface TimeWindowSlidingDataSource {/*** 记录通过记录* @param timeSlices    时间分片* @param recordKey     记录参数* @throws TimeWindowSlidingDataSourceException     时间分片数据源操作异常*/void allocAdoptRecord(int timeSlices, String recordKey) throws TimeWindowSlidingDataSourceException;/*** 获取<recordKey>通过次数* @param timeSlices    时间分片* @param recordKey     记录参数* @return* @throws TimeWindowSlidingDataSourceException*/int getAllocAdoptRecordTimes(int timeSlices, String recordKey) throws TimeWindowSlidingDataSourceException;/*** 将fromIndex~toIndex之间的时间片计数清零* @param fromIndex     起始索引* @param toIndex       结束索引* @param totalLength   窗口数量* @throws TimeWindowSlidingDataSourceException     时间分片数据源操作异常*/void clearBetween(int fromIndex, int toIndex, int totalLength) throws TimeWindowSlidingDataSourceException;/*** 将index时间片计数清零* @param index         索引* @throws TimeWindowSlidingDataSourceException*/void clearSingle(int index) throws TimeWindowSlidingDataSourceException;/*** 根据需求 可不使用该函数 该函数会在初始化阶段调用一次* @throws TimeWindowSlidingDataSourceException     时间分片数据源操作异常*/default void initTimeSlices() throws TimeWindowSlidingDataSourceException {}static TimeWindowSlidingDataSource defaultDataSource() {return new TimeWindowSlidingDataSource() {private Map<String, Map<String,Integer>> timeWindowSlidingMap = new ConcurrentHashMap<>(16);@Overridepublic void allocAdoptRecord(int timeSlices, String recordKey) throws TimeWindowSlidingDataSourceException {Map<String, Integer> timeSlicesMap = timeWindowSlidingMap.get(String.valueOf(timeSlices));if (Objects.isNull(timeSlicesMap)){timeSlicesMap = new ConcurrentHashMap<>(16);timeWindowSlidingMap.put(String.valueOf(timeSlices), timeSlicesMap);}Integer adoptTimes = timeSlicesMap.get(recordKey);int nextTimes = adoptTimes == null ? 0 : adoptTimes;timeSlicesMap.put(recordKey, ++ nextTimes);}@Overridepublic int getAllocAdoptRecordTimes(int timeSlices, String recordKey) throws TimeWindowSlidingDataSourceException {Map<String, Integer> timeSlicesMap = timeWindowSlidingMap.get(String.valueOf(timeSlices));if (Objects.isNull(timeSlicesMap)){return 0;}Integer recordTimes = timeSlicesMap.get(recordKey);return Objects.isNull(recordTimes) ? 0 : recordTimes;}@Overridepublic void clearBetween(int fromIndex, int toIndex, int totalLength) throws TimeWindowSlidingDataSourceException {if (fromIndex >= toIndex){toIndex += totalLength;}while (fromIndex <= toIndex) {Map<String, Integer> timeWindowSlidingScopeMap = timeWindowSlidingMap.get(String.valueOf(fromIndex));if (!Objects.isNull(timeWindowSlidingScopeMap) && timeWindowSlidingScopeMap.size() > 0){timeWindowSlidingScopeMap.clear();}fromIndex++;}}@Overridepublic void clearSingle(int index) throws TimeWindowSlidingDataSourceException {Map<String, Integer> timeWindowSlidingScopeMap = timeWindowSlidingMap.get(String.valueOf(index));if (!Objects.isNull(timeWindowSlidingScopeMap) && timeWindowSlidingScopeMap.size() > 0){timeWindowSlidingScopeMap.clear();}}};}
}

这个类其实就是一个约定,默认提供了一个基于HashMap的实现,我需要统计单用户访问接口频率,所以,数据结构采用HashMap,也就是说相当于有一个List<Map<String,Integer>>的结构,但因为习惯问题,我才用了Map<String, Map<String,Integer>> 的结构。

但这个无所谓,因为出于场景考虑,大概率是要支持分布式的,那就需要引入redis,可这是一个工具包,因为这个工具就引入一个redis的包不太划算,所以定义了这么一个约定接口,不管你用什么存储,memchcheredismangodb,只要实现了这几个方法,传入工具中就能完成对应的功能,算是一个变形的策略模式

这些参数都初始化好之后,看下算法的部分,提供三个接口:

    /*** 判断是否允许进行访问,未超过阈值的话才会对某个时间片+1*/public boolean allowLimitTimes(String key) {int index = locationIndex();int sum = 0;// cursor不等于index,将cursor设置为indexint oldCursor = cursor.getAndSet(index);if (oldCursor != index) {// 清零,访问量不大时会有时间片跳跃的情况clearBetween(oldCursor, index);}for (int i = 1; i < timeSliceSize; i++) {sum += timeWindowSlidingDataSource.getAllocAdoptRecordTimes(i, key);}// 阈值判断if (sum <= threshold) {// 未超过阈值才+1this.timeWindowSlidingDataSource.allocAdoptRecord(index, key);return true;}return false;}/*** 返回平均每秒访问次数*/public int allowNotLimitPerMin(String key) {int index = locationIndex();int sum = 0;int nextIndex = index + 1;this.timeWindowSlidingDataSource.clearSingle(nextIndex);int from = index, to  = index;if (index < windowSize) {from += windowSize + 1;to += 2 * windowSize;}else {from = index - windowSize + 1;}while (from <= to){int targetIndex = from;if (from >= timeSliceSize) {targetIndex = from - 2 * windowSize;}sum += timeWindowSlidingDataSource.getAllocAdoptRecordTimes(targetIndex, key);from ++;}this.timeWindowSlidingDataSource.allocAdoptRecord(index, key);return (sum + 1) / windowSize;}/*** 返回每秒访问次数*/public int allowNotLimit(String key) {int index = locationIndex();int sum = 0;// cursor不等于index,将cursor设置为indexint oldCursor = cursor.getAndSet(index);if (oldCursor != index) {// 清零,访问量不大时会有时间片跳跃的情况clearBetween(oldCursor, index);}for (int i = 0; i <= timeSliceSize; i++) {sum += timeWindowSlidingDataSource.getAllocAdoptRecordTimes(i, key);}this.timeWindowSlidingDataSource.allocAdoptRecord(index, key);return sum + 1;}/*** <p>将fromIndex~toIndex之间的时间片计数都清零* <p>极端情况下,当循环队列已经走了超过1个timeSliceSize以上,这里的清零并不能如期望的进行*/private void clearBetween(int fromIndex, int toIndex) {this.timeWindowSlidingDataSource.clearBetween(fromIndex, toIndex, timeSliceSize);}private int locationIndex() {long time = System.currentTimeMillis();return (int) ((time / timeMillisPerSlice) % timeSliceSize);}

演示结果就不给大家展示了,刚把一大堆log去掉,如果有更好的办法欢迎给我留言,或者github协作,该项目github地址,对应实现在com.el.common.time.sliding包下

另外
感谢博主: tianyaleixiaowu提供的基础思路

【小工具】滑动时间窗统计单位时间内访问频率相关推荐

  1. EXCEL中的滑动时间窗使用

    之前参加一个测试的时候,遇到一个试题,说怎么样计算在过去24小时时间内的销售数据.注意这里是过去的24小时,不是过去的一天也不是昨天.这里就是一个滑动时间窗,然后对滑动时间窗内部的数据进行一个聚合运算 ...

  2. Sentinel滑动时间窗限流算法

    Sentinel系列文章 Sentinel熔断限流器工作原理 Sentinel云原生K8S部署实战 Sentinel核心源码解析 时间窗限流算法 如图 10-20这个时间窗内请求数量是60小于阈值10 ...

  3. Sentinel滑动时间窗限流算法原理及源码解析(上)

    文章目录 时间窗限流算法 滑动时间窗口 滑动时间窗口算法改进 滑动时间窗口源码解析 时间窗限流算法 10t到16t 10个请求 16t-20t 50个请求 20t-26t 60个请求 26t到30t ...

  4. Sentinel滑动时间窗限流算法原理及源码解析(中)

    文章目录 MetricBucket MetricEvent数据统计的维度 WindowWrap样本窗口实例 范型T为MetricBucket windowLengthInMs 样本窗口长度 windo ...

  5. SpringBoot+redis实现用户或者ip恶意单位时间内访问

    中心思想就是把该用户首次访问的时间和访问个数放到reds中用户每访问一次加1,先对比访问的次数是否超出,然后对比访问的时间是超出所设置的时间1.实现一个过滤器接口 package com.exam.i ...

  6. python构造referer_Python爬虫小偏方:修改referer绕开登录和访问频率限制

    看官们在写爬虫程序时应该都会遇到如下问题: 你的爬虫程序开发时能正常抓取网页,但是正式大量抓取时,抓取的网站总是返回403或者500等: 你抓取的网站需要登录,要花大量时间去研究网站登录流程. 遇到问 ...

  7. Sentinel滑动时间窗限流算法原理及源码解析(下)

    文章目录 对统计数据如何使用 获取之前统计好的数据 对统计数据如何使用 流控快速失败 获取之前统计好的数据

  8. 来看一看滑动时间窗格

    有句话叫什么来着?心似平原走马,易放难收!有空的时候抽点时间看看书什么的也没什么,不过,如果一周两周一直不看书,或者不学习,估计再想拿起书本就有点困难了. 当然,拖得越久,就变得更懒~,恶性循环就是这 ...

  9. sentinel 滑动时间窗口算法

    sentinel 滑动时间窗口的算法 为什么要用滑动时间窗口算法? 互联网中最常见的一个问题:限流,即在一段时间内,限制访问某个接口的请求数. 要实现限流(或熔断降级),方法有很多,最基本的如计数器法 ...

最新文章

  1. (超级详细)jit的介绍和用法
  2. SpringSecurity过滤器链汇总
  3. [网络安全提高篇] 一〇六.SQL注入之手工注入和SQLMAP入门案例详解
  4. 下载 infoq 网站视频
  5. 学习android 画板源代码,Android实现画画板案例
  6. 堆垛机器人编程技巧_机器人智能堆垛的控制方法与流程
  7. php+psr4和自动加载,php自动加载规范 PSR4 (Thinkphp)
  8. jquery 自定义插件!
  9. mysql建立聚族索引语句,MySQL学习教程之聚簇索引
  10. android studio 中禁用一个插件功能
  11. python书在线阅读_这7本O’Reilly推出的免费Python电子书,够你看了
  12. 计算机网络按拓扑结构可以划分为,计算机网络按拓扑结构分为哪些
  13. java 获取拼音_Java获取汉字对应的拼音(全拼或首字母)
  14. 用matlab产生chu序列和frank序列
  15. selenium是python_selenium+Python(事件)
  16. 解决sysman.mgmt_task_qtable ORA-600 kdsgrp1错误
  17. 如何编辑图片合成图片?让我们来看看这些合成方法
  18. oracle中sql关键字,Oracle常用的sql语句
  19. 独立性检验的基本思想和初步应用
  20. “天才少年” 稚晖君被曝离职华为,或投身机器人领域

热门文章

  1. 免费的瑞星2008杀毒软件!
  2. goroutine中使用recover,解决协程中出现panic,导致程序崩溃的问题。recover panic 协程的错误处理
  3. 想下载《假如蜗牛有爱情》抢先版?信了你的邪会中木马!
  4. 区块链3.0 ada Cardano卡尔达诺如何获得一致好评?
  5. @ansible自动化运维详解(总述)
  6. 论文研读 —— 1. Modeling Motion Blur in Computer Generated Images
  7. 用匠心创造可期未来!与广州流辰信息科技一起携手创佳绩!
  8. Java 设计模式最佳实践:二、创建型模式
  9. 学习日记-----函数
  10. 江苏实现自然村通宽带 农村宽带网速可达4M