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

限流

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

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

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

漏桶算法

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

在这里插入图片描述

令牌桶算法

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

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

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

时间窗口

时间窗口,就是以时间间隔维度,统计这个时间段内的数量,和设置的阈值比较,达到限流效果。
如何区分各个时间窗口?
可以将“当前的毫秒数/时间区间“”,那么同一个时间区间计算的结果都是一样的,可以将这个值作为key,用来区分各个时间段。
比如: 用"当前的毫秒数/1000",就表示每一秒,用"当前的毫秒数/5000",就表示每5秒。
测试每一秒:从结果可以发现,同一秒内的毫秒数值,计算后得到的结果是一样的,可以将这个结果作为统计交易的key,用来区分各个时间段。
在这里插入图片描述
测试每3秒:可以发现每3秒内的输出值,都是一样的。
在这里插入图片描述
再来看限流实现,用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.
 * <p>
 * 基于redis lua脚本的线程安全的计数器限流方案
 * </p>
 */
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 倍的流量,系统可能承受不了这么大的突发性流量。

  • 8
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

EmineWang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值