分布式环境下限流方案的思考

2024-01-12 19:48

本文主要是介绍分布式环境下限流方案的思考,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 1 前言
  • 2 为什么要对Api接口限流
  • 3 实际场景中常用的限流策略
    • 3.1 场景
    • 3.2 服务接口的流量控制策略
    • 3.3 实际场景中常用的限流策略
    • 3.4 高并发系统下业务端常见的限流方式
  • 4 常见的限流算法
    • 4.1 漏桶算法
    • 4.2 令牌桶算法
    • 4.3 令牌桶算法和漏桶算法比较
    • 4.4 计数器算法
  • 5 限流场景实践
    • 5.1 计数器算法实现限流(限流算法)(适用于分布式环境,推荐使用)
    • 5.2 令牌桶算法实现限流(限流算法)
    • 5.3 漏桶算法实现限流(限流算法)
    • 5.4 应用级限流(适用于单应用)
      • 5.4.1 限流总并发/连接/请求数
      • 5.4.2 限流总资源数
      • 5.4.3 限流某个接口的总并发/请求数
      • 5.4.4 限流某个接口的时间窗请求数
      • 5.4.5 平滑限流某个接口的请求数
        • 5.4.5.1 平滑突发限流(SmoothBursty)
        • 5.4.5.2 平滑预热限流(SmoothWarmingUp)
    • 5.5 分布式限流
      • 5.5.1 基于Redis+Lua脚本限流
      • 5.5.2 使用Nginx+Lua实现限流
    • 5.6 接入层限流(Nginx层进行限制)

1 前言

随着时代的发展,互联网也发生了巨大的变化。其中重要的一个变化时,为了应对高流量,服务的架构从集中式架构,演变成了分布式架构

什么是分布式架构?
简单来说,就是之前的一个单体应用(后台管理系统),通过拆分,拆分成用户中心、产品中心、客户中心等多个小应用服务,这种把一个大的单体应用项目,拆分成多个小应用项目的方式,就是分布式系统应用架构

本文将以一种最高效的方式,实现分布式环境下,接口方法的限流。

2 为什么要对Api接口限流

有时候高流量下,系统难以支撑,可能会导致服务不可用,所以为了保证服务的高可用性,有时候限流就是必要的。虽然降低了服务接口的访问频率和并发量,却换取服务接口和业务应用系统的高可用。

  • 为了保证系统服务的可用性

  • 为了满足各种应用场景,有时候不得不对接口Api进行限流。比如说:短信服务,供应商可能会要求每秒访问不超过400条,如果超过了这个访问量,请求就会被供应商拒绝,从而导致漏发短信。

  • 还有的接口,第三方Api会做限制,他们为了限制访问,设定一分钟只能请求接口20次,超过了就会超时或者响应异常。

鉴于业务方对短信发送或者其他第三方接口的调用频率未知,而服务商的接口服务有上限,为保证服务的可用性,业务层需要对接口调用方的流量进行限制,即:接口限流

总而言之,限流,在好多场景用的还是挺多的。

3 实际场景中常用的限流策略

3.1 场景

很多做服务接口的人或多或少的遇到这样的场景,由于业务应用系统的负载能力有限,为了防止非预期的请求对系统压力过大而拖垮业务应用系统。

也就是面对大流量时,如何进行流量控制?

3.2 服务接口的流量控制策略

在开发高并发系统时有三把利器用来保护系统:缓存降级限流。所以,服务接口的流量控制策略,主要有分流降级限流等

  • 缓存的作用
    缓存的目的是提升系统访问速度增大系统能处理的容量,可谓是抗高并发流量的银弹

  • 降级的作用
    降级是当服务出问题或者影响到核心流程的性能则需要暂时屏蔽掉,待高峰或者问题解决后再打开

  • 限流的场景以及作用
    有些场景并不能用缓存和降级来解决,比如稀缺资源(秒杀、抢购)、写服务(如评论、下单)、频繁的复杂查询(评论的最后几页),因此需有一种手段来限制这些场景的并发/请求量,即限流。

    限流的目的是通过对并发访问/请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务(定向到错误页或告知资源没有了)、排队或等待(比如秒杀、评论、下单)、降级(返回兜底数据或默认数据,如商品详情页库存默认有货)。

本文将讨论下限流策略,限流,虽然降低了服务接口的访问频率和并发量,却换取服务接口和业务应用系统的高可用

3.3 实际场景中常用的限流策略

  • Nginx前端限流
    按照一定的规则如帐号、IP、系统调用逻辑等在Nginx层面做限流

  • 业务应用系统限流
    通过客户端限流或者服务端限流

  • 数据库限流
    红线区,力保数据库

3.4 高并发系统下业务端常见的限流方式

一般开发高并发系统常见的限流有:

  • 限制总并发数(比如数据库连接池、线程池)
  • 限制瞬时并发数(如nginx的limit_conn模块,用来限制瞬时并发连接数)
  • 限制时间窗口内的平均速率(如Guava的RateLimiter、nginx的limit_req模块,限制每秒的平均速率)
  • 其他还有如限制远程接口调用速率限制MQ的消费速率
  • 另外还可以根据网络连接数、网络流量、CPU或内存负载等来限流

先有缓存这个银弹,后有限流来应对618、双十一高并发流量,在处理高并发问题上可以说是如虎添翼,不用担心瞬间流量导致系统挂掉或雪崩,最终做到有损服务而不是不服务。限流需要评估好,不可乱用,否则会正常流量出现一些奇怪的问题而导致用户抱怨。

在实际应用时也不要太纠结算法问题,因为一些限流算法实现是一样的只是描述不一样。具体使用哪种限流技术还是要根据实际场景来选择,不要一味去找最佳模式,白猫黑猫只要能解决问题的猫就是好猫。

因在实际工作中需要进行限流,因此本文会详细介绍各种限流手段。那么接下来我们从限流算法应用级限流分布式限流接入层限流(Nginx层进行限制)来详细了解下限流技术的一些手段。

4 常见的限流算法

常见的限流算法有:令牌桶算法漏桶算法计数器算法也可以进行粗暴限流实现。

4.1 漏桶算法

漏桶作为计量工具(The Leaky Bucket Algorithm as a Meter)时,可以用于流量整形(Traffic Shaping)流量控制(TrafficPolicing)

漏桶(Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求或者跳转失败(限流等待)页面,可以看出,漏桶算法能强行限制数据的传输速率

漏桶算法示意图如下:
在这里插入图片描述
由上图可见,这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(即速率rate)。

因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能改变流量突发(burst)到端口的速率。因此,漏桶算法对于存在突发特性的高流量来说,效率不是很高

漏桶算法的描述总结如下:

  • 一个固定容量的漏桶,按照常量固定速率流出水滴
  • 如果桶是空的,则不需流出水滴
  • 可以以任意速率流入水滴到漏桶
  • 如果流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的

4.2 令牌桶算法

令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。令牌桶算法(Token Bucket)是和漏桶(Leaky Bucket)算法效果一样,但是方向相反的算法,更加容易理解。

随着时间流逝,系统会按恒定1/QPS1 时间间隔(如果QPS=100,则间隔是10ms),往桶里加入Token 令牌(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了。当新请求来临时,会各自拿走一个Token,如果没有Token可拿了就阻塞或者拒绝服务。

在这里插入图片描述
令牌桶的另外一个好处是可以方便的改变速度,一旦需要提高速率,则按需提高放入桶中的令牌的速率。 一般会定时(比如100毫秒)往桶中增加一定数量的令牌,有些变种算法,则实时的计算应该增加的令牌的数量。

令牌桶算法的描述总结如下:

  • 假设限制2r/s(每秒2个),则按照500毫秒的固定速率往桶中添加令牌
  • 桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃或拒绝
  • 当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上。如果桶中的令牌不足n个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么缓冲区等待)

4.3 令牌桶算法和漏桶算法比较

  • 令牌桶是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时则拒绝新的请求。

    漏桶则是按照常量固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝。

  • 令牌桶限制的是平均流入速率(允许突发请求,只要有令牌就可以处理,支持一次拿3个令牌,4个令牌),并允许一定程度突发流量。

    漏桶限制的是常量流出速率(即流出速率是一个固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2),从而平滑突发流入速率。

  • 令牌桶允许一定程度的突发,而漏桶主要目的是平滑流入速率

  • 两个算法实现可以一样,但是方向是相反的,对于相同的参数得到的限流效果是一样的。

4.4 计数器算法

有时候我们还使用计数器算法来进行限流,主要用来限制总并发数,比如数据库连接池线程池秒杀的并发数。主要对全局总请求数或者一定时间段的总请求数设定的阀值来进行限流,是简单粗暴的总数量限流,而不是平均速率限流。常用于限定某个Api在一定时间内限定调用的次数。

到此基本的算法就介绍完了,接下来我们来看一下限流实践。

5 限流场景实践

5.1 计数器算法实现限流(限流算法)(适用于分布式环境,推荐使用)

设计思路: 借助于RedisINCR操作来实现Limit限流

假设一个用户(用IP判断或者其他业务参数)每分钟访问某一个服务接口的次数,或者一个API方法每分钟调用第三方供应商的接口,调用次数不能超过100次,那么我们可以在Redis中创建一个全局唯一的键key,并设置键的过期时间为60秒,当对此服务接口访问时就把键值加1,在60秒内当键值增加到10的时候,就禁止访问服务接口。在某种场景中添加访问时间间隔还是很有必要的。

具体的示例,我在另一篇博文中进行了详细地介绍,请参考博文:分布式环境下,基于Redis实现Restful API接口的限流

5.2 令牌桶算法实现限流(限流算法)

更新中…

5.3 漏桶算法实现限流(限流算法)

更新中…

5.4 应用级限流(适用于单应用)

5.4.1 限流总并发/连接/请求数

对于一个应用系统来说一定会有极限并发/请求数,即总有一个TPS/QPS阀值,如果超了阀值则系统就会不响应用户的请求或者响应的非常慢,因此我们最好进行过载保护,防止大量请求涌入击垮系统

  • TPS
    TPS(Transactions Per Second)(每秒传输的事物处理个数),即服务器每秒处理的事务数。TPS包括一条消息入和一条消息出,加上一次用户数据库访问。(业务TPS = CAPS × 每个呼叫平均TPS)

    TPS是软件测试结果的测量单位。一个事务是指一个客户机向服务器发送请求然后服务器做出反应的过程。客户机在发送请求时开始计时,收到服务器响应后结束计时,以此来计算使用的时间和完成的事务个数。一般的,评价系统性能均以每秒钟完成的技术交易的数量来衡量。系统整体处理能力取决于处理能力最低模块的TPS值

  • QPS
    QPS表示每秒查询率,QPS是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准,在因特网上,作为域名系统服务器的机器的性能经常用每秒查询率来衡量。对应fetches/sec,即每秒的响应请求数,也即是最大吞吐能力。

5.4.2 限流总资源数

如果有的资源是稀缺资源(如数据库连接、线程),而且可能有多个系统都会去使用它,那么需要限制应用。可以使用池化技术来限制总资源数,比如说: 连接池、线程池等。比如分配给每个应用的数据库连接是100,那么本应用最多可以使用100个资源,超出了进入等待或者抛异常。

5.4.3 限流某个接口的总并发/请求数

如果接口可能会有突发访问情况,但又担心访问量太大造成崩溃,如抢购业务。这个时候就需要限制这个接口的总并发/请求数总请求数了,因为粒度比较细,可以为每个接口都设置相应的阀值。

可以使用Java中的AtomicLong进行限流:

try {if(atomic.incrementAndGet() > 限流数) {//拒绝请求}//处理请求
} finally {atomic.decrementAndGet();
}

适合对业务无损的服务或者需要过载保护的服务进行限流,如抢购业务,超出了大小要么让用户进入队列排队,要么告诉用户没货了,对用户来说是可以接受的。而一些开放平台也会限制用户调用某个接口的试用请求量,也可以用这种计数器方式实现。这种方式也是简单粗暴的限流,没有平滑处理,需要根据实际情况选择使用。

5.4.4 限流某个接口的时间窗请求数

即一个时间窗口内的请求数,如想限制某个接口/服务每秒/每分钟/每天的请求数/调用量。如一些基础服务会被很多其他系统调用,比如商品详情页服务会调用基础商品服务调用,但是怕因为更新量比较大将基础服务打挂,这时我们要对每秒/每分钟的调用量进行限速。

一种实现方式如下所示:

LoadingCache<Long, AtomicLong> counter =CacheBuilder.newBuilder().expireAfterWrite(2, TimeUnit.SECONDS).build(new CacheLoader<Long, AtomicLong>() {@Overridepublic AtomicLong load(Long seconds) throws Exception {return new AtomicLong(0);}});
long limit = 1000;
while(true) {//得到当前秒long currentSeconds = System.currentTimeMillis() / 1000;if(counter.get(currentSeconds).incrementAndGet() > limit) {System.out.println("限流了:" + currentSeconds);continue;}//业务处理
}

我们使用Guava的LoadingCache来存储计数器,过期时间设置为2秒(保证1秒内的计数器是有的),然后我们获取当前时间戳然后取秒数来作为Key进行计数统计和限流,这种方式也是简单粗暴,对于刚才说的场景来说,足够用了。

5.4.5 平滑限流某个接口的请求数

之前的限流方式都不能很好地应对突发请求,即瞬间请求可能都被允许从而导致一些问题。因此,在一些场景中需要对突发请求进行整形,整形为平均速率请求处理(比如5r/s,则每隔200毫秒处理一个请求,平滑了速率)。

这个时候有两种算法满足我们的场景: 令牌桶算法漏桶算法Guava框架提供了令牌桶算法实现,我们可以直接拿来使用。

Guava RateLimiter提供了令牌桶算法实现:平滑突发限流(SmoothBursty)平滑预热限流(SmoothWarmingUp)实现。

5.4.5.1 平滑突发限流(SmoothBursty)

代码示例

/*** <p>* 平滑限流某个接口的请求数* <p/>** @author smilehappiness* @Date 2020/7/7 21:10*/
public class GuavaRateLimiter {public static void main(String[] args) {test1();
//        test2();
//        test3();}/*** <p>*  0.0* 获取前10个令牌耗时1:20* 1.996686* 获取第11个令牌耗时2:2019* 0.197969* <p/>** @param* @return void* @Date 2020/7/7 21:26*/private static void test3() {long startTime = System.currentTimeMillis();//同上边的例子2类似,第一秒突发了10个请求,令牌桶算法也允许了这种突发(允许消费未来的令牌),但接下来的limiter.acquire(1)将等待差不多2秒桶中才能有令牌,且接下来的请求也整形为固定速率了。RateLimiter limiter = RateLimiter.create(5);System.out.println(limiter.acquire(10));System.out.println("获取前10个令牌耗时1:" + (System.currentTimeMillis() - startTime));System.out.println(limiter.acquire(1));System.out.println("获取第11个令牌耗时2:" + (System.currentTimeMillis() - startTime));System.out.println(limiter.acquire(1));}/*** <p>*  0.0*  0.99661*  耗时1:1022*  0.197849*  耗时2:1221* <p/>** @param* @return void* @Date 2020/7/7 21:22*/private static void test2() {long startTime = System.currentTimeMillis();//另一个突发请求示例RateLimiter limiter = RateLimiter.create(5);//limiter.acquire(5)表示桶的容量为5且每秒新增5个令牌,令牌桶算法允许一定程度的突发,所以可以一次性消费5个令牌,但接下来的limiter.acquire(1)将等待差不多1秒桶中才能有令牌,且接下来的请求也整形为固定速率了(200毫秒)。System.out.println(limiter.acquire(5));System.out.println(limiter.acquire(1));System.out.println("耗时1:" + (System.currentTimeMillis() - startTime));System.out.println(limiter.acquire(1));System.out.println("耗时2:" + (System.currentTimeMillis() - startTime));}private static void test1() {//表示桶容量为5且每秒新增5个令牌,即每隔200毫秒新增一个令牌RateLimiter limiter = RateLimiter.create(5);//limiter.acquire()表示消费一个令牌System.out.println(limiter.acquire());System.out.println(limiter.acquire());System.out.println(limiter.acquire());System.out.println(limiter.acquire());System.out.println(limiter.acquire());System.out.println(limiter.acquire());}}

test1输出结果如下:

0.0
0.196277
0.198796
0.199935
0.199476
0.200038

对以上方法和执行结果分析:

  • RateLimiter.create(5) 表示桶容量为5且每秒新增5个令牌,即每隔200毫秒新增一个令牌
  • limiter.acquire()表示消费一个令牌,如果当前桶中有足够令牌则成功(返回值为0),如果桶中没有令牌则暂停一段时间,比如发令牌间隔是200毫秒,则等待200毫秒后再去消费令牌(如上测试用例返回的为0.196277,差不多等待了200毫秒桶中才有令牌可用),这种实现将突发请求速率平均为了固定请求速率

接下来再看一个突发的例子:

/*** <p>* 0.0* 耗时1:23* 0.0* 耗时2:2023* 0.0* 耗时3:2023* 0.0* 耗时4:2023* 0.499819* 耗时5:2526* 0.496035* 耗时6:3023* <p/>** @param* @return void* @Date 2020/7/7 21:55*/private static void test4() {long startTime = System.currentTimeMillis();RateLimiter limiter = RateLimiter.create(2);System.out.println(limiter.acquire());System.out.println("耗时1:" + (System.currentTimeMillis() - startTime));try {Thread.sleep(2000L);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(limiter.acquire());System.out.println("耗时2:" + (System.currentTimeMillis() - startTime));System.out.println(limiter.acquire());System.out.println("耗时3:" + (System.currentTimeMillis() - startTime));System.out.println(limiter.acquire());System.out.println("耗时4:" + (System.currentTimeMillis() - startTime));System.out.println(limiter.acquire());System.out.println("耗时5:" + (System.currentTimeMillis() - startTime));System.out.println(limiter.acquire());System.out.println("耗时6:" + (System.currentTimeMillis() - startTime));}
  • 创建了一个桶容量为2且每秒新增2个令牌;
  • 首先调用limiter.acquire()消费一个令牌,此时令牌桶可以满足(返回值为0);
  • 然后线程暂停2秒,接下来的两个limiter.acquire()都能消费到令牌,第四个limiter.acquire()也同样消费到了令牌,到第五个时就需要等待500毫秒了。

此处可以看到我们设置的桶容量为2(即允许的突发量),这是因为SmoothBursty中有一个参数: 最大突发秒数(maxBurstSeconds)默认值是1s突发量/桶容量=速率*maxBurstSeconds,所以本示例桶容量/突发量为2,例子中前两个是消费了之前积攒的突发量,而第三个开始就是正常计算的了。令牌桶算法允许将一段时间内没有消费的令牌暂存到令牌桶中,留待未来使用,并允许未来请求的这种突发。

SmoothBursty通过平均速率和最后一次新增令牌的时间计算出下次新增令牌的时间的,另外需要一个桶暂存一段时间内没有使用的令牌(即可以突发的令牌数)。另外RateLimiter还提供了tryAcquire方法来进行无阻塞或可超时的令牌消费。

因为SmoothBursty允许一定程度的突发,会有人担心如果允许这种突发,假设突然间来了很大的流量,那么系统很可能扛不住这种突发。因此需要一种平滑速率的限流工具,从而系统冷启动后慢慢的趋于平均固定速率(即刚开始速率小一些,然后慢慢趋于我们设置的固定速率)。Guava也提供了SmoothWarmingUp来实现这种需求,其可以认为是漏桶算法,但是在某些特殊场景又不太一样。

5.4.5.2 平滑预热限流(SmoothWarmingUp)

SmoothWarmingUp创建方式:RateLimiter.create(double permitsPerSecond, long warmupPeriod, TimeUnit unit)
permitsPerSecond表示每秒新增的令牌数,warmupPeriod表示在从冷启动速率过渡到平均速率的时间间隔

代码示例

	private static void test5() {RateLimiter limiter = RateLimiter.create(5, 1000, TimeUnit.MILLISECONDS);for(int i = 1; i < 5;i++) {System.out.println(limiter.acquire());}try {Thread.sleep(2000L);} catch (InterruptedException e) {e.printStackTrace();}for(int i = 1; i < 5;i++) {System.out.println(limiter.acquire());}}

以上示例说明,速率是梯形上升速率的,也就是说冷启动时会以一个比较大的速率慢慢到平均速率,然后趋于平均速率(梯形下降到平均速率)。可以通过调节warmupPeriod参数实现一开始就是平滑固定速率。

到此应用级限流的一些方法就介绍完了。假设将应用部署到多台机器,应用级限流方式只是单应用内的请求限流,不能进行全局限流。因此我们需要分布式限流和接入层限流来解决这个问题

5.5 分布式限流

更新中,后续完善更新…

分布式限流最关键的是要将限流服务做成原子化,而解决方案可以使用redis+lua或者nginx+lua技术进行实现,通过这两种技术可以实现服务的高并发和高可用。

Lua脚本本身就是一种编程语言,也可以使用它实现复杂的令牌桶或漏桶算法

5.5.1 基于Redis+Lua脚本限流

首先我们来使用redis+lua脚本,实现时间窗内某个接口的请求数限流,实现了该功能后可以改造为限流总并发数或者限流总请求数和限制总资源数。

Redis+Lua实现方案中,Lua的基础实现如下:

local key = KEYS[1] --限流KEY(一秒一个)
local limit = tonumber(ARGV[1])        --限流大小
local current = tonumber(redis.call("INCRBY", key, "1")) --请求数+1
if current > limit then --如果超出限流大小return 0
elseif current == 1 then  --只有第一次访问需要设置2秒的过期时间redis.call("expire", key,"2")
end
return 1

如上操作因是在一个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 --如果超出限流大小return 0
else  --请求数+1,并设置2秒过期redis.call("INCRBY", key,"1")redis.call("expire", key,"2")return 1
end

因为Redis的限制(Lua中有写操作不能使用带随机性质的读操作,如TIME)不能在Redis Lua中使用TIME获取时间戳,因此只好从应用获取然后传入,在某些极端情况下(机器时钟不准的情况下),限流会存在一些小问题。

如下是Java中判断是否需要限流的代码:

public static boolean acquire() throws Exception {String luaScript = Files.toString(new File("limit.lua"), Charset.defaultCharset());Jedis jedis = new Jedis("127.0.0.1", 6379);//此处将当前时间戳取秒数String key = "ip:" + System.currentTimeMillis()/ 1000; //限流大小Stringlimit = "3"; return (Long)jedis.eval(luaScript,Lists.newArrayList(key), Lists.newArrayList(limit)) == 1;
}

5.5.2 使用Nginx+Lua实现限流

Nginx+Lua实现方案中,Lua脚本的基础实现如下:

local locks = require "resty.lock"local function acquire()local lock =locks:new("locks")local elapsed, err =lock:lock("limit_key") --互斥锁local limit_counter =ngx.shared.limit_counter --计数器local key = "ip:" ..os.time()local limit = 5 --限流大小local current =limit_counter:get(key)if current ~= nil and current + 1> limit then --如果超出限流大小lock:unlock()return 0endif current == nil thenlimit_counter:set(key, 1, 1) --第一次需要设置过期时间,设置key的值为1,过期时间为1elselimit_counter:incr(key, 1) --第二次开始加1即可endlock:unlock()return 1
end
ngx.print(acquire())

实现中我们需要使用lua-resty-lock互斥锁模块来解决原子性问题(在实际工程中使用时请考虑获取锁的超时问题),并使用ngx.shared.DICT共享字典来实现计数器。如果需要限流则返回0,否则返回1。使用时需要先定义两个共享字典(分别用来存放锁和计数器数据):

http {……lua_shared_dict locks 10m;lua_shared_dict limit_counter 10m;}

有人估计会有疑问,Redis或者Nginx可以扛得住高并发?答案是肯定的。
这个问题要从多方面考虑: 你的流量是不是真的有这么大,是不是可以通过一致性哈希将分布式限流进行分片,是不是可以将高并发量降级为应用级限流,可以根据实际情况调节。

像京东就是使用Redis+Lua脚本来限流抢购流量,首页也是基于这种技术实现的快速渲染数据。

5.6 接入层限流(Nginx层进行限制)

对于分布式限流目前遇到的场景大多是业务上的限流,而不是流量入口的限流,流量入口限流应该在接入层完成,而接入层限流笔者一般使用Nginx,简单的一种实现就是,使用Nginx进行负载均衡

更新中,后续更新…

本文主要介绍了限流的一些方案,鉴于笔者的理解,就先介绍到这里了,后续会继续完善更新Lua等方式,上面介绍的计数器算法,基于Redis的实现,可以解决大部分场景下Api方法的限流了,希望对老铁们有所帮助。

参考资料链接:
https://www.cnblogs.com/softidea/p/6229543.html
https://cloud.tencent.com/developer/article/1408819

写博客是为了记住自己容易忘记的东西,另外也是对自己工作的总结,希望尽自己的努力,做到更好,大家一起努力进步!

如果有什么问题,欢迎大家评论,一起探讨,代码如有问题,欢迎各位大神指正!

给自己的梦想添加一双翅膀,让它可以在天空中自由自在的飞翔!


  1. 每秒查询率(QPS,Queries-per-second)是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准,在因特网上,经常用每秒查询率来衡量作为域名系统服务器的机器的性能。 ↩︎

这篇关于分布式环境下限流方案的思考的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/598967

相关文章

java如何分布式锁实现和选型

《java如何分布式锁实现和选型》文章介绍了分布式锁的重要性以及在分布式系统中常见的问题和需求,它详细阐述了如何使用分布式锁来确保数据的一致性和系统的高可用性,文章还提供了基于数据库、Redis和Zo... 目录引言:分布式锁的重要性与分布式系统中的常见问题和需求分布式锁的重要性分布式系统中常见的问题和需求

Golang使用etcd构建分布式锁的示例分享

《Golang使用etcd构建分布式锁的示例分享》在本教程中,我们将学习如何使用Go和etcd构建分布式锁系统,分布式锁系统对于管理对分布式系统中共享资源的并发访问至关重要,它有助于维护一致性,防止竞... 目录引言环境准备新建Go项目实现加锁和解锁功能测试分布式锁重构实现失败重试总结引言我们将使用Go作

Java汇编源码如何查看环境搭建

《Java汇编源码如何查看环境搭建》:本文主要介绍如何在IntelliJIDEA开发环境中搭建字节码和汇编环境,以便更好地进行代码调优和JVM学习,首先,介绍了如何配置IntelliJIDEA以方... 目录一、简介二、在IDEA开发环境中搭建汇编环境2.1 在IDEA中搭建字节码查看环境2.1.1 搭建步

Redis分布式锁使用及说明

《Redis分布式锁使用及说明》本文总结了Redis和Zookeeper在高可用性和高一致性场景下的应用,并详细介绍了Redis的分布式锁实现方式,包括使用Lua脚本和续期机制,最后,提到了RedLo... 目录Redis分布式锁加锁方式怎么会解错锁?举个小案例吧解锁方式续期总结Redis分布式锁如果追求

在 VSCode 中配置 C++ 开发环境的详细教程

《在VSCode中配置C++开发环境的详细教程》本文详细介绍了如何在VisualStudioCode(VSCode)中配置C++开发环境,包括安装必要的工具、配置编译器、设置调试环境等步骤,通... 目录如何在 VSCode 中配置 C++ 开发环境:详细教程1. 什么是 VSCode?2. 安装 VSCo

Java解析JSON的六种方案

《Java解析JSON的六种方案》这篇文章介绍了6种JSON解析方案,包括Jackson、Gson、FastJSON、JsonPath、、手动解析,分别阐述了它们的功能特点、代码示例、高级功能、优缺点... 目录前言1. 使用 Jackson:业界标配功能特点代码示例高级功能优缺点2. 使用 Gson:轻量

Redis KEYS查询大批量数据替代方案

《RedisKEYS查询大批量数据替代方案》在使用Redis时,KEYS命令虽然简单直接,但其全表扫描的特性在处理大规模数据时会导致性能问题,甚至可能阻塞Redis服务,本文将介绍SCAN命令、有序... 目录前言KEYS命令问题背景替代方案1.使用 SCAN 命令2. 使用有序集合(Sorted Set)

MyBatis延迟加载的处理方案

《MyBatis延迟加载的处理方案》MyBatis支持延迟加载(LazyLoading),允许在需要数据时才从数据库加载,而不是在查询结果第一次返回时就立即加载所有数据,延迟加载的核心思想是,将关联对... 目录MyBATis如何处理延迟加载?延迟加载的原理1. 开启延迟加载2. 延迟加载的配置2.1 使用

Android WebView的加载超时处理方案

《AndroidWebView的加载超时处理方案》在Android开发中,WebView是一个常用的组件,用于在应用中嵌入网页,然而,当网络状况不佳或页面加载过慢时,用户可能会遇到加载超时的问题,本... 目录引言一、WebView加载超时的原因二、加载超时处理方案1. 使用Handler和Timer进行超

鸿蒙开发搭建flutter适配的开发环境

《鸿蒙开发搭建flutter适配的开发环境》文章详细介绍了在Windows系统上如何创建和运行鸿蒙Flutter项目,包括使用flutterdoctor检测环境、创建项目、编译HAP包以及在真机上运... 目录环境搭建创建运行项目打包项目总结环境搭建1.安装 DevEco Studio NEXT IDE