限流——漏桶算法和令牌桶算法的区别

文章目录

  • 限流
  • 漏桶算法
  • 令牌桶算法
  • 漏桶算法和令牌桶算法的区别
  • 时间窗口
  • 时间窗口和令牌桶优缺点

限流

在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流

  • 缓存:缓存的目的是提升系统访问速度和增大系统处理能力
  • 降级:当服务流量剧增,影响到核心流程的性能,需要暂时屏蔽掉一些功能,待高峰过去或问题解决后再重新打开,以此释放服务器资源以保证核心任务的正常运行。
  • 限流:限流的目的是通过对并发访问或请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、或排队或等待、降级等处理

常用的限流算法有令牌桶和,漏桶,滑动窗口

漏桶算法

漏桶(Leaky Bucket) 算法思路简单,水(请求)先进入到漏桶中,漏桶以一定的速度出水(接口有响应速率),当水流速度过大直接溢出(访问频率超过接口响应频率),然后就拒绝请求,可以看出漏桶算法能强制限制数据的传输速率。

限流——漏桶算法和令牌桶算法的区别_第1张图片

令牌桶算法

对于很多应用场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。如图2所示,令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。
限流——漏桶算法和令牌桶算法的区别_第2张图片
相关实现算法
参考链接:redis令牌桶实现

漏桶算法和令牌桶算法的区别

**漏桶算法的出水速度是恒定的,**那么意味着如果瞬时大流量的话,将有大部分请求被丢弃掉(也就是所谓的溢出)。漏桶算法通常可以用于限制访问外部接口的流量,保护其他人系统,比如我们请求银行接口,通常要限制并发数。
令牌桶算法生成令牌的速度是恒定的,而请求去拿令牌是没有速度限制的。这意味,面对瞬时大流量,该算法可以在短时间内请求拿到大量令牌,可以处理瞬时流量,而且拿令牌的过程并不是消耗很大的事情。令牌桶算法通常可以用于限制被访问的流量,保护自身系统。

时间窗口

时间窗口,就是以时间间隔维度,统计这个时间段内的数量,和设置的阈值比较,达到限流效果。
如何区分各个时间窗口?
可以将“当前的毫秒数/时间区间“”,那么同一个时间区间计算的结果都是一样的,可以将这个值作为key,用来区分各个时间段。
比如: 用"当前的毫秒数/1000",就表示每一秒,用"当前的毫秒数/5000",就表示每5秒。
测试每一秒:从结果可以发现,同一秒内的毫秒数值,计算后得到的结果是一样的,可以将这个结果作为统计交易的key,用来区分各个时间段。
限流——漏桶算法和令牌桶算法的区别_第3张图片
测试每3秒:可以发现每3秒内的输出值,都是一样的。
限流——漏桶算法和令牌桶算法的区别_第4张图片
再来看限流实现,用Redis + Lua
Lua本身就是一种编程语言,虽然redis 官方没有直接提供限流相应的API,但却支持了 Lua 脚本的功能,可以使用它实现复杂的令牌桶或漏桶算法,也是分布式系统中实现限流的主要方式之一。

相比Redis事务,Lua脚本的优点:
减少网络开销:使用Lua脚本,无需向Redis发送多次请求,执行一次即可,减少网络传输
原子操作:Redis将整个Lua脚本作为一个命令执行,原子,无需担心并发
复用:Lua脚本一旦执行,会永久保存 Redis 中,其他客户端可复用

local key = KEYS[1] --限流KEY(一秒一个)
local limit = tonumber(ARGV[1])        --限流大小
local current = tonumber(redis.call('get', key) or "0")
if current + 1 > limit then --如果超出限流大小
    redis.call("INCRBY", key,"1") -- 如果不需要统计真是访问量可以不加这行
    return 0
else  --请求数+1,并设置2秒过期
    redis.call("INCRBY", key,"1")
    if tonumber(ARGV[2]) > -1 then
        redis.call("expire", key,tonumber(ARGV[2])) --时间窗口最大时间后销毁键
    end
    return 1
end

lua脚本返回值比较奇怪,用java客户端接受返回值,只能使用Long,没有去深究。这个脚本只需要传入key(url+时间戳/预设时间窗口大小),便可以实现限流。
这里也贴下java中配套的工具类

package sinosoftgz.apiGateway.utils;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.util.Assert;

import java.util.Arrays;

/**
 * Created by xujingfeng on 2017/3/13.
 * 

* 基于redis lua脚本的线程安全的计数器限流方案 *

*/
public class RedisRateLimiter { /** * 限流访问的url */ private String url; /** * 单位时间的大小,最大值为 Long.MAX_VALUE - 1,以秒为单位 */ final Long timeUnit; /** * 单位时间窗口内允许的访问次数 */ final Integer limit; /** * 需要传入一个lua script,莫名其妙redisTemplate返回值永远是个Long */ private RedisScript<Long> redisScript; private RedisTemplate redisTemplate; /** * 配置键是否会过期, * true:可以用来做接口流量统计,用定时器去删除 * false:过期自动删除,时间窗口过小的话会导致键过多 */ private boolean isDurable = false; public void setRedisScript(RedisScript<Long> redisScript) { this.redisScript = redisScript; } public void setRedisTemplate(RedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public boolean isDurable() { return isDurable; } public void setDurable(boolean durable) { isDurable = durable; } public RedisRateLimiter(Integer limit, Long timeUnit) { this.timeUnit = timeUnit; Assert.isTrue(timeUnit < Long.MAX_VALUE - 1); this.limit = limit; } public RedisRateLimiter(Integer limit, Long timeUnit, boolean isDurable) { this(limit, timeUnit); this.isDurable = isDurable; } public boolean acquire() { return this.acquire(this.url); } public boolean acquire(String url) { StringBuffer key = new StringBuffer(); key.append("rateLimiter").append(":") .append(url).append(":") .append(System.currentTimeMillis() / 1000 / timeUnit); // 这里System.currentTimeMillis() / 1000 / timeUnit就是一个时间窗口,比如 5秒 内处理10个请求,timeUnit=5,limit=10 Integer expire = limit + 1; String convertExpire = isDurable ? "-1" : expire.toString(); return redisTemplate.execute(redisScript, Arrays.asList(key.toString()), limit.toString(), convertExpire).equals(1l); } }

时间窗口和令牌桶优缺点

优点:时间窗口和令牌桶相比,这种算法不需要去等待令牌生成的时间,在新的时间窗口,可以立即处理大量的请求。
缺点:在一个窗口临界点的前后时间,比如时间窗口是1分钟,在59秒和1分01秒同时突发大量请求,刚好请求全部落在前后相隔很近的节点上,极端情况下可能会带来 2 倍的流量,系统可能承受不了这么大的突发性流量。

你可能感兴趣的:(并发,限流)