滑动时间窗口实现简单接口限流

  • 本文目的

在学习阿里开源框架sentinel后,为加深对滑动时间窗口的理解,故自己实现简单接口限流。

  • Sentinel

Sentinel 是面向分布式服务架构的高可用流量防护组件,主要以流量为切入点,从限流、流量整形、熔断降级、系统负载保护、热点防护等多个维度来帮助开发者保障微服务的稳定性。

Sentinel的一切功能都是以流量统计为基础。基于滑动时间窗口实现秒级的流量统计。

  • 滑动时间窗口

Sentinel以1秒为时间宽度,将1秒平均分隔成指定数量的时间窗口,任意的时间对应唯一的一个时间窗口,每个时间窗口有一个开始时间。在每个时间窗口内统计当前时间窗口的流量。因为时间窗口比较小的,所以避免了大量的统计并发冲突,提高统计性能。当要统计1秒内的流量时,只需要统计当前时间窗口前的n(1秒除以时间窗口的宽度)个时间窗口的数据。

  • 简单实现

1. 创建简单的springboot工程,添加测试接口

@RestController
public class TestController {

    @GetMapping("/hello")
    public String hello() {
        return "hello world";
    }
}

2. 时间窗口类 Window

@Data
public class Window {

    private String url;

    private long time;

    private AtomicInteger count;

}

3. 时间窗口统计类 Statistics

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReferenceArray;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Statistics {

    // 时间窗口宽度
    private long WINDOW_PERIOD = 500l;



    // 统计时长,单位:秒
    private long STATICTIS_PERIOD = 1;

    // 时间窗口数组,保存所有时间窗口,数组长度由统计时长与时间窗口宽度共同决定, 需要能整除
    private AtomicReferenceArray<Window> array = new AtomicReferenceArray(STATICTIS_PERIOD * 1000 / WINDOW_PERIOD);

    private Lock lock = new ReentrantLock();

    /**
     * 根据当前时间获取时间窗口
    */
    public Window getWindow(long time) {
        // 1、根据当前时间,算出该时间的timeId,timeId就是在整个时间轴的位置
        long timeId = time / WINDOW_PERIOD;
        // 2、据timeId算出当前时间窗口在采样窗口区间中的索引idx
        int idx = (int)(timeId % array.length());

        // 3、根据当前时间算出当前窗口应该对应的窗口开始时间time,以毫秒为单位
        time = time - time % WINDOW_PERIOD;

        // 循环判断直到获取到一个当前时间窗口
        while(true) {

            // 获取时间窗口数组中idx位置的时间窗口
            Window oldWindow = array.get(idx);
            // 如果时间窗口不存在,则新建一个时间窗口并添加到时间窗口数组中
            if (oldWindow == null) {
                Window window = new Window();
                window.setTime(time);
                window.setCount(new AtomicInteger(0));
                boolean newWindowFlag = array.compareAndSet(idx, null, window);
                if (newWindowFlag) {
                    return window;
                }
                oldWindow = array.get(idx);
            }
            // 时间窗口的时间等于当前时间,则直接返回该时间窗口
            if (time == oldWindow.getTime()) {
                return oldWindow;
            // 当前时间大于该时间窗口,则表明该时间窗口已过期,重置时间窗口
            } else if (time > oldWindow.getTime()) {
                if (lock.tryLock()) {
                    try {
                        oldWindow.setTime(time);
                        oldWindow.getCount().set(0);
                        return oldWindow;
                    } finally {
                        lock.unlock();
                    }
                }
            }
        }

    }

    // 统计指定时间内的流量数据
    public int count(long time, long period) {
        int count = 0;
        for(int i = 0; i < array.length(); i++) {
            Window window = array.get(i);
            if (null != window && time - period <= window.getTime()) {
                count += window.getCount().get();
            }
        }
        return count;
    }
}

4. 限流过滤器

import com.example.demo.timewindow.Statistics;
import com.example.demo.timewindow.Window;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

public class CurrentLimitingFilter implements Filter {

    Statistics statistics = new Statistics();

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        long time = System.currentTimeMillis();
        int count = statistics.count(time, 1000);
        if (count <= 15) {
            Window window = statistics.getWindow(time);
            window.getCount().getAndIncrement();
            System.out.println(time / 1000 + "-" + (time % 1000) + "requst url : " + request.getRequestURI() + "-" + count);
        } else {
            System.out.println(time / 1000 + "-" + (time % 1000) + "request url blocked" + "-" + count);
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }
}

5. 配置过滤器

import com.example.demo.filter.CurrentLimitingFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean registerAuthFilter() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new CurrentLimitingFilter());
        registration.addUrlPatterns("/*");
        registration.setOrder(1);  //值越小,Filter越靠前。
        return registration;
    }
}
  • 测试

使用jmeter模拟并发请求

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值