限流的两种算法以及相关的实现方法
令牌桶算法限流
限流
限流是对某一时间窗口内的请求数进行限制,保持系统的可用性和稳定性,防止因流量暴增而导致的系统运行缓慢或宕机。常用的限流算法有令牌桶和和漏桶,而Google开源项目Guava中的RateLimiter使用的就是令牌桶控制算法。
在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流
- 缓存:缓存的目的是提升系统访问速度和增大系统处理容量
- 降级:降级是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行
- 限流:限流的目的是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理
我们经常在调别人的接口的时候会发现有限制,比如微信公众平台接口、百度API Store、聚合API等等这样的,对方会限制每天最多调多少次或者每分钟最多调多少次
我们自己在开发系统的时候也需要考虑到这些,比如我们公司在上传商品的时候就做了限流,因为用户每一次上传商品,我们需要将商品数据同到到美团、饿了么、京东、百度、自营等第三方平台,这个工作量是巨大,频繁操作会拖慢系统,故做限流。
以上都是题外话,接下来我们重点看一下令牌桶算法
令牌桶算法
下面是从网上找的两张图来描述令牌桶算法:
RateLimiter
https://github.com/google/guava
RateLimiter的代码不长,注释加代码432行,看一下RateLimiter怎么用
1 package com.cjs.example;2 3 import com.google.common.util.concurrent.RateLimiter;4 import org.springframework.web.bind.annotation.RequestMapping;5 import org.springframework.web.bind.annotation.RestController;6 7 import java.text.SimpleDateFormat;8 import 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 private static final RateLimiter rateLimiter = RateLimiter.create(2); 16 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(500); 26 } catch (InterruptedException e) { 27 e.printStackTrace(); 28 } 29 }else { 30 System.out.println("limit"); 31 } 32 return "hello"; 33 } 34 35 /** 36 * acquire拿不到就等待,拿到为止 37 */ 38 @RequestMapping("/sayHi") 39 public String sayHi() { 40 rateLimiter.acquire(5); // 一次拿5个 41 System.out.println(sdf.format(new Date())); 42 return "hi"; 43 } 44 45 }
关于RateLimiter:
- A rate limiter。每个acquire()方法如果必要的话会阻塞直到一个permit可用,然后消费它。获得permit以后不需要释放。
- RateLimiter在并发环境下使用是安全的:它将限制所有线程调用的总速率。注意,它不保证公平调用。
- RateLimiter在并发环境下使用是安全的:它将限制所有线程调用的总速率。注意,它不保证公平调用。Rate limiter(直译为:速度限制器)经常被用来限制一些物理或者逻辑资源的访问速率。这和java.util.concurrent.Semaphore正好形成对照。
- 一个RateLimiter主要定义了发放permits的速率。如果没有额外的配置,permits将以固定的速度分配,单位是每秒多少permits。默认情况下,Permits将会被稳定的平缓的发放。
- 可以配置一个RateLimiter有一个预热期,在此期间permits的发放速度每秒稳步增长直到到达稳定的速率
基本用法:
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 waitexecutor.execute(task);} }
实现
SmoothBursty以稳定的速度生成permit
SmoothWarmingUp是渐进式的生成,最终达到最大值趋于稳定
源码片段解读:
public abstract class RateLimiter {/*** 用给定的吞吐量(“permits per second”)创建一个RateLimiter。* 通常是QPS*/public static RateLimiter create(double permitsPerSecond) {return create(permitsPerSecond, SleepingStopwatch.createFromSystemTimer());}static RateLimiter create(double permitsPerSecond, SleepingStopwatch stopwatch) {RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);rateLimiter.setRate(permitsPerSecond);return rateLimiter;}/*** 用给定的吞吐量(QPS)和一个预热期创建一个RateLimiter*/public static RateLimiter create(double permitsPerSecond, long warmupPeriod, TimeUnit unit) {checkArgument(warmupPeriod >= 0, "warmupPeriod must not be negative: %s", warmupPeriod);return create(permitsPerSecond, warmupPeriod, unit, 3.0, SleepingStopwatch.createFromSystemTimer());}static RateLimiter create(double permitsPerSecond,long warmupPeriod,TimeUnit unit,double coldFactor,SleepingStopwatch stopwatch) {RateLimiter rateLimiter = new SmoothWarmingUp(stopwatch, warmupPeriod, unit, coldFactor);rateLimiter.setRate(permitsPerSecond);return rateLimiter;}private final SleepingStopwatch stopwatch;// 锁private volatile Object mutexDoNotUseDirectly;private Object mutex() {Object mutex = mutexDoNotUseDirectly;if (mutex == null) {synchronized (this) {mutex = mutexDoNotUseDirectly;if (mutex == null) {mutexDoNotUseDirectly = mutex = new Object();}}}return mutex;}/*** 从RateLimiter中获取一个permit,阻塞直到请求可以获得为止* @return 休眠的时间,单位是秒,如果没有被限制则是0.0*/public double acquire() {return acquire(1);}/*** 从RateLimiter中获取指定数量的permits,阻塞直到请求可以获得为止*/public double acquire(int permits) {long microsToWait = reserve(permits);stopwatch.sleepMicrosUninterruptibly(microsToWait);return 1.0 * microsToWait / SECONDS.toMicros(1L);}/*** 预定给定数量的permits以备将来使用* 直到这些预定数量的permits可以被消费则返回逝去的微秒数*/final long reserve(int permits) {checkPermits(permits);synchronized (mutex()) {return reserveAndGetWaitLength(permits, stopwatch.readMicros());}}private static void checkPermits(int permits) {checkArgument(permits > 0, "Requested permits (%s) must be positive", permits);}final long reserveAndGetWaitLength(int permits, long nowMicros) {long momentAvailable = reserveEarliestAvailable(permits, nowMicros);return max(momentAvailable - nowMicros, 0);} }abstract class SmoothRateLimiter extends RateLimiter {/** The currently stored permits. */double storedPermits;/** The maximum number of stored permits. */double maxPermits;/*** The interval between two unit requests, at our stable rate. E.g., a stable rate of 5 permits* per second has a stable interval of 200ms.*/double stableIntervalMicros;/*** The time when the next request (no matter its size) will be granted. After granting a request,* this is pushed further in the future. Large requests push this further than small requests.*/private long nextFreeTicketMicros = 0L; // could be either in the past or futurefinal long reserveEarliestAvailable(int requiredPermits, long nowMicros) {resync(nowMicros);long returnValue = nextFreeTicketMicros;double storedPermitsToSpend = min(requiredPermits, this.storedPermits); // 本次可以获取到的permit数量double freshPermits = requiredPermits - storedPermitsToSpend; // 差值,如果存储的permit大于本次需要的permit数量则此处是0,否则是一个正数long waitMicros =storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)+ (long) (freshPermits * stableIntervalMicros); // 计算需要等待的时间(微秒)this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);this.storedPermits -= storedPermitsToSpend; // 减去本次消费的permit数return returnValue;}void resync(long nowMicros) {// if nextFreeTicket is in the past, resync to nowif (nowMicros > nextFreeTicketMicros) { // 表示当前可以获得permitdouble newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros(); // 计算这段时间可以生成多少个permitstoredPermits = min(maxPermits, storedPermits + newPermits); // 如果超过maxPermit,则取maxPermit,否则取存储的permit+新生成的permitnextFreeTicketMicros = nowMicros; // 设置下一次可以获得permit的时间点为当前时间}} }
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(信号量)
A counting semaphore. Conceptually, a semaphore maintains a set of permits. Each acquire() blocks if necessary until a permit is available, and then takes it. Each release() adds a permit, potentially releasing a blocking acquirer. However, no actual permit objects are used; the Semaphore just keeps a count of the number available and acts accordingly.
一个信号量维护了一系列permits。
每次调用acquire()方法获取permit,如果必要的话会阻塞直到有一个permit可用为止。
调用release()方法则会释放自己持有的permit,即用完了再还回去。
信号量限制的是并发访问临界资源的线程数。
令牌桶算法 VS 漏桶算法
漏桶
漏桶的出水速度是恒定的,那么意味着如果瞬时大流量的话,将有大部分请求被丢弃掉(也就是所谓的溢出)。
令牌桶
生成令牌的速度是恒定的,而请求去拿令牌是没有速度限制的。这意味,面对瞬时大流量,该算法可以在短时间内请求拿到大量令牌,而且拿令牌的过程并不是消耗很大的事情。
最后,不论是对于令牌桶拿不到令牌被拒绝,还是漏桶的水满了溢出,都是为了保证大部分流量的正常使用,而牺牲掉了少部分流量,这是合理的,如果因为极少部分流量需要保证的话,那么就可能导致系统达到极限而挂掉,得不偿失。
小定律:排队理论
https://en.wikipedia.org/wiki/Little%27s_law
the long-term average number L of customers in a stationary system is equal to the long-term average effective arrival rate λ multiplied by the average time W that a customer spends in the system. Expressed algebraically the law is:
在一个固定系统中,顾客的长期平均数量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.jb51.net/article/127996.htm
限流的两种算法以及相关的实现方法相关推荐
- Sentinel限流及其滑动窗口算法
Sentinel限流及其滑动窗口算法 Sentinel的限流原理 滑动时间窗口算法 Sentinel的限流原理 限流效果,对应有DefaultController快速失败 WarmUpControll ...
- Redis 限流的 3 种方式
大家好,我是DD. 现在出去面试各种后端开发岗位,高并发场景相关的题基本是不会少的.而在高并发场景下,限流又是非常重要的一块.今天就来聊聊限流的解决方案. 当然,限流有许多种实现的方式,Redis具有 ...
- 信号量与令牌桶_限流的4种方式令牌桶实战
限流的4种方式 正文 限流 限流是对某一时间窗口内的请求数进行限制,保持系统的可用性和稳定性,防止因流量暴增而导致的系统运行缓慢或宕机.常用的限流算法有令牌桶和和漏桶,而Google开源项目Guava ...
- OpenResty实现限流的几种方式
原文地址:http://blog.51cto.com/zhweizhi/2063157 在开发 api 网关的时,做过一些简单的限流,比如说静态拦截和动态拦截:静态拦截说白了就是限流某一个接口在一定时 ...
- Redis 实现限流的三种方式
欢迎关注方志朋的博客,回复"666"获面试宝典 面对越来越多的高并发场景,限流显示的尤为重要. 当然,限流有许多种实现的方式,Redis具有很强大的功能,我用Redis实践了三种的 ...
- Redis 限流的 3 种方式,还有谁不会!
面对越来越多的高并发场景,限流显示的尤为重要. 当然,限流有许多种实现的方式,Redis具有很强大的功能,我用Redis实践了三种的实现方式,可以较为简单的实现其方式.Redis不仅仅是可以做限流,还 ...
- 求解最大流的四种算法介绍、利用最大流模型解题入门
求解最大流的四种算法介绍.利用最大流模型解题入门 上一篇中介绍了网络流的基础,最大流最小割定理的证明,下面来看如何求一个容量网络的最大流,这里介绍四种算法:EK算法.SAP算法.DINIC算法.HLP ...
- 图文详解两种算法:深度优先遍历(DFS)和广度优先遍历(BFS)
图文详解两种算法:深度优先遍历(DFS)和广度优先遍历(BFS) 阅读本文前,请确保你已经掌握了递归.栈和队列的基本知识,如想掌握搜索的代码实现,请确保你能够用代码实现栈和队列的基本操作. 深度优先遍 ...
- “斐波那契数列”的两种算法
"斐波那契数列"的两种算法 斐波那契数列有个规律:从第三个数开始,每个数是前两个数之和,比如: 1 1 2 3 5 8 13 21 34 55...... 现在通过两种方式(递归与 ...
最新文章
- 实体服务是一种反模式
- Spring Boot 2.x基础教程:使用 Thymeleaf开发Web页面
- Pytorch:使用Alexnet网络实现CIFAR10分类
- Scala入门到精通——第十六节 泛型与注解
- git保护--git分支创建
- 又见高铁霸座,占座者嚣张称“就不让”!结果舒适…
- 怎么把截屏的一部分内容涂掉_电脑怎么录屏?录屏软件的使用技巧
- Spring Boot 项目打成 .exe 程序,实战来了!
- 信息技术开拓视野——记IT战略规划与企业架构培训课程
- 18年6月英语六级第一套听力单词
- 爱普生L4158 L4165 L4166 L4168 L4167 L4163清零软件教程
- 【STM32F429】第10章 ThreadX GUIX移植到STM32F429(GCC)
- JavaScript高级程序设计之表单基础
- 应该将composer.lock致力于版本控制吗?
- CSS3的新特性以及IE下的实现
- 机器学习之路一:线性模型、非线性模型、神经网络
- 在springboot中使用jsp,设置webapp目录时的操作步骤
- 各行各业数据及分析研究报告网站参考
- U盘制作-BGA焊接练习
- 【Linux c】 Sqlite3 操作与功能实现
热门文章
- DS18b20温度值换算
- 高端光刻机为什么那么难制造?
- Verilog -- 改进的Booth乘法(基4)
- jk触发器改为四进制_数字电路学习笔记(十):更多锁存器和触发器
- 很多事情都由计算机或机器人来完成英语,八年级下册英语作文6篇
- 电脑突然上不了网,该怎么解决
- Bootstrap系列之卡片(Cards)
- 《Patterns, Principles, and Pract》— chapter14 Introducing the Domain Modeling Building Blocks
- Win8应用开发笔记(1):谈谈Windows RT
- 旋钮编码器c代码_非常稳定的旋转编码器解码程序(C51源代码)