限流算法(代码片段)

帅性而为1号 帅性而为1号     2023-02-23     165

关键词:

一、限流的作用

由于 API 接口无法控制调用方的行为,因此当遇到瞬时请求量激增时,会导致接口占用过多服务器资源,使得其他请求响应速度降低或是超时,更有甚者可能导致服务器宕机。

限流 (Ratelimiting) 指对应用服务的请求进行限制,例如某一接口的请求限制为 100 个每秒, 对超过限制的请求则进行快速失败或丢弃。

限流可以应对:

  • 热点业务带来的突发请求;
  • 调用方 bug 导致的突发请求;
  • 恶意攻击请求。

因此,对于公开的接口最好采取限流措施。

二、为什么要分布式限流

当应用为单点应用时,只要应用进行了限流,那么应用所依赖的各种服务也都得到了保护。

但线上业务出于各种原因考虑,多是分布式系统,单节点的限流仅能保护自身节点,但无法保护应用依赖的各种服务,并且在进行节点扩容、缩容时也无法准确控制整个服务的请求限制。

而如果实现了分布式限流,那么就可以方便地控制整个服务集群的请求限制,且由于整个集群的请求数量得到了限制,因此服务依赖的各种资源也得到了限流的保护。

三、限流的算法

实现限流有很多办法,在程序中时通常是根据每秒处理的事务数 (Transactionpersecond) 来衡量接口的流量。

本文介绍几种最常用的限流算法:

  • 固定窗口计数器;
  • 滑动窗口计数器;
  • 漏桶;
  • 令牌桶。

1、固定窗口计数器算法

固定窗口计数器算法概念如下:

  • 将时间划分为多个窗口;
  • 在每个窗口内每有一次请求就将计数器加一;
  • 如果计数器超过了限制数量,则本窗口内所有的请求都被丢弃当时间到达下一个窗口时,计数器重置。

固定窗口计数器是最为简单的算法,但这个算法有时会让通过请求量允许为限制的两倍。考虑如下情况:限制 1 秒内最多通过 5 个请求,在第一个窗口的最后半秒内通过了 5 个请求,第二个窗口的前半秒内又通过了 5 个请求。这样看来就是在 1 秒内通过了 10 个请求。

2、滑动窗口计数器算法

滑动窗口计数器算法概念如下:

  • 将时间划分为多个区间;
  • 在每个区间内每有一次请求就将计数器加一维持一个时间窗口,占据多个区间;
  • 每经过一个区间的时间,则抛弃最老的一个区间,并纳入最新的一个区间;
  • 如果当前窗口内区间的请求计数总和超过了限制数量,则本窗口内所有的请求都被丢弃。

滑动窗口计数器是通过将窗口再细分,并且按照时间 " 滑动 ",这种算法避免了固定窗口计数器带来的双倍突发请求,但时间区间的精度越高,算法所需的空间容量就越大。

3、漏桶算法

漏桶算法概念如下:

  • 将每个请求视作 " 水滴 " 放入 " 漏桶 " 进行存储;
  • “漏桶 " 以固定速率向外 " 漏 " 出请求来执行如果 " 漏桶 " 空了则停止 " 漏水”;
  • 如果 " 漏桶 " 满了则多余的 " 水滴 " 会被直接丢弃。

漏桶算法多使用队列实现,服务的请求会存到队列中,服务的提供方则按照固定的速率从队列中取出请求并执行,过多的请求则放在队列中排队或直接拒绝。

漏桶算法的缺陷也很明显,当短时间内有大量的突发请求时,即便此时服务器没有任何负载,每个请求也都得在队列中等待一段时间才能被响应。

4、令牌桶算法

令牌桶算法概念如下:

  • 令牌以固定速率生成;
  • 生成的令牌放入令牌桶中存放,如果令牌桶满了则多余的令牌会直接丢弃,当请求到达时,会尝试从令牌桶中取令牌,取到了令牌的请求可以执行;
  • 如果桶空了,那么尝试取令牌的请求会被直接丢弃。

令牌桶算法既能够将所有的请求平均分布到时间区间内,又能接受服务器能够承受范围内的突发请求,因此是目前使用较为广泛的一种限流算法。

四、代码实现

作为如此重要的功能,在 Java 中自然有很多实现限流的类库,例如 Google 的开源项目 guava 提供了 RateLimiter 类,实现了单点的令牌桶限流。

而分布式限流常用的则有 Hystrix、resilience4j、Sentinel 等框架,但这些框架都需引入第三方的类库,对于国企等一些保守的企业,引入外部类库都需要经过层层审批,较为麻烦。

分布式限流本质上是一个集群并发问题,而 Redis 作为一个应用广泛的中间件,又拥有单进程单线程的特性,天然可以解决分布式集群的并发问题。本文简单介绍一个通过 Redis 实现单次请求判断限流的功能。

1、脚本编写

经过上面的对比,最适合的限流算法就是令牌桶算法。而为实现限流算法,需要反复调用 Redis 查询与计算,一次限流判断需要多次请求较为耗时。因此我们采用编写 Lua 脚本运行的方式,将运算过程放在 Redis 端,使得对 Redis 进行一次请求就能完成限流的判断。

令牌桶算法需要在 Redis 中存储桶的大小、当前令牌数量,并且实现每隔一段时间添加新的令牌。最简单的办法当然是每隔一段时间请求一次 Redis,将存储的令牌数量递增。

但实际上我们可以通过对限流两次请求之间的时间和令牌添加速度来计算得出上次请求之后到本次请求时,令牌桶应添加的令牌数量。因此我们在 Redis 中只需要存储上次请求的时间和令牌桶中的令牌数量,而桶的大小和令牌的添加速度可以通过参数传入实现动态修改。

由于第一次运行脚本时默认令牌桶是满的,因此可以将数据的过期时间设置为令牌桶恢复到满所需的时间,及时释放资源。

编写完成的 Lua 脚本如下:

local ratelimit_info = redis.pcall('HMGET',KEYS[1],'last_time','current_token')
local last_time = ratelimit_info[1]
local current_token = tonumber(ratelimit_info[2])
local max_token = tonumber(ARGV[1])
local token_rate = tonumber(ARGV[2])
local current_time = tonumber(ARGV[3])
local reverse_time = 1000/token_rate
if current_token == nil then
  current_token = max_token
  last_time = current_time
else
  local past_time = current_time-last_time
  local reverse_token = math.floor(past_time/reverse_time)
  current_token = current_token+reverse_token
  last_time = reverse_time*reverse_token+last_time
  if current_token>max_token then
    current_token = max_token
  end
end
local result = 0
if(current_token>0) then
  result = 1
  current_token = current_token-1
end
redis.call('HMSET',KEYS[1],'last_time',last_time,'current_token',current_token)
redis.call('pexpire',KEYS[1],math.ceil(reverse_time*(max_token-current_token)+(current_time-last_time)))
return result

2、执行限流

这里使用 SpringDataRedis 来进行 Redis 脚本的调用。

编写 Redis 脚本类:

public class RedisReteLimitScript implements RedisScript<String> 
   private static final String SCRIPT =
      "local ratelimit_info = redis.pcall('HMGET',KEYS[1],'last_time','current_token') local last_time = ratelimit_info[1] local current_token = tonumber(ratelimit_info[2]) local max_token = tonumber(ARGV[1]) local token_rate = tonumber(ARGV[2]) local current_time = tonumber(ARGV[3]) local reverse_time = 1000/token_rate if current_token == nil then current_token = max_token last_time = current_time else local past_time = current_time-last_time; local reverse_token = math.floor(past_time/reverse_time) current_token = current_token+reverse_token; last_time = reverse_time*reverse_token+last_time if current_token>max_token then current_token = max_token end end local result = '0' if(current_token>0) then result = '1' current_token = current_token-1 end redis.call('HMSET',KEYS[1],'last_time',last_time,'current_token',current_toke  redis.call('pexpire',KEYS[1],math.ceil(reverse_time*(max_tokencurrent_token)+(current_time-last_time))) return result";

  @Override   public String getSha1() 
    return DigestUtils.sha1Hex(SCRIPT);
  

  @Override   public Class<String> getResultType()      return String.class;
  

  @Override   public String getScriptAsString()      return SCRIPT;
  

 

通过 RedisTemplate 对象执行脚本:

public boolean rateLimit(String key, int max, int rate) 
    List<String> keyList = new ArrayList<>(1);
    keyList.add(key);
    return "1".equals(stringRedisTemplate
        .execute(new RedisReteLimitScript(), keyList, Integer.toString(max), Integer.toString(rate),
            Long.toString(System.currentTimeMillis())));
  

rateLimit 方法传入的 key 为限流接口的 ID,max 为令牌桶的最大大小,rate 为每秒钟恢复的令牌数量,返回的 boolean 即为此次请求是否通过了限流。为了测试 Redis 脚本限流是否可以正常工作,我们编写一个单元测试进行测试看看。

@Autowired
  private RedisManager redisManager;

  @Test
  public void rateLimitTest() throws InterruptedException 
    String key = "test_rateLimit_key";
    int max = 10;  // 令牌桶大小
    int rate = 10; // 令牌每秒恢复速度
    AtomicInteger successCount = new AtomicInteger(0);
    Executor executor = Executors.newFixedThreadPool(10);
    CountDownLatch countDownLatch = new CountDownLatch(30);
    for (int i = 0; i < 30; i++) 
      executor.execute(() -> 
        boolean isAllow = redisManager.rateLimit(key, max, rate);
        if (isAllow) 
          successCount.addAndGet(1);
        
        log.info(Boolean.toString(isAllow));
        countDownLatch.countDown();
      );
    
    countDownLatch.await();
    log.info(" 请求成功次 ", successCount.get());
  

 

设置令牌桶大小为 10,令牌桶每秒恢复 10 个,启动 10 个线程在短时间内进行 30 次请求,并输出每次限流查询的结果。日志输出:

[19:12:50,283]true
[19:12:50,284]true
[19:12:50,284]true
[19:12:50,291]true
[19:12:50,291]true
[19:12:50,291]true
[19:12:50,297]true
[19:12:50,297]true
[19:12:50,298]true
[19:12:50,305]true
[19:12:50,305]false
[19:12:50,305]true
[19:12:50,312]false
[19:12:50,312]false
[19:12:50,312]false
[19:12:50,319]false
[19:12:50,319]false
[19:12:50,319]false
[19:12:50,325]false
[19:12:50,325]false
[19:12:50,326]false
[19:12:50,380]false
[19:12:50,380]false
[19:12:50,380]false
[19:12:50,387]false
[19:12:50,387]false
[19:12:50,387]false
[19:12:50,392]false
[19:12:50,392]false
[19:12:50,392]false
[19:12:50,393] 请求成功 11 次

 

可以看到,在 0.1 秒内请求的 30 次请求中,除了初始的 10 个令牌以及随时间恢复的 1 个令牌外,剩下 19 个没有取得令牌的请求均返回了 false,限流脚本正确的将超过限制的请求给判断出来了,业务中此时就可以直接返回系统繁忙或接口请求太过频繁等提示。

3、开发中遇到的问题

1)Lua 变量格式

Lua 中的 String 和 Number 需要通过 tonumber() 和 tostring() 进行转换。

2)Redis 入参

Redis 的 pexpire 等命令不支持小数,但 Lua 的 Number 类型可以存放小数,因此 Number 类型传递给 Redis 时最好通过 math.ceil() 等方式转换以避免存在小数导致命令失败。

3)Time 命令

由于 Redis 在集群下是通过复制脚本及参数到所有节点上,因此无法在具有不确定性的命令后面执行写入命令,因此只能请求时传入时间而无法使用 Redis 的 Time 命令获取时间。

3.2 版本之后的 Redis 脚本支持 redis.replicate_commands(),可以改为使用 Time 命令获取当前时间。

4)潜在的隐患

由于此 Lua 脚本是通过请求时传入的时间做计算,因此务必保证分布式节点上获取的时间同步,如果时间不同步会导致限流无法正常运作。

作者介绍

段然,甜橙金融创新中心开发工程师,目前负责公司平台化建设及媒介能力聚合。

原文链接

https://mp.weixin.qq.com/s/qb3rg_ZpcMcvyaIRsvc1fw

图解+代码|常见限流算法以及限流在单机分布式场景下的思考(代码片段)

大家好,我是yes。今天来说说限流的相关内容,包括常见的限流算法、单机限流场景、分布式限流场景以及一些常见限流组件。当然在介绍限流算法和具体场景之前我们先得明确什么是限流,为什么要限流?。任何技术都要搞清... 查看详情

常见的几种限流算法代码实现(java)(代码片段)

最近在学习Sentinel组件需要了解限流算法相关的知识,正好在微信公众号上看到了一篇不错的文章,在此记录一下以下是原文链接。年轻人,来手撸几种常见的限流算法!限流算法接口publicinterfaceRateLimiter/***判断... 查看详情

常见的几种限流算法代码实现(java)(代码片段)

最近在学习Sentinel组件需要了解限流算法相关的知识,正好在微信公众号上看到了一篇不错的文章,在此记录一下以下是原文链接。年轻人,来手撸几种常见的限流算法!限流算法接口publicinterfaceRateLimiter/***判断... 查看详情

详解4种经典的限流算法(代码片段)

文章目录引言1.限流是什么?2.固定窗口限流算法3.滑动窗口限流算法4.漏桶算法5.令牌桶算法6.Nginx限流7.总结引言瞬时流量过高,服务被压垮?恶意用户高频光顾,导致服务器宕机?消息消费过快,导致数据库压... 查看详情

详解4种经典的限流算法(代码片段)

文章目录引言1.限流是什么?2.固定窗口限流算法3.滑动窗口限流算法4.漏桶算法5.令牌桶算法6.Nginx限流7.总结引言瞬时流量过高,服务被压垮?恶意用户高频光顾,导致服务器宕机?消息消费过快,导致数据库压... 查看详情

nginx限流配置(代码片段)

限流算法令牌桶算法算法思想是:令牌以固定速率产生,并缓存到令牌桶中;令牌桶放满时,多余的令牌被丢弃;请求要消耗等比例的令牌才能被处理;令牌不够时,请求被缓存。漏桶算法算法思想是:水(请求)从上方倒入水... 查看详情

常见的限流算法与实现(代码片段)

限流的实现常见的限流算法:限流是对某一时间窗口内的请求数进行限制,保持系统的可用性和稳定性,防止因流量暴增而导致的系统运行缓慢或宕机。常见的限流算法有三种:计数器限流(固定窗口)原理:时... 查看详情

限流算法之漏桶算法令牌桶算法(代码片段)

...imiter是Guava的concurrent包下的一个用于限制访问频率的类.1.限流每个API接口都是有访问上限的,当访问频率或者并发量超过其承受范围时候,我们就必须考虑限流来保证接口的可用性或者降级可用性.即接口也需要安装上保险丝,以防... 查看详情

限流算法之计数器算法(代码片段)

计数器法是限流算法里最简单也是最容易实现的一种算法。假设一个接口限制一分钟内的访问次数不能超过100个,维护一个计数器,每次有新的请求过来,计数器加一,这时候判断,如果计数器的值小于限流... 查看详情

限流算法之计数器算法(代码片段)

计数器法是限流算法里最简单也是最容易实现的一种算法。假设一个接口限制一分钟内的访问次数不能超过100个,维护一个计数器,每次有新的请求过来,计数器加一,这时候判断,如果计数器的值小于限流... 查看详情

nginx浅析4-限流(秒杀,高并发)(代码片段)

限流:控制单位时间内用户访问服务器的次数限流方式:控制速率控制并发连接数限流控制速率算法:令牌桶算法漏桶算法1.令牌桶算法 算法思路: 令牌以固定速率产生,放到令牌桶中令牌桶放满时,多余令... 查看详情

php对api接口做限流(代码片段)

阅读目录什么是接口限流接口限流的常用算法计数器法代码如下漏桶算法(LeakyBucket)简单的算法实现令牌桶算法(TokenBucket)使用redis的队列作为令牌桶容器TokenBucket.php令牌的假如与消耗什么是接口限流顾名思义,限流就是限制流量... 查看详情

php对api接口做限流(代码片段)

阅读目录什么是接口限流接口限流的常用算法计数器法代码如下漏桶算法(LeakyBucket)简单的算法实现令牌桶算法(TokenBucket)使用redis的队列作为令牌桶容器TokenBucket.php令牌的假如与消耗什么是接口限流顾名思义,限流就是限制流量... 查看详情

常用限流算法与guavaratelimiter源码解析(代码片段)

在分布式系统中,应对高并发访问时,缓存、限流、降级是保护系统正常运行的常用方法。当请求量突发暴涨时,如果不加以限制访问,则可能导致整个系统崩溃,服务不可用。同时有一些业务场景,比如短信验证码,或者其它... 查看详情

限流算法介绍(转)(代码片段)

...高并发系统时,有三把利器用来保护系统:缓存、降级和限流。服务限流:  流量控制本质上是减小访问量,而服务处理能力不变;而服务降级本质上是降低了部分服务的处理能力,增强另一部分服务处理能力,而访问量不变... 查看详情

限流接口限流(代码片段)

...爆发的访问量造成系统奔溃,我们需要最这样的接口进行限流。在上一篇“限流算法”中,我们简单提到了两种限流方式:1)(令牌桶、漏桶算法)限速率,例如:每5r/1s=1r/200ms即一个请求以200毫秒的速率来执行;2)(计数器... 查看详情

限流算法,以golang方式(代码片段)

限流算法,以Golang方式速率限制在WebServer、TCP通讯、API交互等领域中,速率限制,RateLimit,一般是面向请求次数、流量等参数进行速率控制。有的时候它又被称作流量控制。谈论流量控制时,大抵上要考虑到如下两... 查看详情

resilience4j源码解析:限流模块ratelimiter与常见限流算法(代码片段)

目录1、概览:限流模块RateLimiter2、常见限流算法3、Resilience4j限流算法4、小结Resilience4j源码解析(1):简介及调试环境搭建Resilience4j源码解析(2):浅析框架设计原理Resilience4j源码解析(3)&#x... 查看详情