最近在做一个功能,需要对并发量大的路径设置限流,防止因为并发太大,导致服务器宕机,老大给我在网上搜了限流神器之-Guava RateLimiter 实战,我进去看了下https://www.cnblogs.com/guxiong/p/11024289.html,我们项目是springboot项目,结合前面大佬的博客,我在自己项目上实现了限流,接下来分享下,我实现过程中遇到的问题
这个是我改了以后的代码,源代码请从上面链接去原博客那里看
首先按照大佬的方式,创建了四个类,限流实体类
public class RateLimit {
private String rateLimitId;
/**
* 限流路径,支持通配符,示例 /user/**
*/
private String limitPath;
/**
* 每秒限流频率
*/
private Integer permitsPerSecond;
/**
* 限流等待超时时间,单位s
*/
private Integer permitsTimeOut;
/**
* 排序
*/private Integer orderNo;
/**
* 最大线程数
*/
private Integer maxThread;
/**
* 创建时间
*/
private Date gmtCreate;
//get、set略
}
封装了一个限流信息类RateLimitInfo(我在创建这个类的时候,一直不明白RateLimitVo是个什么,以为还要再创建一个vo类,但是阅读后面的代码后,发现这个RateLimitVo就是RateLimit,这里替换成RateLimit后面的代码都是能执行的,可能博主当时有创建了一个vo类来区分什么东西吧,这里不太懂博主的意思,至少换成RateLimit类,后面执行是没问题的,而且博主的博客中也没出现RateLimitVo类)
/**
* @描述: 限流信息
*/
public class RateLimitInfo {
private RateLimiter rateLimiter;
private RateLimit rateLimit;
private long lastUpdateTime;
public RateLimitInfo(RateLimiter rateLimiter, RateLimit rateLimit, long lastUpdateTime) {
this.rateLimiter = rateLimiter;
this.rateLimit = rateLimit;
this.lastUpdateTime = lastUpdateTime;
}
//get、set略
}
定义限流策略RateLimitStrategist(这里,原博主是从数据库查询配置的限流路径和限流配置,因为我的项目只需要配置一个路径,所有将从数据库查询的代码注释,换成了在初始化时手动新建一个实体类,并且设置好超时时间,路径名,最大线程数,和每秒限流数)其实可以同步注释,线面的每5分钟同步一次数据库配置的限流配置,但是我没注释,准备以后换成数据库配置。
import com.google.common.util.concurrent.RateLimiter;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class RateLimitStrategist {
private PathMatcher pathMatcher = new AntPathMatcher();
private final Map<String, RateLimitInfo> limiterMap = new LinkedHashMap<>();
private final Map<String, Semaphore> threadMap = new LinkedHashMap<>();
public RateLimitStrategist(){
init();
}
/**
* 更新频率,意为后台配置路径后5分钟生效
*/
private static final long UPDATE_RATE = 1000*60*5;
private long lastUpdateTime = 0;
public void init() {
limiterMap.clear();
threadMap.clear();
RateLimit limit = new RateLimit(); // 因为只限流一个路径,没从数据库中查,初始化时,手动加入
limit.setLimitPath("/api/app/moyujiafei/abc");
limit.setMaxThread(10);
limit.setPermitsTimeOut(5);
limit.setPermitsPerSecond(10);
// List<RateLimit> rateLimitVos = rateLimitManager.findListForPriority(); //查询数据库中配置的路径信息,需要自己实现
List<RateLimit> rateLimitVos = new ArrayList<>();
rateLimitVos.add(limit);
if(!CollectionUtils.isNotEmpty(rateLimitVos)) {
return;
}
for (RateLimit rateLimit : rateLimitVos) {
RateLimiter rateLimiter = RateLimiter.create(rateLimit.getPermitsPerSecond());
limiterMap.put(rateLimit.getLimitPath(), new RateLimitInfo(rateLimiter, rateLimit, System.currentTimeMillis()));
threadMap.put(rateLimit.getLimitPath(), new Semaphore(rateLimit.getMaxThread(), true));
}
lastUpdateTime = System.currentTimeMillis();
}
public boolean tryAcquire(String requestUri) {
//目前设置5分钟更新一次
if(System.currentTimeMillis() - lastUpdateTime > UPDATE_RATE) {
synchronized (this) {
if(System.currentTimeMillis() - lastUpdateTime > UPDATE_RATE) {
init();
}
}
}
for (Map.Entry<String, RateLimitInfo> entry : limiterMap.entrySet()) {
if(!pathMatcher.match(entry.getKey(), requestUri)) {
continue;
}
RateLimitInfo rateLimitInfo = entry.getValue();
RateLimit rateLimitVo = rateLimitInfo.getRateLimit();
RateLimiter rateLimiter = rateLimitInfo.getRateLimiter();
boolean concurrentFlag = rateLimiter.tryAcquire(1, rateLimitVo.getPermitsTimeOut(), TimeUnit.SECONDS);
if(!concurrentFlag) { //验证失败,直接返回
return concurrentFlag;
} else {
if(threadMap.get(requestUri).availablePermits() != 0) { //当前路径对应剩余可执行线程数不为0
try {
//申请可执行线程
threadMap.get(requestUri).acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
return true;
} else {
return false;
}
}
}
return true;
}
public void setLastUpdateTime(long lastUpdateTime) {
this.lastUpdateTime = lastUpdateTime;
}
/**
* 释放路径对应的线程数
* @param requestURI
*/
public void releaseSemaphore(String requestURI) {
if(null != threadMap.get(requestURI)) {
threadMap.get(requestURI).release();
}
}
}
定义拦截器RateLimitFilter,在拦截器中调用限流策略,这里我将创建RateLimitStrategist的方式改了,原文是通过实例工厂来实现的,博客并未给出工厂的代码,看起来像是通过反射获得的,这里为了方便使用单例模式直接new出对象(因为使用的springboot,所以配置filter,我是直接通过注解的方式来的,有关xml配置可以去看原博主的博客,或者自己搜索)同时使用 @WebFilter 和 @Component,Spring Boot将会自动注册过滤器,不管写成什么拦截地址,过滤器注册的地址都是 “/*”(这个有点坑,目前不知道怎么解决)
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @描述: 限流过滤器,配置后生效
*/
@Component
@Order //过滤器加载的顺序 默认Integer.MAXVALUE
@WebFilter(urlPatterns = "/api/app/moyujiafei/*",filterName = "wholeFilter")
public class RateLimitFilter implements Filter {
private RateLimitStrategist rateLimitStrategist;
private static final Logger LOGGER = LoggerFactory.getLogger(RateLimitFilter.class);
@Override
public void init(FilterConfig filterConfig) throws ServletException {
if(rateLimitStrategist == null) {
rateLimitStrategist = new RateLimitStrategist();
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if(rateLimitStrategist == null) {
rateLimitStrategist = new RateLimitStrategist();
}
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
String requestURI = req.getRequestURI();
String contextPath = req.getContextPath();
if(StringUtils.isNotBlank(contextPath)) {
requestURI = StringUtils.substring(requestURI, contextPath.length());
}
if(!rateLimitStrategist.tryAcquire(requestURI)) {
res.setContentType("text/html;charset=UTF-8");
res.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("当前服务器繁忙,请稍后再试!");
LOGGER.info(requestURI + "路径请求服务器繁忙,请稍后再试");
} else {
try {
chain.doFilter(request, response);
} catch (Exception e) {
e.printStackTrace();
} finally {
rateLimitStrategist.releaseSemaphore(requestURI);
}
}
}
@Override
public void destroy() {
}
}
原博主的代码写的很好,只用根据自身情况修改一下限流策略里面的代码就可以了,我现在只是在使用时整理了一下问题,这个东西还是值得后面花时间学习的!
我导入的包如下
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>