服务保护浅谈--Guava
为什么需要限流?
在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流。限流可以认为服务降级的一种,限流通过限制请求的流量以达到保护系统的目的。
一般来说,系统的吞吐量是可以计算出一个阈值的,为了保证系统的稳定运行,一旦达到这个阈值,就需要限制流量并采取一些措施以完成限制流量的目的。比如:延迟处理,拒绝处理,或者部分拒绝处理等等。否则,很容易导致服务器的宕机。
现有的方案
Google的Guava工具包中就提供了一个限流工具类——RateLimiter,本文也是通过使用该工具类来实现限流功能。RateLimiter是基于“令牌通算法”来实现限流的
令牌桶算法
令牌桶算法是一个存放固定容量令牌(token)的桶,按照固定速率往桶里添加令牌。令牌桶算法基本可以用下面的几个概念来描述:
- 假如用户配置的平均发送速率为r,则每隔1/r秒一个令牌被加入到桶中。
- 桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃或拒绝。
- 当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上。
- 如果桶中的令牌不足n个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么缓冲区等待)。
限流器实现
1.pom文件中引入Guava包
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>23.0</version> </dependency>
2.自定义拦截器,并在拦截器中实现限流
a)定义一个拦截器抽象类,用于多个拦截器复用,主要是继承HandlerInterceptorAdapter,重写preHandle方法;并提供preFilter抽象方法,供子类实现。
package com.xwx.guava.intercepter; import com.alibaba.fastjson.JSONObject; import com.xwx.guava.dto.ResponseDTO; import com.xwx.guava.enums.ResponseEnum; import lombok.extern.log4j.Log4j; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.util.Objects; @Component public abstract class AbstractInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { ResponseEnum result; try { result = preFilter(request); } catch (Exception e) { result = ResponseEnum.SERVER_ERROR; } if (ResponseEnum.OK == result) { return true; } handleResponse(result, response); return false; } //过滤方法 protected abstract ResponseEnum preFilter(HttpServletRequest request); private void handleResponse(ResponseEnum result, HttpServletResponse response) { ResponseDTO responseDTO = new ResponseDTO(); responseDTO.setCode(result.getCode()); responseDTO.setMsg(result.getMsg()); response.setStatus(HttpServletResponse.SC_OK); response.setContentType(MediaType.APPLICATION_JSON_VALUE); PrintWriter writer = null; try { writer = response.getWriter(); writer.write(JSONObject.toJSONString(responseDTO)); } catch (IOException e) { } finally { if (writer != null) { writer.close(); } } } }
b)定义流量控制拦截器,流量控制拦截器继承自上面的拦截器抽象类,在preFilter方法中进行流量控制。
package com.xwx.guava.intercepter;
import com.google.common.util.concurrent.RateLimiter;
import com.xwx.guava.enums.ResponseEnum;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Component("rateLimitInterceptor")
public class RateLimitInterceptor extends AbstractInterceptor {
/**
* 单机全局限流器
*/
private static final RateLimiter rateLimiter=RateLimiter.create(1);
@Override
protected ResponseEnum preFilter(HttpServletRequest request) {
if(!rateLimiter.tryAcquire()){
System.out.println("限流中......");
return ResponseEnum.RATE_LIMIT;
}
System.out.println("请求成功");
return ResponseEnum.OK;
}
}
使用Guava提供的RateLimiter类来实现流量控制,过程很简单:定义了一个QPS为1的全局限流器(便于测试),使用tryAcquire()方法来尝试获取令牌,如果成功则返回ResponseEnum.OK,否则返回ResponseEnum.RATE_LIMIT。
3.继承WebMvcConfigurerAdapter来添加自定义拦截器
package com.xwx.guava.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.CacheControl;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestAttributeMethodArgumentResolver;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Autowired
@Qualifier("rateLimitInterceptor")
private HandlerInterceptorAdapter rateLimitInterceptor;
// @Autowired
// @Qualifier("authorityInterceptor")
// private HandlerInterceptorAdapter authorityInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(rateLimitInterceptor).addPathPatterns("/**");
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("/").setCacheControl(CacheControl.maxAge(1, TimeUnit.DAYS).cachePublic());
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new RequestAttributeMethodArgumentResolver());
}
}
4.写一个Controller来提供一个简单的访问接口
package com.xwx.guava.controller; import com.xwx.guava.dto.ResponseDTO; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/test") public class TestController { @RequestMapping("testGuava") public ResponseDTO testGuava() throws InterruptedException { ResponseDTO dto=new ResponseDTO(); return dto; } }
上文使用到的ResponseEnum是一个返回Code的枚举:
package com.xwx.guava.enums; public enum ResponseEnum { OK(200,"成功"), RATE_LIMIT(401,"访问次数受限"), SERVER_ERROR(500,"服务器错误"), QUERY_USER_FAILED(601,"查询用户失败"); private int code; private String msg; ResponseEnum(int code,String msg){ this.code=code; this.msg=msg; } public int getCode(){ return code; } public String getMsg(){ return msg; } }
最后我们使用jmeter来进行测试
选择10个线程
我们看下结果
至此,简单的限流器实现完成。