限流算法类别
- 计数器算法
- 令牌桶算法
- 漏桶算法
计数器算法
-
简要
计算器算法是以固定速率单位时间内计数,如果达到最大速率则拒绝访问走服务降级。类似返回:“服务忙,请稍后重试!”等服务降级信息。
固定计数速率:限制时间段内访问总数,例如限制一分钟10个请求:10R/M
-
图示
-
缺点
临界点问题,如下图所示
平滑计数器算法(滑动窗口)
-
简要
平滑计数器算法主要是为了解决计数器算法临界点问题,达到平滑访问。相比计数器算法,滑动窗口会更加平滑,能自动消除毛刺。
-
图示
令牌桶算法
- 简要
令牌桶算法是以恒定的速率源源不断地产生令牌放入桶中。如果令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,直到把桶填满。如果桶中无令牌则拒绝访问。
固定速率:固定速率生成令牌数量
固定大小:桶容量大小固定,当令牌桶满后则直接丢弃还在生成的令牌,当桶容量有剩余时继续放入令牌。
-
图示
-
缺点
存在t1时间段无请求,此时令牌还在生成,生成的令牌数量为t1*速率,当t2时间段时候假设此时桶中已有100个令牌,此时则能一次性接收100个请求,给服务器带来的是瞬间性的压力,所以不能够达到平滑效果。
-
实现
使用Google guava包中的
RateLimiter
类可以很简单的实现令牌桶限流算法。@RestController public class IndexController { /** * guava工具包提供的限流类, * 参数:permitsPerSecond 表示一秒生成令牌的数量 */ private RateLimiter rateLimiter = RateLimiter.create(1.0); @RequestMapping("/index") public String index() { //尝试获取令牌,获取令牌超时时间为500毫秒 boolean tryAcquire = rateLimiter.tryAcquire(500, TimeUnit.MICROSECONDS); //如果未拿到令牌,则走服务降级,返回提示信息 if (!tryAcquire) { System.out.println("服务忙,请稍后重试!"); return "服务忙,请稍后重试!"; } // 执行业务逻辑 System.out.println("....执行业务逻辑....."); return "处理成功"; } }
以上设置令牌桶生成的速率为1R/S,浏览器访问/index,狂按F5刷新访问,可以看到偶尔会出现"服务忙,请稍后重试!"提示信息,控制台打印信息如下。
....执行业务逻辑..... ....执行业务逻辑..... ....执行业务逻辑..... ....执行业务逻辑..... 服务忙,请稍后重试! 服务忙,请稍后重试! 服务忙,请稍后重试! 服务忙,请稍后重试! 服务忙,请稍后重试! ....执行业务逻辑..... 服务忙,请稍后重试! 服务忙,请稍后重试! 服务忙,请稍后重试! 服务忙,请稍后重试! 服务忙,请稍后重试! ....执行业务逻辑..... 服务忙,请稍后重试! 服务忙,请稍后重试! 服务忙,请稍后重试! 服务忙,请稍后重试! 服务忙,请稍后重试! ....执行业务逻辑.....
漏桶算法
-
简要
漏桶算法可以控制端口的流量输出速率,平滑网络上的突发流量,实现流量整形,从而为网络提供一个稳定的流量。类似于沙漏效果。
固定流出速率:流出桶外的水滴速率固定
固定大小:漏桶算法桶大小固定,当桶满后续请求直接走服务降级操作。
任意流入速率:流入桶中的水滴速率任意
-
图示
-
实现
1、使用guava的RateLimiter创建SmoothWarmingUp限速器达到类似漏桶算法的实现。
/** * double permitsPerSecond : 每秒生成令牌数 * long warmupPeriod, * TimeUnit unit */ private RateLimiter rateLimiter = RateLimiter.create(10.0, 1L, TimeUnit.SECONDS); @RequestMapping("/guavaIndex") public String index() { //尝试获取令牌,获取令牌超时时间为500毫秒 boolean tryAcquire = rateLimiter.tryAcquire(500, TimeUnit.MICROSECONDS); //如果未拿到令牌,则走服务降级,返回提示信息 if (!tryAcquire) { System.out.println("服务忙,请稍后重试!"); return "服务忙,请稍后重试!"; } // 执行业务逻辑 System.out.println("....执行业务逻辑....."); return "处理成功"; }
2、使用队列方式实现漏桶算法
public class LeakyBucketLimiter { /** * 桶容量 */ private static final int MAX_BUCKET_SIZE = 10; /** * 桶出口大小 */ private static final int MAX_OUT_SIZE = 1; /** * 初始化一个而请求队列,并设置最大容量 */ private LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>(MAX_BUCKET_SIZE); /** * 模拟执行业务逻辑的线程池 */ private ExecutorService executorService = Executors.newFixedThreadPool(2); /** * 模拟执行出口水流的线程池 */ private ExecutorService outProcessExecutorService = Executors.newSingleThreadExecutor(); /** * 存放请求对应处理的未来结果 */ private Map<String, Future<String>> requestProcessFutureMap = new ConcurrentHashMap<>(); public void put(String requestId) { try { queue.put(requestId); } catch (InterruptedException e) { e.printStackTrace(); } } /** * 是否成功入队 * * @param requestId 请求id * @return */ public boolean acquire(String requestId) { //桶满拒绝服务 if (MAX_BUCKET_SIZE == queue.size()) { return false; } //桶未满放入队列 put(requestId); for (; ; ) { //如果结果集中包含了此请求则返回 if (requestProcessFutureMap.containsKey(requestId)) { return true; } } } /** * 启动一个独立的线程按一定速率MAX_OUT_SIZE/s从队列中拉取数据 */ public void startOut() { outProcessExecutorService.submit(() -> { for (; ; ) { for (int i = 0; i < MAX_OUT_SIZE; i++) { String requestId = queue.poll(); if (requestId == null) { //队列空 continue; } Future<String> future = executorService.submit(() -> { System.out.println("...开始处理请求....:" + requestId); //模拟复杂业务逻辑 TimeUnit.SECONDS.sleep(1); return "成功"; }); requestProcessFutureMap.put(requestId, future); } try { //休眠一秒 速率 MAX_OUT_SIZE/s TimeUnit.SECONDS.sleep(1L); } catch (InterruptedException e) { e.printStackTrace(); } } }); } /** * 获取请求的未来结果 * * @param requestedSessionId * @return */ public Future<String> removeFuture(String requestedSessionId) { return requestProcessFutureMap.remove(requestedSessionId); } }
定义一个请求方法
@RequestMapping("/myIndex") public String myIndex() { String requestId = UUID.randomUUID().toString(); boolean tryAcquire = leakyBucketLimiter.acquire(requestId); //如果未拿到令牌,则走服务降级,返回提示信息 if (!tryAcquire) { System.out.println("服务忙,请稍后重试!"); return "服务忙,请稍后重试!"; } Future<String> future = leakyBucketLimiter.removeFuture(requestId); try { Object o = future.get(); System.out.println("...执行业务逻辑完成...结果为:" + o); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } return "处理成功"; }
浏览器或者jemeter频繁请求此url,控制台打印结果如下:
...开始处理请求....:51230fae-59bd-4d73-be8d-f8f384e5cf2f ...执行业务逻辑完成...结果为:成功 ...开始处理请求....:d422c2b3-32c6-44ae-9335-dcef6aa4acb7 ...开始处理请求....:87076846-8617-49a0-860e-c820c430b82b ...执行业务逻辑完成...结果为:成功 服务忙,请稍后重试! 服务忙,请稍后重试! ...开始处理请求....:112dc4a4-ad9f-4678-ae4a-842d1566e588 ...执行业务逻辑完成...结果为:成功 ...开始处理请求....:7a8d909a-9411-4f3b-8a8d-02862491a315 ...执行业务逻辑完成...结果为:成功 ...开始处理请求....:cdddaacb-c2ed-4272-972e-91e0ac514da4 ...执行业务逻辑完成...结果为:成功 ...开始处理请求....:77acd420-db75-4e67-8d08-8fdff93ad28b ...执行业务逻辑完成...结果为:成功 ...执行业务逻辑完成...结果为:成功 ...开始处理请求....:df807a57-9356-4cd0-bef2-ac1f9dd93524 ...开始处理请求....:23c08cc4-56ee-42e4-ab5a-b766511905a1 ...执行业务逻辑完成...结果为:成功 ...开始处理请求....:5ed02381-d40d-4877-aeab-2e5545bb7448 ...执行业务逻辑完成...结果为:成功 ...执行业务逻辑完成...结果为:成功 ...开始处理请求....:cb819fbb-8f29-4aea-b680-2be29a01cfd7 ...执行业务逻辑完成...结果为:成功 ...开始处理请求....:6297dab6-ee87-4ee2-90f1-b8679dc3899f ...执行业务逻辑完成...结果为:成功 ...开始处理请求....:c9904439-8cef-4fcd-9f05-47439ce1dc68 ...执行业务逻辑完成...结果为:成功 ...开始处理请求....:c867cf6e-1573-431e-9473-a4015dda112e ...执行业务逻辑完成...结果为:成功