在高并发的分布式系统,如大型电商系统中,因为接口 API 没法控制上游调用方的行为,所以当瞬间请求量突增时,会致使服务器占用过多资源,发生响应速度下降、超时乃至宕机,甚至引起雪崩形成整个系统不可用。java
面对这种状况,一方面咱们会提高 API 的吞吐量和 QPS(Query Per Second 每秒查询量),但总归会有上限,因此另外一方面为了应对巨大流量的瞬间提交,咱们须要作对应的限流处理,也就是对请求量进行限制,对于超出限制部分的请求做出快速拒绝、快速失败、丢弃处理,以保证本服务以及下游资源系统的稳定。redis
常见的限流算法有计数器、漏斗、令牌桶。算法
计数器限流方式比较粗暴,一次访问就增长一次计数,在系统内设置每 N 秒的访问量,超过访问量的访问直接丢弃,从而实现限流访问。具体大概是如下步骤:数组
这种算法的弊端是,在开始的时间,访问量被使用完后,1 s 内会有很长时间的真空期是处于接口不可用的状态的,同时也有可能在一秒内出现两倍的访问量。服务器
实现方式和扩展方式不少,这里以 Redis 举例简单的实现,计数器主要思路就是在单位时间内,有且仅有 N 数量的请求可以访问个人代码程序。因此能够利用 Redis 的 setnx
来实现这方面的功能。网络
好比如今须要在 10 秒内限定 20 个请求,那么能够在 setnx
的时候设置过时时间 10,当请求的 setnx
数量达到 20 的时候即达到了限流效果。数据结构
滑动窗口计数法的思路是:并发
利用 Redis 的 list 数据结构能够垂手可得地实现该功能。咱们能够将请求打形成一个 zset 数组,当每一次请求进来的时候,key 保持惟一,value 能够用 UUID 生成,而 score 能够用当前时间戳表示,由于 score 咱们能够用来计算当前时间戳以内有多少的请求数量。而 zset 数据结构也提供了 range 方法让咱们能够很轻易地获取到两个时间戳内有多少请求。负载均衡
public Response limitFlow() { Long currentTime = new Date().getTime(); if (redisTemplate.hasKey("limit")) { Integer count = redisTemplate.opsForZset().rangeByScore("limit", currentTime - intervalTime, currentTime).size(); if (count != null && count > 5) { return Response.ok("每分钟最多只能访问 5 次!"); } } redisTemplate.opsForZSet().add("limit", UUID.randomUUID().toString(), currentTime); return Response.ok("访问成功"); }
经过上述代码能够作到滑动窗口的效果,而且能保证每 N 秒内至多 M 个请求,实现方式相对来讲也是比较简单的,可是所带来的缺点就是 zset 的数据结构会愈来愈大。dom
在计数器算法中咱们看到,当使用了全部的访问量后,接口会彻底处于不可用状态,有些系统不能接受这样的处理方式,对此可使用漏斗算法进行限流,漏斗算法的原理就像名字,访问量从漏斗的大口进入,从漏斗的小口进入系统。这样不论是多大的访问量进入漏斗,最后进入系统的访问量都是固定的。漏斗的好处就是,大批量访问进入时,漏斗有容量,不超过容量(容量的设计=固定处理的访问量 * 可接受等待时长)的数据均可以排队等待处理,超过的才会丢弃。
实现方式可使用队列,队列设置容量,访问能够大批量塞入队列,满队列后丢弃后续访问量。队列的出口以固定速率拿去访问量处理。
这种方案因为出口速率是固定的,因此并无办法应对短期的突发流量。
令牌桶算法是漏斗算法的改进版,为了处理短期的突发流量而作了优化,令牌桶算法主要由三部分组成:令牌流
、数据流
、令牌桶
。
名词释义:
令牌桶会按照必定的速率生成令牌放入令牌桶,访问要进入系统时,须要从令牌桶中获取令牌,有令牌的能够进入,没有的被抛弃,因为令牌桶的令牌是源源不断生成的,当访问量小时,能够留存令牌达到令牌桶的上限,这样当短期的突发访问量时,积累的令牌数能够处理这个问题。当访问量持续大量流入时,因为生成令牌的速率是固定的,最后也就变成了相似漏斗算法的固定流量处理。
实现方式和漏斗也比较相似,可使用一个队列保存令牌,一个定时任务用等速率生成令牌放入队列,访问量进入系统时,从队列获取令牌再进入系统。
google
开源的 guava
包中的 RateLimiter
类实现了令牌桶算法,不一样其实现方式是单机的,集群能够按照上面的实现方式,队列使用中间件 MQ 实现,配合负载均衡算法,考虑集群各个服务器的承压状况作对应服务器的队列是较好的作法。
这里简单用 Redis 以及定时任务模拟大概的过程:
首先依靠 List 的 leftPop 来获取令牌:
// 输出令牌 public Response limitFlow() { Object result = redisTemplate.opsForList().leftPop("limit_list"); if (result == null) { return Response.ok("当前令牌桶中无令牌!"); } return Response.ok("访问成功!"); }
再依靠 Java 的定时任务,定时往 List 中 rightPush 令牌,固然令牌也须要保证惟一性,因此这里利用 UUID 生成:
// 10S的速率往令牌桶中添加UUID,只为保证惟一性 @Scheduled(fixedDelay = 10_000,initialDelay = 0) public void setIntervalTimeTask(){ redisTemplate.opsForList().rightPush("limit_list",UUID.randomUUID().toString()); }
单点应用下,对应用进行限流,既能知足本服务的需求,又能够很好地保护好下游资源。在选型上,能够采用上面说起的 Google Guava 的 RateLimiter。
而在多机部署的场景下,对单点的限流,并不能达到咱们想要的最好效果,须要引入分布式限流。分布式限流的算法,依然能够采用令牌桶算法,只不过将令牌桶的发放、存储改成全局的模式。
在真实应用场景,能够采用 redis + lua 的方式,经过把逻辑放在 redis 端,来减小调用次数。
lua 的逻辑以下:
文章内容收集于网络。