一、应用背景
在当前微服务的大环境下,大部分的服务一般都是通过接入分布式限流框架来实现限流、熔断、降级,但大型的分布式框架有时显得过于”重量”,在一些小型系统或单机Demo级别的应用开发中,其实可以考虑单机型的限流框架,例如今天的主角——RateLimiter。
RateLimiter是Google Guava下的一款单机轻量级别的小框架、小工具。
二、算法介绍
一般聊到限流算法,最常见的莫过于两种:漏桶算法和令牌桶算法
-
漏桶算法:也就是有一个桶,底部有一个小洞,当有水不断地从上面加入进来时,底部出水的速率都是一样的,当加水速度持续大于漏水速度时,就会有一个时刻,桶满了,此时就不再允许往桶里加水了(再加就溢出来了)。
特点:不管上面加水快还是加水慢,漏水速率会一致保持不变
-
令牌桶算法:有一个桶,有人以一定的速率一直往里面放令牌,所以被叫做令牌桶,此时如果有人需要这个令牌了,就会从桶里拿一个,如果又来人了就会又被拿一个,拿到了令牌的人就可以进站了,没有拿到的人就要在外面等。
特点:令牌桶算法只固定了放令牌的速度,而不会限制拿令牌的人,拿的人有就能拿到,没有就拿不到了,所以令牌桶有个优点就是可以有一定的峰值,一次性发很多令牌(如果有的话)
三、应用实践
-
RateLimiter使用的就是令牌桶算法
-
常用API介绍
- RateLimiter.create(permitsPerSecond):设置当前接口的QPS
- rateLimiter.tryAcquire(timeout, timeUnit):尝试在一定时间内获取令牌,超时则退出
-
代码实践
-
工具类封装
public class GuavaRateLimiterUtils { private final static GuavaRateLimiterUtils INSTANCE = new GuavaRateLimiterUtils(); private ConcurrentHashMap<String, RateLimiter> RATE_LIMITER_MAP = new ConcurrentHashMap<>(); private GuavaRateLimiterUtils() {} /** * 单例 * @return 返回单例对象 */ public static GuavaRateLimiterUtils getInstance() { return INSTANCE; } /** * 设置限流值 * @param path 请求路径 * @param permitsPerSecond 一秒内的限流数 */ public void setPermits(String path, Double permitsPerSecond) { if (!RATE_LIMITER_MAP.containsKey(path)) { RATE_LIMITER_MAP.put(path, RateLimiter.create(permitsPerSecond)); } } /** * 尝试获取令牌 * @param requestPath 接口路径 * @param timeout 超时时间 * @param timeUnit 超时时间单位 * @return 获取是否成功 */ public Boolean tryAcquire(String requestPath, Long timeout, TimeUnit timeUnit) { if (!RATE_LIMITER_MAP.containsKey(requestPath)) { log.debug("限流列表中未查询到该路径:{},直接放行", requestPath); return true; } RateLimiter rateLimiter = RATE_LIMITER_MAP.get(requestPath); if (!rateLimiter.tryAcquire(timeout, timeUnit)) { log.info("当前接口限流:{}", requestPath); return false; } return true; } /** * 第一次接口结束以后,根据本次执行时间,自适应调整QPS,使其尽量接近TPS * @param path 接口路径 * @param spendTime 本次执行时间 */ public void adaptivePermits(String path, Long spendTime) { if (RATE_LIMITER_MAP.containsKey(path)) { RATE_LIMITER_MAP.get(path).setRate(1000.00 / spendTime); log.info("{}限流自适应调整成功:permits: {}", path, RATE_LIMITER_MAP.get(path).getRate()); } } }
-
限流注解实现
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RateLimiter { /** * 一秒内允许通过的请求数 QPS */ double permitRate(); /** * 超时时间 */ long timeout(); /** * 超时时间单位 */ TimeUnit timeUnit(); }
-
拦截器实现
@Slf4j public class RateLimiterInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //解析注解 RateLimiter rateLimiter = ((HandlerMethod) handler).getMethodAnnotation(RateLimiter.class); if (Objects.nonNull(rateLimiter)) { GuavaRateLimiterUtils instance = GuavaRateLimiterUtils.getInstance(); //限流 instance.setPermits(request.getRequestURI(), rateLimiter.permitRate()); //限流拦截 if (!instance.tryAcquire(request.getRequestURI(), rateLimiter.timeout(), rateLimiter.timeUnit())) { response.setContentType("application/json;charset=UTF-8"); response.getWriter().append(JSONObject.toJSONString(CommonResponse.failureWithErrorCode(ErrorCodeConstant.BUSINESS_BUSY))); return false; } } return true; } }
-
controller层应用
/** * 导出Excel * @param response servlet响应 */ @RateLimiter(permitRate = 0.5, timeout = 5, timeUnit = TimeUnit.SECONDS) @GetMapping(value = "/export", produces = "application/json; charset=utf-8") public CommonResponse<Void> exportExcel(HttpServletResponse response, PurchaseOrderRequest request) { //业务代码省略...... return CommonResponse.successWithData(null); }
-
四、拓展
- 从基础层面来讲,RateLimiter本身实现的是QPS的限流,对于一些要求TPS限流的接口可能不太友好,但我们可以考虑率通过自适应的方式,使其尽量接近达到TPS限流的效果,例如根据每次接口响应的时间来计算平均一秒能够处理的请求数,进而修改RateLimiter的rate参数,见工具类封装的void adaptivePermits(String path, Long spendTime)方法