令牌桶算法限流
限流限流是对某一时间窗口内的请求数进行限制,保持系统的可用性和稳定性,防止因流量暴增而导致的系统运行缓慢或宕机。常用的限流算法有令牌桶和和漏桶,而Google开源项目Guava中的RateLimiter使用的就是令牌桶控制算法。 在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流
我们经常在调别人的接口的时候会发现有限制,比如微信公众平台接口、百度API Store、聚合API等等这样的,对方会限制每天最多调多少次或者每分钟最多调多少次 我们自己在开发系统的时候也需要考虑到这些,比如我们公司在上传商品的时候就做了限流,因为用户每一次上传商品,我们需要将商品数据同到到美团、饿了么、京东、百度、自营等第三方平台,这个工作量是巨大,频繁操作会拖慢系统,故做限流。 以上都是题外话,接下来我们重点看一下令牌桶算法 令牌桶算法下面是从网上找的两张图来描述令牌桶算法:
RateLimiterhttps://github.com/google/guava RateLimiter的代码不长,注释加代码432行,看一下RateLimiter怎么用 1 package com.cjs.example; 2 3 import com.google.common.util.concurrent.RateLimiter; 4 org.springframework.web.bind.annotation.RequestMapping; 5 org.springframework.web.bind.annotation.RestController; 6 7 java.text.SimpleDateFormat; 8 java.util.Date; 9 10 @RestController 11 public class HelloController { 12 13 private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); 14 15 final RateLimiter rateLimiter = RateLimiter.create(216 17 /** 18 * tryAcquire尝试获取permit,默认超时时间是0,意思是拿不到就立即返回false 19 */ 20 @RequestMapping("/sayHello") 21 public String sayHello() { 22 if (rateLimiter.tryAcquire()) { // 一次拿1个 23 System.out.println(sdf.format(new Date())); 24 try { 25 Thread.sleep(50026 } catch (InterruptedException e) { 27 e.printStackTrace(); 28 } 29 }else30 System.out.println("limit"31 } 32 return "hello"; 33 } 34 35 36 * acquire拿不到就等待,拿到为止 37 38 @RequestMapping("/sayHi"39 String sayHi() { 40 rateLimiter.acquire(5); 一次拿5个 41 System.out.println(sdf.format(42 return "hi"43 44 45 } 关于RateLimiter:
基本用法:final RateLimiter rateLimiter = RateLimiter.create(2.0); rate is "2 permits per second" void submitTasks(List<Runnable> tasks,Executor executor) { for (Runnable task : tasks) { rateLimiter.acquire(); may wait executor.execute(task); } } 实现SmoothBursty以稳定的速度生成permit SmoothWarmingUp是渐进式的生成,最终达到最大值趋于稳定 源码片段解读:abstract RateLimiter { /** * 用给定的吞吐量(“permits per second”)创建一个RateLimiter。 * 通常是QPS */ static RateLimiter create(double permitsPerSecond) { return create(permitsPerSecond,SleepingStopwatch.createFromSystemTimer()); } permitsPerSecond,SleepingStopwatch stopwatch) { RateLimiter rateLimiter = new SmoothBursty(stopwatch,1.0 /* maxBurstSeconds */); rateLimiter.setRate(permitsPerSecond); rateLimiter; } * 用给定的吞吐量(QPS)和一个预热期创建一个RateLimiter double permitsPerSecond,long warmupPeriod,TimeUnit unit) { checkArgument(warmupPeriod >= 0,"warmupPeriod must not be negative: %s",warmupPeriod); return create(permitsPerSecond,warmupPeriod,unit,3.0static RateLimiter create( coldFactor,1)"> SmoothWarmingUp(stopwatch,coldFactor); rateLimiter.setRate(permitsPerSecond); rateLimiter; } final SleepingStopwatch stopwatch; 锁 volatile Object mutexDoNotUseDirectly; private Object mutex() { Object mutex = mutexDoNotUseDirectly; if (mutex == null) { synchronized (this) { mutex = mutexDoNotUseDirectly; ) { mutexDoNotUseDirectly = mutex = Object(); } } } mutex; } * 从RateLimiter中获取一个permit,阻塞直到请求可以获得为止 * @return 休眠的时间,单位是秒,如果没有被限制则是0.0 acquire() { return acquire(1); } * 从RateLimiter中获取指定数量的permits,阻塞直到请求可以获得为止 double acquire(int permits) { long microsToWait = reserve(permits); stopwatch.sleepMicrosUninterruptibly(microsToWait); return 1.0 * microsToWait / SECONDS.toMicros(1L); } * 预定给定数量的permits以备将来使用 * 直到这些预定数量的permits可以被消费则返回逝去的微秒数 final long reserve( permits) { checkPermits(permits); synchronized (mutex()) { reserveAndGetWaitLength(permits,stopwatch.readMicros()); } } void checkPermits( permits) { checkArgument(permits > 0,"Requested permits (%s) must be positive"long reserveAndGetWaitLength(int permits,1)"> nowMicros) { long momentAvailable = reserveEarliestAvailable(permits,nowMicros); return max(momentAvailable - nowMicros,0); } } RateLimiter实现的令牌桶算法,不仅可以应对正常流量的限速,而且可以处理突发暴增的请求,实现平滑限流。 通过代码,我们可以看到它可以预消费,怎么讲呢 nextFreeTicketMicros表示下一次请求获得permits的最早时间。每次授权一个请求以后,这个值会向后推移(PS:想象一下时间轴)即向未来推移。因此,大的请求会比小的请求推得更。这里的大小指的是获取permit的数量。这个应该很好理解,因为上一次请求获取的permit数越多,那么下一次再获取授权时更待的时候会更长,反之,如果上一次获取的少,那么时间向后推移的就少,下一次获得许可的时间更短。可见,都是有代价的。正所谓:要浪漫就要付出代价。 还要注意到一点,就是获取令牌和处理请求是两个动作,而且,并不是每一次都获取一个,也不要想当然的认为一个请求获取一个permit(或者叫令牌),可以再看看前面那幅图 Stopwatch一个以纳秒为单位度量流逝时间的对象。它是一个相对时间,而不是绝对时间。 Stopwatch stopwatch = Stopwatch.createStarted(); System.out.println("hahah"); stopwatch.stop(); Duration duration = stopwatch.elapsed(); System.out.println(stopwatch); Semaphore(信号量)
一个信号量维护了一系列permits。 每次调用acquire()方法获取permit,如果必要的话会阻塞直到有一个permit可用为止。 调用release()方法则会释放自己持有的permit,即用完了再还回去。 信号量限制的是并发访问临界资源的线程数。 令牌桶算法 VS 漏桶算法漏桶漏桶的出水速度是恒定的,那么意味着如果瞬时大流量的话,将有大部分请求被丢弃掉(也就是所谓的溢出)。 令牌桶生成令牌的速度是恒定的,而请求去拿令牌是没有速度限制的。这意味,面对瞬时大流量,该算法可以在短时间内请求拿到大量令牌,而且拿令牌的过程并不是消耗很大的事情。 最后,不论是对于令牌桶拿不到令牌被拒绝,还是漏桶的水满了溢出,都是为了保证大部分流量的正常使用,而牺牲掉了少部分流量,这是合理的,如果因为极少部分流量需要保证的话,那么就可能导致系统达到极限而挂掉,得不偿失。 小定律:排队理论https://en.wikipedia.org/wiki/Little%27s_law
在一个固定系统中,顾客的长期平均数量L等于顾客的长期平均到达速率λ乘以顾客在系统中平均花费的时间W。用公式表示为: 虽然这看起来很容易,但这是一个非常显著的举世瞩目的结果,因为这种关系“不受到达过程的分布,服务分布,服务顺序,或其他任何因素的影响”。这个结果适用于任何系统,特别是适用于系统内的系统。唯一的要求是系统必须是稳定的非抢占式的。 例子例1:找响应时间假设有一个应用程序没有简单的方法来度量响应时间。如果系统的平均数量和吞吐量是已知的,那么平均响应时间就是: mean response time = mean number in system / mean throughput 平均响应时间 = 系统的平均数量 / 平均吞吐量. ? 例2:顾客在店里想象一下,一家小商店只有一个柜台和一个可供浏览的区域,每次只能有一个人在柜台,并且没有人不买东西就离开。 所以这个系统大致是:进入 --> 浏览 --> 柜台结账 --> 离开 在一个稳定的系统中,人们进入商店的速度就是他们到达商店的速度(我们叫做到达速度),它们离开的速度叫做离开速度。 相比之下,到达速度超过离开速度代表是一个不稳定的系统,这就会造成等待的顾客数量将逐渐增加到无穷大。 前面的小定律告诉我们,商店的平均顾客数量L等于有效的到达速度λ乘以顾客在商店的平均停留时间W。用公式表示为: 假设,顾客以每小时10个的速度到达,并且平均停留时间是0.5小时。那么这就意味着,任意时间商店的平均顾客数量是5 现在假设商店正在考虑做更多的广告,把到达率提高到每小时20。商店必须准备好容纳平均10人,或者必须将每个顾客在商店中的时间减少到0.25小时。商店可以通过更快地结帐或者增加更多的柜台来达到后者的目的。 我们可以把前面的小定律应用到商店系统中。例如,考虑柜台和在柜台前排的队。假设平均有2个人在柜台前排队,我们知道顾客到达速度是每小时10,所以顾客平均必须停留时间为0.2小时。 最后这是单机(单进程)的限流,是JVM级别的的限流,所有的令牌生成都是在内存中,在分布式环境下不能直接这么用。 如果我们能把permit放到Redis中就可以在分布式环境中用了。 参考https://blog.csdn.net/jek123456/article/details/77152571 https://blog.csdn.net/syc001/article/details/72841951 https://segmentfault.com/a/1190000012875897 https://blog.csdn.net/charleslei/article/details/53152883 https://www.jianshu.com/p/8f548e469bbe https://www.cnblogs.com/f-zhao/p/7210158.html https://m.aspzz.cn/article/127996.htm (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |
- 在java中输出一个方块到控制台
- 如何在Netbeans中为Java项目导入Eclipse语法高亮配置文件?
- TreeSet判断重复元素解析及代码示例
- java – 如何获取离线令牌并刷新令牌并自动刷新对Google AP
- java – SoftReferences与Weakreferences / OutOfMemoryErr
- java 查询oracle数据库所有表DatabaseMetaData的用法(详解)
- 如何更改JFileChooser中的默认java图标
- java – ARM是一个更安全的指令集吗?
- java – (简单)DateFormat,允许24:00:00和00:00:00作为输入
- java – 使用PostgreSQL,为什么Hibernate / JPA不会创建级联