- 本文目的
在学习阿里开源框架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模拟并发请求