文案摘抄自网络与同事分享。
一、为何要限流:
在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流。本文结合做者的一些经验介绍限流的相关概念、算法和常规的实现方式。redis
缓存
缓存比较好理解,在大型高并发系统中,若是没有缓存数据库将分分钟被爆,系统也会瞬间瘫痪。使用缓存不仅仅可以提高系统访问速度、提升并发访问量,也是保护数据库、保护系统的有效方式。大型网站通常主要是“读”,缓存的使用很容易被想到。在大型“写”系统中,缓存也经常扮演者很是重要的角色。好比累积一些数据批量写入,内存里面的缓存队列(生产消费),以及HBase写数据的机制等等也都是经过缓存提高系统的吞吐量或者实现系统的保护措施。甚至消息中间件,你也能够认为是一种分布式的数据缓存。算法
降级
服务降级是当服务器压力剧增的状况下,根据当前业务状况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行。降级每每会指定不一样的级别,面临不一样的异常等级执行不一样的处理。根据服务方式:能够拒接服务,能够延迟服务,也有时候能够随机服务。根据服务范围:能够砍掉某个功能,也能够砍掉某些模块。总之服务降级须要根据不一样的业务需求采用不一样的降级策略。主要的目的就是服务虽然有损可是总比没有好。数据库
限流
限流能够认为服务降级的一种,限流就是限制系统的输入和输出流量已达到保护系统的目的。通常来讲系统的吞吐量是能够被测算的,为了保证系统的稳定运行,一旦达到的须要限制的阈值,就须要限制流量并采起一些措施以完成限制流量的目的。好比:延迟处理,拒绝处理,或者部分拒绝处理等等。缓存
常见算法:服务器
1.计数器法:
原理:网络
系统维护一个计数器,来一个请求就加1,请求解决完成就减1,当计数器大于指定的阈值,就拒绝新的请求。数据结构
基于这个简单的方法,能够再延伸出少许高级功能,比方阈值能够不是固定值,是动态调整的。另外,还能够有多组计数器分别管理不一样的服务,以保证互不影响等。并发
缺点:app
恶意用户经过在时间窗口的重置节点处突发请求, 能够瞬间超过咱们的速率限制。用户有可能经过算法的这个漏洞,瞬间压垮咱们的应用。分布式
二、队列:
就是基于FIFO队列,全部请求都进入队列,后台程序从队列中取出待解决的请求依次解决。
基于队列的方法,也能够延伸出更多的玩法来,比方能够设置多个队列以配置不一样的优先级。
三、滑动窗口,又称rolling window(队列的升级版)
好比某个服务最多只能每秒钟处理100个请求。咱们能够设置一个1秒钟的滑动窗口,窗口中有10个格子,每一个格子100毫秒,每100毫秒移动一次,每次移动都须要记录当前服务请求的次数。内存中须要保存10次的次数。能够用数据结构LinkedList来实现。格子每次移动的时候判断一次,当前访问次数和LinkedList中最后一个相差是否超过100,若是超过就须要限流了。
当滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。
这种模式的实现的方式更加契合流控的本质意义,理解较为简单。但因为访问量的不可预见性,会发生单位时间的前半段大量请求涌入,然后半段则拒绝全部请求的状况(一般,须要能够将单位时间切的足够的小来缓解);其次,很难肯定这个阈值设置在多少比较合适,只能经过经验或者模拟(如压测)来进行估计,即便是压测也很难估计的准确。集群部署中每台机器的硬件参数不一样,可能致使须要对每台机器的阈值设置的都不尽相同。同一台机子在不一样的时间点的系统压力也不同(好比晚上还有一些任务,或其余的一些业务操做的影响),可以承受的最大阈值也不尽相同,没法考虑的周全。
因此滑窗模式一般适用于对某一资源的保护的需求上,如对db的保护,对某一服务的调用的控制上。
四、漏桶算法
漏桶(Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,漏桶以必定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),而后就拒绝请求,能够看出漏桶算法能强行限制数据的传输速率。
由于漏桶的漏出速率是固定的参数,因此,即便网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发(burst)到端口速率.所以,漏桶算法对于存在突发特性的流量来讲缺少效率
五、令牌桶算法
首先仍是要基于一个队列,请求放到队列里面。但除了队列之外,还要设置一个令牌桶,另外有一个脚本以持续恒定的速度往令牌桶里面放令牌,后台解决程序每解决一个请求就必需从桶里拿出一个令牌,假如令牌拿完了,那就不能解决请求了。咱们能够控制脚本放令牌的速度来达到控制后台解决的速度,以实现动态流控。
1.每秒会有 r 个令牌放入桶中,或者说,每过 1/r 秒桶中增长一个令牌
2.桶中最多存放 b 个令牌,若是桶满了,新放入的令牌会被丢弃
3.当一个 n 字节的数据包到达时,消耗 n 个令牌,而后发送该数据包
4.若是桶中可用令牌小于 n,则该数据包将被缓存或丢弃
Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法(Token Bucket)来完成限流, 将一秒钟切割为令牌数的时间片断,每一个时间片断等同于一个token。很是易于使用.RateLimiter常常用于限制对一些物理资源或者逻辑资源的访问速率.
六、分布式限流
实际生产环境下最快捷且有效的方式是使用RateLimiter实现,可是这很容易踩到一个坑,单节点模式下,使用RateLimiter进行限流一点问题都没有。但线上是分布式系统,布署了多个节点,并且多个节点最终调用的是同一个API/服务商接口。虽然咱们对单个节点能作到将QPS限制在400/s,可是多节点条件下,若是每一个节点均是400/s,那么到服务商那边的总请求就是节点数x400/s,因而限流效果失效。使用该方案对单节点的阈值控制是难以适应分布式环境的。
方式一:redis
@GetMapping("/") public void index(HttpServletResponse response) throws IOException { Jedis jedis = jedisPool.getResource(); String token = RedisRateLimiter.acquireTokenFromBucket(jedis, LIMIT, TIMEOUT); if (token == null) { response.sendError(500); }else{ //TODO 你的业务逻辑 } jedisPool.returnResource(jedis); }
方式二:方式二 Reids+Lua脚本 (可保证操做的原子性)
local key = "rate.limit:" .. KEYS[1] local limit = tonumber(ARGV[1]) local expire_time = ARGV[2] local is_exists = redis.call("EXISTS", key) if is_exists == 1 then if redis.call("INCR", key) > limit then return 0 else return 1 end else redis.call("SET", key, 1) redis.call("EXPIRE", key, expire_time) return 1 end