前面已经介绍了不少zuul的功能,本篇继续介绍它的另外一大功能。在高并发的应用中,限流每每是一个绕不开的话题。本文详细探讨在Spring Cloud中如何实现限流。html
在 Zuul
上实现限流是个不错的选择,只须要编写一个过滤器就能够了,关键在于如何实现限流的算法。常见的限流算法有漏桶算法以及令牌桶算法。这个可参考 https://www.cnblogs.com/LBSer/p/4083131.html ,写得通俗易懂,你值得拥有,我就不拽文了。redis
GoogleGuava
为咱们提供了限流工具类 RateLimiter
,因而乎,咱们能够撸代码了。算法
@Component public class RateLimitZuulFilter extends ZuulFilter { private final RateLimiter rateLimiter = RateLimiter.create(1000.0); @Override public String filterType() { return FilterConstants.PRE_TYPE; } @Override public int filterOrder() { return Ordered.HIGHEST_PRECEDENCE; } @Override public boolean shouldFilter() { // 这里能够考虑弄个限流开启的开关,开启限流返回true,关闭限流返回false,你懂的。 return true; } @Override public Object run() { try { RequestContext currentContext = RequestContext.getCurrentContext(); HttpServletResponse response = currentContext.getResponse(); if (!rateLimiter.tryAcquire()) { HttpStatus httpStatus = HttpStatus.TOO_MANY_REQUESTS; response.setContentType(MediaType.TEXT_PLAIN_VALUE); response.setStatus(httpStatus.value()); response.getWriter().append(httpStatus.getReasonPhrase()); currentContext.setSendZuulResponse(false); throw new ZuulException( httpStatus.getReasonPhrase(), httpStatus.value(), httpStatus.getReasonPhrase() ); } } catch (Exception e) { ReflectionUtils.rethrowRuntimeException(e); } return null; } }
如上,咱们编写了一个 pre
类型的过滤器。对Zuul过滤器有疑问的可参考个人博客:spring
Spring Cloud内置的Zuul过滤器详解:http://www.itmuch.com/spring-cloud/zuul/zuul-filter-in-spring-cloud
Spring Cloud Zuul过滤器详解:http://www.itmuch.com/spring-cloud/zuul/spring-cloud-zuul-filter
在过滤器中,咱们使用 GuavaRateLimiter
实现限流,若是已经达到最大流量,就抛异常数据库
以上单节点Zuul下的限流,但在生产中,咱们每每会有多个Zuul实例。对于这种场景如何限流呢?咱们能够借助Redis实现限流。并发
使用redis实现,存储两个key,一个用于计时,一个用于计数。请求每调用一次,计数器增长1,若在计时器时间内计数器未超过阈值,则能够处理任务app
if(!cacheDao.hasKey(TIME_KEY)) { cacheDao.putToValue(TIME_KEY, 0, 1, TimeUnit.SECONDS); } if(cacheDao.hasKey(TIME_KEY) && cacheDao.incrBy(COUNTER_KEY, 1) > 400) { // 抛个异常什么的 }
一些场景下,咱们可能还须要实现微服务粒度的限流。此时能够有两种方案:分布式
和在Zuul上实现限流相似,只需编写一个过滤器或者拦截器便可,比较简单,不做赘述。我的不太喜欢这种方式,由于每一个微服务都得编码,感受成本很高啊。ide
加班那么多,做为程序猿的咱们,应该学会偷懒,这样才可能有时间孝顺父母、抱老婆、逗儿子、遛狗养鸟、聊天打屁、追求人生信仰。好了不扯淡了,看方法二吧。微服务
在讲解以前,咱们不妨模拟两个路由规则,两种路由规则分别表明Zuul的两种路由方式。
zuul:
routes:
microservice-provider-user: /user/**
user2:
url: http://localhost:8000/
path: /user2/**
如配置所示,在这里,咱们定义了两个路由规则, microservice-provider-user
以及 user2
,其中 microservice-provider-user
这个路由规则使用到Ribbon + Hystrix,走的是 RibbonRoutingFilter
;而 user2
这个路由用不上Ribbon也用不上Hystrix,走的是 SipleRoutingFilter
。若是你搞不清楚这点,请参阅个人博客:
Spring Cloud内置的Zuul过滤器详解:http://www.itmuch.com/spring-cloud/zuul/zuul-filter-in-spring-cloud
Spring Cloud Zuul过滤器详解:http://www.itmuch.com/spring-cloud/zuul/spring-cloud-zuul-filter
搞清楚这点以后,咱们就能够撸代码了:
@Component public class RateLimitZuulFilter extends ZuulFilter { private Map<String, RateLimiter> map = Maps.newConcurrentMap(); @Override public String filterType() { return FilterConstants.PRE_TYPE; } @Override public int filterOrder() { // 这边的order必定要大于org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter的order // 也就是要大于5 // 不然,RequestContext.getCurrentContext()里拿不到serviceId等数据。 return Ordered.LOWEST_PRECEDENCE; } @Override public boolean shouldFilter() { // 这里能够考虑弄个限流开启的开关,开启限流返回true,关闭限流返回false,你懂的。 return true; } @Override public Object run() { try { RequestContext context = RequestContext.getCurrentContext(); HttpServletResponse response = context.getResponse(); String key = null; // 对于service格式的路由,走RibbonRoutingFilter String serviceId = (String) context.get(SERVICE_ID_KEY); if (serviceId != null) { key = serviceId; map.putIfAbsent(serviceId, RateLimiter.create(1000.0)); } // 若是压根不走RibbonRoutingFilter,则认为是URL格式的路由 else { // 对于URL格式的路由,走SimpleHostRoutingFilter URL routeHost = context.getRouteHost(); if (routeHost != null) { String url = routeHost.toString(); key = url; map.putIfAbsent(url, RateLimiter.create(2000.0)); } } RateLimiter rateLimiter = map.get(key); if (!rateLimiter.tryAcquire()) { HttpStatus httpStatus = HttpStatus.TOO_MANY_REQUESTS; response.setContentType(MediaType.TEXT_PLAIN_VALUE); response.setStatus(httpStatus.value()); response.getWriter().append(httpStatus.getReasonPhrase()); context.setSendZuulResponse(false); throw new ZuulException( httpStatus.getReasonPhrase(), httpStatus.value(), httpStatus.getReasonPhrase() ); } } catch (Exception e) { ReflectionUtils.rethrowRuntimeException(e); } return null; } }
简单讲解一下这段代码:
对于 microservice-provider-user
这个路由,咱们能够用 context.get(SERVICE_ID_KEY);
获取到serviceId,获取出来就是 microservice-provider-user
;
而对于 user2
这个路由,咱们使用 context.get(SERVICE_ID_KEY);
得到是null,可是呢,能够用 context.getRouteHost()
得到路由到的地址,获取出来就是 http://localhost:8000/
。接下来的事情,大家懂的。
实际项目中,除以上实现的限流方式,还可能会:
下面,笔者借助Spring Boot Actuator提供的 Metrics
能力进行实现基于内存压力的限流——当可用内存低于某个阈值就开启限流,不然不开启限流。
@Component public class RateLimitZuulFilter extends ZuulFilter { @Autowired private SystemPublicMetrics systemPublicMetrics; @Override public boolean shouldFilter() { // 这里能够考虑弄个限流开启的开关,开启限流返回true,关闭限流返回false,你懂的。 Collection<Metric<?>> metrics = systemPublicMetrics.metrics(); Optional<Metric<?>> freeMemoryMetric = metrics.stream() .filter(t -> "mem.free".equals(t.getName())) .findFirst(); // 若是不存在这个指标,稳妥起见,返回true,开启限流 if (!freeMemoryMetric.isPresent()) { return true; } long freeMemory = freeMemoryMetric.get() .getValue() .longValue(); // 若是可用内存小于1000000KB,开启流控 return freeMemory < 1000000L; } // 省略其余方法 }
例如:
对请求的目标URL进行限流(例如:某个URL每分钟只容许调用多少次)
对客户端的访问IP进行限流(例如:某个IP每分钟只容许请求多少次)
对某些特定用户或者用户组进行限流(例如:非VIP用户限制每分钟只容许调用100次某个API等)
多维度混合的限流。此时,就须要实现一些限流规则的编排机制。与、或、非等关系。
分布式环境下限流方案的实现:http://blog.csdn.net/Justnow_/article/details/53055299