增加网站备案,wordpress与,珠海网站建设维护,免费视频网站制作限流在很多场景中用来限制并发和请求量#xff0c;比如说秒杀抢购#xff0c;保护自身系统和下游系统不被巨型流量冲垮等。
以微博为例#xff0c;例如某某明星公布了恋情#xff0c;访问从平时的50万增加到了500万#xff0c;系统的规划能力#xff0c;最多可以支撑200万…限流在很多场景中用来限制并发和请求量比如说秒杀抢购保护自身系统和下游系统不被巨型流量冲垮等。以微博为例例如某某明星公布了恋情访问从平时的50万增加到了500万系统的规划能力最多可以支撑200万访问那么就要执行限流规则保证是一个可用的状态不至于服务器崩溃所有请求不可用。限流的思想在保证可用的情况下尽可能多增加进入的人数其余的人在排队等待或者返回友好提示保证里面的进行系统的用户可以正常使用防止系统雪崩。日常生活中有哪些需要限流的地方?像我旁边有一个国家景区平时可能根本没什么人前往但是一到五一或者春节就人满为患这时候景区管理人员就会实行一系列的政策来限制进入人流量为什么要限流呢?假如景区能容纳一万人现在进去了三万人势必摩肩接踵整不好还会有事故发生这样的结果就是所有人的体验都不好如果发生了事故景区可能还要关闭导致对外不可用这样的后果就是所有人都觉得体验糟糕透了。本地限流/本地限流的四大算法限流算法很多常见的有四大算法分别是计数器算法、滑动窗口算法、 漏桶算法、令牌桶算法下面逐一讲解。限流的手段通常有计数器、漏桶、令牌桶。注意限流和限速所有请求都会处理的差别视业务场景而定。1计数器也叫做 固定窗口算法。在一段时间间隔内时间窗/时间区间处理请求的最大数量固定超过部分不做处理。2滑动窗口算法滑动窗口限流是对固定窗口限流算法的一种改进。在固定窗口限流中时间窗口是固定划分的而滑动窗口限流将时间窗口划分为多个更小的子窗口随着时间的推移窗口会不断地滑动。在统计请求数量时会统计当前滑动窗口内所有子窗口的请求总和当请求数量超过预设的阈值时就拒绝后续的请求。3漏桶漏桶大小固定处理速度固定但请求进入速度不固定在突发情况请求过多时会丢弃过多的请求。4令牌桶令牌桶的大小固定令牌的产生速度固定但是消耗令牌即请求速度不固定可以应对一些某些时间请求过多的情况每个请求都会从令牌桶中取出令牌如果没有令牌则丢弃该次请求。计数器限流固定窗口限流图解计数器限流原理在一段时间间隔内时间窗/时间区间处理请求的最大数量固定超过部分不做处理。计数器限流 也叫做 固定窗口算法 是一种简单直观的限流算法其原理是将时间划分为固定大小的窗口在每个窗口内限制请求的数量或速率。计数器算法是限流算法里最简单也是最容易实现的一种算法。计数器限流 具体实现时可以使用一个计数器来记录当前窗口内的请求数并与预设的阈值进行比较。计数器限流 的原理如下(1) 将时间划分固定大小窗口例如每秒一个窗口。(2) 在每个窗口内记录请求的数量。(3) 当有请求到达时将请求计数加一。(4) 如果请求计数超过了预设的阈值比如3个请求拒绝该请求。(5) 窗口结束后重置请求计数。举个例子比如我们规定对于A接口我们1分钟的访问次数不能超过100个。那么我们可以这么做在一开 始的时候我们可以设置一个计数器counter每当一个请求过来的时候counter就加1如果counter的值大于100并且该请求与第一个请求的间隔时间还在1分钟之内那么说明请求数过多拒绝访问如果该请求与第一个请求的间隔时间大于1分钟且counter的值还在限流范围内那么就重置 counter就是这么简单粗暴。计数器限流固定窗口限流限流的实现packagecom.crazymaker.springcloud.ratelimit;importlombok.extern.slf4j.Slf4j;importorg.junit.Test;importjava.util.concurrent.CountDownLatch;importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;importjava.util.concurrent.atomic.AtomicInteger;importjava.util.concurrent.atomic.AtomicLong;// 计速器 限速Slf4jpublicclassCounterLimiter{// 起始时间privatestaticlongstartTimeSystem.currentTimeMillis();// 时间区间的时间间隔 msprivatestaticlonginterval1000;// 每秒限制数量privatestaticlongmaxCount2;//累加器privatestaticAtomicLongaccumulatornewAtomicLong();// 计数判断, 是否超出限制privatestaticlongtryAcquire(longtaskId,intturn){longnowTimeSystem.currentTimeMillis();//在时间区间之内if(nowTimestartTimeinterval){longcountaccumulator.incrementAndGet();if(countmaxCount){returncount;}else{return-count;}}else{//在时间区间之外synchronized(CounterLimiter.class){log.info(新时间区到了,taskId{}, turn {}..,taskId,turn);// 再一次判断防止重复初始化if(nowTimestartTimeinterval){accumulator.set(0);startTimenowTime;}}return0;}}//线程池用于多线程模拟测试privateExecutorServicepoolExecutors.newFixedThreadPool(10);TestpublicvoidtestLimit(){// 被限制的次数AtomicIntegerlimitednewAtomicInteger(0);// 线程数finalintthreads2;// 每条线程的执行轮数finalintturns20;// 同步器CountDownLatchcountDownLatchnewCountDownLatch(threads);longstartSystem.currentTimeMillis();for(inti0;ithreads;i){pool.submit(()-{try{for(intj0;jturns;j){longtaskIdThread.currentThread().getId();longindextryAcquire(taskId,j);if(index0){// 被限制的次数累积limited.getAndIncrement();}Thread.sleep(200);}}catch(Exceptione){e.printStackTrace();}//等待所有线程结束countDownLatch.countDown();});}try{countDownLatch.await();}catch(InterruptedExceptione){e.printStackTrace();}floattime(System.currentTimeMillis()-start)/1000F;//输出统计结果log.info(限制的次数为limited.get(),通过的次数为(threads*turns-limited.get()));log.info(限制的比例为(float)limited.get()/(float)(threads*turns));log.info(运行的时长为time);}}计数器限流固定窗口限流的优点(1) 实现简单固定窗口算法的实现相对简单易于理解和部署。(2) 稳定性较高对于突发请求能够较好地限制和控制稳定性较高。(3) 易于实现速率控制固定窗口算法可以很容易地限制请求的速率例如每秒最多允许多少个请求。计数器限流固定窗口限流的严重问题1. 临界问题突刺现象问题描述固定窗口限流在窗口切换的瞬间可能会出现流量的突刺导致在短时间内有大量请求通过从而突破限流阈值对系统造成冲击。示例假设设置每分钟允许 60 个请求一个固定窗口的时间范围是从 0 分 0 秒到 1 分 0 秒。在 0 分 59 秒时已经有 59 个请求通过了限流此时还没有达到阈值。当时间进入 1 分 0 秒时新的窗口开始计数器重置为 0。在这一瞬间又可以有 60 个请求通过那么在 0 分 59 秒到 1 分 0 秒这极短的时间内系统可能会收到多达 119 个请求远远超过了每分钟 60 个请求的限流阈值。影响短时间内的大量请求可能会使系统资源耗尽如 CPU 使用率过高、内存溢出等从而导致系统性能下降甚至崩溃。如果对临界问题突刺现象不了解我们看下图从上图中我们可以看到假设有一个恶意用户他在0:59时瞬间发送了100个请求并且1:00又瞬间发送了100个请求那么其实这个用户在 1秒里面瞬间发送了200个请求。我们刚才规定的是1分钟最多100个请求规划的吞吐量也就是每秒钟最多1.7个请求用户通过在时间窗口的重置节点处突发请求 可以瞬间超过我们的速率限制。用户有可能通过算法的这个漏洞瞬间压垮我们的应用。除了临界问题突刺现象还有其他问题比如无法应对突发流量、限流精度低等等。2. 无法应对突发流量无法应对突发流量 的问题描述固定窗口限流只能对固定时间窗口内的平均流量进行限制无法灵活应对突发的流量高峰。即使在大部分时间内流量较低但只要在某个窗口内流量突然增加并超过阈值后续请求就会被拒绝这可能会影响用户体验。无法应对突发流量 的示例一个在线商城在正常情况下每分钟的请求量大约为 20 个但在促销活动开始的瞬间可能会在几秒钟内产生大量的请求。如果固定窗口限流的阈值设置为每分钟 50 个请求那么在促销活动开始时由于请求量突然增加超过了阈值后续的很多请求都会被拒绝导致用户无法正常访问商城页面。无法应对突发流量 的影响用户可能会因为频繁收到请求被拒绝的提示而感到不满从而降低对系统的信任度甚至可能会流失用户。3. 限流精度低限流精度低问题描述固定窗口的时间粒度通常是固定的无法根据实际情况进行更细粒度的限流控制。例如如果设置的时间窗口是 1 分钟那么只能对每分钟的请求量进行统计和限制无法对更短时间内的流量进行精确控制。限流精度低示例在一些对流量控制要求较高的场景中如金融交易系统可能需要对每秒甚至更短时间内的请求量进行精确控制。但固定窗口限流只能以分钟为单位进行限流无法满足这种高精度的限流需求。限流精度低影响可能会导致系统在某些情况下无法及时对流量进行调整从而影响系统的稳定性和可靠性。滑动窗口限流滑动窗口限流是对固定窗口限流算法的一种改进。在固定窗口限流中时间窗口是固定划分的而滑动窗口限流将时间窗口划分为多个更小的子窗口随着时间的推移窗口会不断地滑动。在统计请求数量时会统计当前滑动窗口内所有子窗口的请求总和当请求数量超过预设的阈值时就拒绝后续的请求。这样可以更平滑地处理流量减少固定窗口限流中可能出现的临界问题突刺现象。滑动窗口算法在固定窗口的基础上将一个计时窗口分成了若干个小窗口然后每个小窗口维护一个独立的计数器。当请求的时间大于当前窗口的最大时间时则将计时窗口向前平移一个小窗口。平移时将第一个小窗口的数据丢弃然后将第二个小窗口设置为第一个小窗口同时在最后面新增一个小窗口将新的请求放在新增的小窗口中。同时要保证整个窗口中所有小窗口的请求数目之后不能超过设定的阈值。从图中不难看出滑动窗口算法就是固定窗口的升级版。将计时窗口划分成一个小窗口滑动窗口算法就退化成了固定窗口算法。而滑动窗口算法其实就是对请求数进行了更细粒度的限流窗口划分的越多则限流越精准。图解滑动窗口限流原理上文已经说明当遇到时间窗口的临界突变时固定窗口算法可能无法灵活地应对流量的变化。滑动窗口限流 的原理如下(1) 窗口大小确定一个固定的窗口大小例如1秒。(2) 请求计数在窗口内每次有请求到达时将请求计数加1。(3) 限制条件如果窗口内的请求计数超过了设定的阈值即超过了允许的最大请求数就拒绝该请求。(4) 窗口滑动随着时间的推移窗口会不断滑动移除过期的请求计数以保持窗口内的请求数在限制范围内。(5) 动态调整在滑动窗口算法中我们可以根据实际情况调整窗口的大小。当遇到下一个时间窗口之前我们可以根据当前的流量情况来调整窗口的大小以适应流量的变化。滑动窗口限流代码实现importjava.util.concurrent.ConcurrentHashMap;importjava.util.concurrent.ConcurrentLinkedQueue;importjava.util.concurrent.atomic.AtomicInteger;publicclassSlidingWindowRateLimiter{// 时间窗口大小毫秒privatefinallongwindowSize;// 允许的最大请求数privatefinalintmaxRequests;// 子窗口数量privatefinalintsubWindows;// 每个子窗口的大小毫秒privatefinallongsubWindowSize;// 存储每个子窗口的请求计数privatefinalConcurrentHashMapLong,AtomicIntegerwindowCounts;// 存储子窗口的时间戳队列privatefinalConcurrentLinkedQueueLongwindowTimestamps;publicSlidingWindowRateLimiter(longwindowSize,intmaxRequests,intsubWindows){this.windowSizewindowSize;this.maxRequestsmaxRequests;this.subWindowssubWindows;this.subWindowSizewindowSize/subWindows;this.windowCountsnewConcurrentHashMap();this.windowTimestampsnewConcurrentLinkedQueue();}publicsynchronizedbooleanallowRequest(){longcurrentTimeSystem.currentTimeMillis();// 移除过期的子窗口while(!windowTimestamps.isEmpty()currentTime-windowTimestamps.peek()windowSize){longexpiredTimestampwindowTimestamps.poll();windowCounts.remove(expiredTimestamp);}// 计算当前时间所在的子窗口时间戳longcurrentSubWindowcurrentTime-(currentTime%subWindowSize);// 获取或创建当前子窗口的计数器AtomicIntegercurrentCountwindowCounts.computeIfAbsent(currentSubWindow,k-{windowTimestamps.offer(currentSubWindow);returnnewAtomicInteger(0);});// 计算当前滑动窗口内的总请求数inttotalRequestswindowCounts.values().stream().mapToInt(AtomicInteger::get).sum();if(totalRequestsmaxRequests){// 请求通过增加当前子窗口的计数currentCount.incrementAndGet();returntrue;}returnfalse;}publicstaticvoidmain(String[]args){// 时间窗口为 1000 毫秒允许最大请求数为 100子窗口数量为 10SlidingWindowRateLimiterlimiternewSlidingWindowRateLimiter(1000,100,10);for(inti0;i120;i){if(limiter.allowRequest()){System.out.println(Request i allowed);}else{System.out.println(Request i denied);}try{Thread.sleep(10);}catch(InterruptedExceptione){e.printStackTrace();}}}}调整窗口大小为了实现根据提供的 API 方法调整窗口大小的功能 需要对原有的 SlidingWindowRateLimiter 类进行一些修改。主要思路是将 windowSize 和 subWindowSize 变为可变的属性并提供一个公共方法来调整窗口大小同时在调整窗口大小后需要重新计算过期的子窗口。以下是优化后的代码importjava.util.concurrent.ConcurrentHashMap;importjava.util.concurrent.ConcurrentLinkedQueue;importjava.util.concurrent.atomic.AtomicInteger;publicclassSlidingWindowRateLimiter{// 时间窗口大小毫秒privatelongwindowSize;// 允许的最大请求数privatefinalintmaxRequests;// 子窗口数量privatefinalintsubWindows;// 每个子窗口的大小毫秒privatelongsubWindowSize;// 存储每个子窗口的请求计数privatefinalConcurrentHashMapLong,AtomicIntegerwindowCounts;// 存储子窗口的时间戳队列privatefinalConcurrentLinkedQueueLongwindowTimestamps;publicSlidingWindowRateLimiter(longwindowSize,intmaxRequests,intsubWindows){this.windowSizewindowSize;this.maxRequestsmaxRequests;this.subWindowssubWindows;this.subWindowSizewindowSize/subWindows;this.windowCountsnewConcurrentHashMap();this.windowTimestampsnewConcurrentLinkedQueue();}publicsynchronizedbooleanallowRequest(){longcurrentTimeSystem.currentTimeMillis();// 移除过期的子窗口removeExpiredWindows(currentTime);// 计算当前时间所在的子窗口时间戳longcurrentSubWindowcurrentTime-(currentTime%subWindowSize);// 获取或创建当前子窗口的计数器AtomicIntegercurrentCountwindowCounts.computeIfAbsent(currentSubWindow,k-{windowTimestamps.offer(currentSubWindow);returnnewAtomicInteger(0);});// 计算当前滑动窗口内的总请求数inttotalRequestswindowCounts.values().stream().mapToInt(AtomicInteger::get).sum();if(totalRequestsmaxRequests){// 请求通过增加当前子窗口的计数currentCount.incrementAndGet();returntrue;}returnfalse;}// 移除过期的子窗口privatevoidremoveExpiredWindows(longcurrentTime){while(!windowTimestamps.isEmpty()currentTime-windowTimestamps.peek()windowSize){longexpiredTimestampwindowTimestamps.poll();windowCounts.remove(expiredTimestamp);}}// 调整窗口大小的 API 方法publicsynchronizedvoidadjustWindowSize(longnewWindowSize){this.windowSizenewWindowSize;// subWindows 子窗口数量不变this.subWindowSizenewWindowSize/subWindows;longcurrentTimeSystem.currentTimeMillis();// 移除因窗口大小调整而过期的子窗口removeExpiredWindows(currentTime);}publicstaticvoidmain(String[]args){// 时间窗口为 1000 毫秒允许最大请求数为 100子窗口数量为 10SlidingWindowRateLimiterlimiternewSlidingWindowRateLimiter(1000,100,10);for(inti0;i50;i){if(limiter.allowRequest()){System.out.println(Request i allowed);}else{System.out.println(Request i denied);}try{Thread.sleep(10);}catch(InterruptedExceptione){e.printStackTrace();}}// 调整窗口大小为 2000 毫秒limiter.adjustWindowSize(2000);for(inti50;i120;i){if(limiter.allowRequest()){System.out.println(Request i allowed);}else{System.out.println(Request i denied);}try{Thread.sleep(10);}catch(InterruptedExceptione){e.printStackTrace();}}}}代码解释(1) 属性修改将 windowSize 和 subWindowSize 的 final 修饰符去掉使其可以被修改。(2) removeExpiredWindows 方法封装了移除过期子窗口的逻辑方便在 allowRequest 方法和 adjustWindowSize 方法中复用。(3) adjustWindowSize 方法提供了一个公共的 API 方法用于调整窗口大小。在调整窗口大小后重新计算 subWindowSize并调用 removeExpiredWindows 方法移除因窗口大小调整而过期的子窗口。(4) main 方法模拟了先处理 50 个请求然后调用 adjustWindowSize 方法将窗口大小调整为 2000 毫秒再处理后续的请求。通过这些修改我们实现了在运行时动态调整窗口大小的功能。滑动窗口限流优点(1) 灵活性滑动窗口算法可以根据实际情况动态调整窗口的大小以适应流量的变化。这种灵活性使得算法能够更好地应对突发流量和请求分布不均匀的情况。(2) 实时性由于滑动窗口算法在每个时间窗口结束时都会进行窗口滑动它能够更及时地响应流量的变化提供更实时的限流效果。(3) 精度相比于固定窗口算法滑动窗口算法的颗粒度更小可以提供更精确的限流控制。滑动窗口限流缺点滑动窗口限流虽然是对固定窗口限流算法的改进能更平滑地处理流量减少临界问题但它也存在一些缺点具体如下滑动窗口不足之一实现复杂度较高逻辑复杂相较于固定窗口限流滑动窗口限流需要将时间窗口划分为多个子窗口并对每个子窗口的请求计数进行维护。在窗口滑动时要判断哪些子窗口已经过期并移除其计数这增加了算法的逻辑复杂度。例如在 Java 代码实现中需要使用 ConcurrentHashMap 存储子窗口的请求计数使用 ConcurrentLinkedQueue 存储子窗口的时间戳还需要处理线程安全问题使得代码实现难度增大。调试困难由于逻辑复杂当出现限流不准确或其他问题时调试难度较大。需要仔细检查子窗口的划分、过期子窗口的移除、请求计数的统计等多个环节排查问题的时间和精力成本较高。滑动窗口不足之二资源消耗较大内存占用为了存储每个子窗口的请求计数和时间戳需要额外的内存空间。尤其是当子窗口数量较多时内存占用会显著增加。例如在高并发场景下如果将时间窗口划分为大量的子窗口ConcurrentHashMap 和 ConcurrentLinkedQueue 会占用较多的内存可能导致系统内存资源紧张。CPU 开销在每次请求到达时需要遍历子窗口的时间戳队列判断哪些子窗口已经过期并移除这会带来一定的 CPU 开销。特别是在高并发情况下频繁的请求会使这种 CPU 开销更加明显影响系统的性能。滑动窗口不足之三时间精度有限子窗口划分限制滑动窗口限流的时间精度取决于子窗口的划分粒度。如果子窗口划分得不够细仍然可能无法精确地应对短时间内的流量突变。例如将 1 秒的时间窗口划分为 10 个子窗口每个子窗口为 100 毫秒对于小于 100 毫秒内的流量高峰可能无法进行精确的限流控制。滑动窗口算法实际上是颗粒度更小的固定窗口算法它可以在一定程度上提高限流的精度和实时性并不能从根本上解决请求分布不均匀的问题。特别是在极端情况下如突发流量过大或请求分布极不均匀的情况下仍然可能导致限流不准确。因此在实际应用中要采用更复杂的算法或策略来进一步优化限流效果。时钟同步问题在分布式系统中不同节点的时钟可能存在一定的偏差这会影响滑动窗口的准确性。例如节点 A 和节点 B 的时钟不一致可能导致在同一时刻节点 A 认为某个子窗口已经过期而节点 B 却认为该子窗口仍然有效从而造成限流的不准确。漏桶算法限流漏桶算法限流的基本原理为水对应请求从进水口进入到漏桶里漏桶以一定的速度出水请求放行当水流入速度过大桶内的总水量大于桶容量会直接溢出请求被拒绝如图所示。大致的漏桶限流规则如下1进水口对应客户端请求以任意速率流入进入漏桶。2漏桶的容量是固定的出水放行速率也是固定的。3漏桶容量是不变的如果处理速度太慢桶内水量会超出了桶的容量则后面流入的水滴会溢出表示请求拒绝。图解漏桶算法原理漏桶算法思路很简单漏桶以一定的速度出水水 代表了用户请求。漏桶算法 可以粗略的认为就是注水 漏水过程往桶中以任意速率注入水以一定速率流出水当水超过桶容量capacity则丢弃因为桶容量是不变的保证了整体的速率。水 先进入到漏桶里当水流入速度过大时 会超过桶可接纳的容量时直接溢出。变化的地方入口变化入口速度 变化。具体来说可以以任意速率往桶中 注入水。固定的部分出口固定出口速度 变化漏桶算法 以一固定速率流出水。从 图 可以看出漏桶算法能强行限制数据的出水速率。由于 出口固定所以 漏桶算法至少有下面的两个作用削峰有大量流量进入时会发生溢出从而限流保护服务可用缓冲不至于直接请求到服务器缓冲压力漏桶算法的实现漏桶的容量就像队列的容量当请求堆积超过指定容量时会触发拒绝策略即新到达的请求将被丢弃或延迟处理。算法的实现如下(1) 漏桶容量确定一个固定的漏桶容量表示漏桶可以存储的最大请求数。(2) 漏桶速率确定一个固定的漏桶速率表示漏桶每秒可以处理的请求数。(3) 请求处理当请求到达时生产者将请求放入漏桶中。(4) 漏桶流出漏桶以固定的速率从漏桶中消费请求并处理这些请求。如果漏桶中有请求则处理一个请求如果漏桶为空则不处理请求。(5) 请求丢弃或延迟如果漏桶已满即漏桶中的请求数达到了容量上限新到达的请求将被丢弃或延迟处理。importjava.util.concurrent.*;publicclassLeakyBucketRateLimiter{privatefinalintcapacity;// 漏桶容量privatefinalintrate;// 漏桶速率每秒处理的请求数privatefinalBlockingQueueRunnablebucket;// 漏桶队列privatefinalScheduledExecutorServiceexecutor;// 定时任务执行器publicLeakyBucketRateLimiter(intcapacity,intrate){this.capacitycapacity;this.raterate;this.bucketnewLinkedBlockingQueue(capacity);this.executorExecutors.newScheduledThreadPool(1);// 启动漏桶的处理任务executor.scheduleAtFixedRate(this::processRequest,0,1000L/rate,TimeUnit.MILLISECONDS);}// 尝试将请求放入漏桶publicbooleantryPutRequest(Runnablerequest){if(bucket.remainingCapacity()0){// 漏桶已满拒绝请求System.out.println(漏桶已满请求被拒绝);returnfalse;}bucket.add(request);System.out.println(请求已放入漏桶);returntrue;}// 漏桶处理请求privatevoidprocessRequest(){if(!bucket.isEmpty()){try{Runnablerequestbucket.take();// 从漏桶中取出一个请求request.run();// 处理请求System.out.println(请求已处理);}catch(InterruptedExceptione){Thread.currentThread().interrupt();}}}// 关闭漏桶限流器publicvoidshutdown(){executor.shutdown();}publicstaticvoidmain(String[]args){LeakyBucketRateLimiterlimiternewLeakyBucketRateLimiter(capacity:10,rate:2);// 模拟多个请求for(inti0;i15;i){Runnablerequest()-System.out.println(处理请求: i);limiter.tryPutRequest(request);}// 等待一段时间后关闭try{Thread.sleep(10000);}catch(InterruptedExceptione){e.printStackTrace();}finally{limiter.shutdown();}}}漏桶算法是一种常用的限流算法其核心思想是将请求比作水漏桶以固定的速率处理请求当请求超过漏桶容量时新请求会被丢弃或延迟处理。下面详细介绍漏桶算法的优点和缺点。漏桶算法的优点1. 平滑流量输出稳定处理速率漏桶算法以固定的速率处理请求无论外部请求的流量波动有多大漏桶都会按照预设的速率依次处理请求。这就像一个底部有固定大小小孔的桶水总是以稳定的速度流出。例如在一个 API 服务中设置漏桶的处理速率为每秒 10 个请求那么无论在某一时刻是突然涌入 100 个请求还是只有 1 个请求服务都会以每秒 10 个的速度处理这些请求从而避免了系统因瞬间的高流量冲击而崩溃。避免系统过载稳定的处理速率可以有效地保护系统资源防止系统因处理过多请求而出现过载的情况。当请求流量过大时多余的请求会在漏桶中排队等待处理或者被直接丢弃这样可以确保系统始终在其处理能力范围内运行保证系统的稳定性和可靠性。2. 易于实现和理解简单的逻辑结构漏桶算法的逻辑相对简单主要涉及到两个核心概念漏桶的容量和漏桶的处理速率。在实现上通常只需要使用一个队列来模拟漏桶再通过一个定时器或者线程以固定的速率从队列中取出请求进行处理即可。例如在 Java 中可以使用BlockingQueue来实现漏桶使用一个线程以固定的时间间隔从队列中取出元素进行处理代码实现较为简洁易懂。直观的工作原理漏桶算法的工作原理与现实生活中的漏桶现象类似容易让人理解。开发者可以很直观地根据业务需求设置漏桶的容量和处理速率从而实现对流量的控制。3. 公平性先进先出原则漏桶算法遵循先进先出FIFO的原则即先进入漏桶的请求会先被处理。这保证了每个请求都有公平的处理机会不会因为请求的优先级或者其他因素而导致某些请求被优先处理。例如在一个多用户的系统中每个用户的请求都会按照到达的顺序依次在漏桶中排队等待处理不会出现某个用户的请求因为特殊原因而插队的情况体现了公平性。漏桶算法的缺点1. 无法应对突发流量固定处理速率的限制由于漏桶算法以固定的速率处理请求当出现突发的高流量时即使系统有足够的处理能力也只能按照预设的速率处理请求无法及时响应突发流量。例如在电商系统的促销活动开始瞬间会有大量的用户同时发起请求此时漏桶算法只能以固定的速率处理这些请求导致大量请求在漏桶中排队等待甚至被丢弃用户体验会受到严重影响。资源利用率低在流量低谷期系统的处理能力可能远远没有达到上限但漏桶算法仍然以固定的速率处理请求无法充分利用系统的空闲资源。这就导致了系统资源的浪费降低了系统的整体性能。2. 缺乏灵活性参数调整困难漏桶算法的性能很大程度上取决于漏桶的容量和处理速率这两个参数的设置。然而在实际应用中很难准确地预测系统的流量变化情况因此很难设置出合适的参数。如果参数设置过小可能会导致大量请求被丢弃影响系统的可用性如果参数设置过大又可能无法起到有效的限流作用。不适应动态场景在一些动态变化的场景中如系统的负载随着时间不断变化或者不同类型的请求对系统资源的消耗不同漏桶算法无法根据实际情况动态调整处理速率缺乏灵活性。3. 可能导致请求延迟排队等待时间长当请求流量超过漏桶的处理速率时请求会在漏桶中排队等待处理。如果流量持续过高排队的请求会越来越多导致请求的处理延迟增加。对于一些对实时性要求较高的业务场景如金融交易、实时通信等过长的请求延迟可能会带来严重的后果。例如在金融交易中延迟的交易请求可能会导致错过最佳的交易时机造成经济损失。漏桶算法的适用场景适合漏桶算法的场景需要平滑流量的系统如 API 网关、 Nginx 网关、消息队列。需要防止系统过载的场景如 DDoS 防护、限流保护。资源受限的环境如嵌入式系统、低资源服务器。不适合漏桶算法的场景需要处理突发流量的系统。实时性要求较高的场景如在线游戏、实时通信。需要区分请求优先级的场景如任务调度、实时计算。图解令牌桶限流原理令牌桶算法以一个设定的速率产生令牌并放入令牌桶每次用户请求都得申请令牌如果令牌不足则拒绝请求。 令牌桶算法中新请求到来时会从桶里拿走一个令牌如果桶内没有令牌可拿就拒绝服务。当然令牌的数量也是有上限的。令牌的数量与时间和发放速率强相关时间流逝的时间越长会不断往桶里加入越多的令牌如果令牌发放的速度比申请速度快令牌桶会放满令牌直到令牌占满整个令牌桶。令牌桶限流大致的规则如下1进水口按照某个速度向桶中放入令牌。2令牌的容量是固定的但是放行的速度不是固定的只要桶中还有剩余令牌一旦请求过来就能申请成功然后放行。3如果令牌的发放速度慢于请求到来速度桶内就无牌可领请求就会被拒绝。总之令牌的发送速率可以设置从而可以对突发的出口流量进行有效的应对。令牌桶与漏桶相似不同的是令牌桶桶中放了一些令牌服务请求到达后要获取令牌之后才会得到服务。举个例子我们平时去食堂吃饭都是在食堂内窗口前排队的这就好比是漏桶算法大量的人员聚集在食堂内窗口外以一定的速度享受服务如果涌进来的人太多食堂装不下了可能就有一部分人站到食堂外了。这就没有享受到食堂的服务称之为溢出溢出可以继续请求也就是继续排队这个还是带burst的漏桶算法。那么这样有什么问题呢?如果 有特殊情况比如 要高考啦这种情况就是突发情况不能让考生排太长得想办法并发处理掉这些突发流量 比如 外调一批 厨师来 做更多的伙食 把突发的流量满足掉。所以这时候漏桶算法可能就不合适了令牌桶算法更为适合。如图所示令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌而如果请求需要被处理则需要先从桶里获取一个令牌当桶里没有令牌可取时则拒绝服务。令牌桶算法实现令牌桶算法的实现步骤如下(1) 初始化一个令牌桶包括桶的容量和令牌产生的速率。(2) 持续以固定速率产生令牌并放入令牌桶中直到桶满为止。(3) 当请求到达时尝试从令牌桶中获取一个令牌。(4) 如果令牌桶中有足够的令牌则请求通过并从令牌桶中移除一个令牌。(5) 如果令牌桶中没有足够的令牌则请求被限制或丢弃。importjava.util.concurrent.TimeUnit;// 令牌桶限流器类publicclassTokenBucketRateLimiter{// 令牌桶的容量即桶最多能容纳的令牌数量privatefinalintcapacity;// 令牌产生的速率单位为每秒产生的令牌数privatefinalintrate;// 当前令牌桶中的令牌数量privateinttokens;// 上次更新令牌数量的时间戳单位为毫秒privatelonglastRefillTime;/*构造函数用于初始化令牌桶限流器*paramcapacity 令牌桶的容量*paramrate 令牌产生的速率每秒产生的令牌数*/publicTokenBucketRateLimiter(intcapacity,intrate){this.capacitycapacity;this.raterate;// 初始化时令牌桶是满的this.tokenscapacity;// 记录初始化时的时间this.lastRefillTimeSystem.currentTimeMillis();}/*尝试获取一个令牌如果有足够的令牌则返回true否则返回false*return如果成功获取令牌返回true否则返回false/publicsynchronizedbooleantryAcquire(){// 首先进行令牌的补充操作refill();// 检查令牌桶中是否有足够的令牌if(tokens0){// 如果有足够的令牌消耗一个令牌tokens--;returntrue;}// 没有足够的令牌返回 falsereturnfalse;}/*补充令牌的方法根据时间间隔和令牌产生速率来补充令牌/privatevoidrefill(){// 获取当前时间戳longnowSystem.currentTimeMillis();// 计算从上次更新令牌数量到现在经过的时间单位秒longelapsedTimeTimeUnit.MILLISECONDS.toSeconds(now-lastRefillTime);// 根据经过的时间和令牌产生速率计算应该补充的令牌数量intnewTokens(int)(elapsedTime*rate);if(newTokens0){// 更新令牌数量取补充后的令牌数量和桶容量的最小值确保令牌数量不超过桶的容量tokensMath.min(capacity,tokensnewTokens);// 更新上次更新令牌数量的时间戳lastRefillTimenow;}}publicstaticvoidmain(String[]args){// 创建一个令牌桶限流器容量为 100每秒产生 10 个令牌TokenBucketRateLimiterlimiternewTokenBucketRateLimiter(100,10);// 模拟 20 次请求for(inti0;i20;i){if(limiter.tryAcquire()){System.out.println(Request i is allowed.);}else{System.out.println(Request i is denied.);}try{// 模拟请求间隔 100 毫秒Thread.sleep(100);}catch(InterruptedExceptione){e.printStackTrace();}}}}令牌桶的好处令牌桶的好处之一就是可以方便地应对 突发出口流量后端能力的提升。比如可以改变令牌的发放速度算法能按照新的发送速率调大令牌的发放数量使得出口突发流量能被处理。Guava RateLimiterGuava是Java领域优秀的开源项目它包含了Google在Java项目中使用一些核心库包含集合(Collections)缓存(Caching)并发编程库(Concurrency)常用注解(Common annotations)String操作I/O操作方面的众多非常实用的函数。Guava的 RateLimiter提供了令牌桶算法实现平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)实现。令牌桶的 优缺点优点(1) 平滑流量令牌桶算法可以平滑突发流量使得突发流量在一段时间内均匀地分布避免了流量的突然高峰对系统的冲击。(2) 灵活性令牌桶算法可以通过调整令牌生成速率和桶的大小来灵活地控制流量。(3) 允许突发流量由于令牌桶可以积累一定数量的令牌因此在流量突然增大时如果桶中有足够的令牌可以应对这种突发流量。缺点(1) 实现复杂相比于其他一些限流算法如漏桶算法令牌桶算法的实现稍微复杂一些需要维护令牌的生成和消耗。(2) 需要精确的时间控制令牌桶算法需要根据时间来生成令牌因此需要有精确的时间控制。如果系统的时间控制不精确可能会影响限流的效果。(3) 可能会有资源浪费如果系统的流量持续低于令牌生成的速率那么桶中的令牌可能会一直积累造成资源的浪费。四种基本算法的对比工业级案例Nginx 的漏桶 限流 原理Nginx 的漏桶算法限流的使用Nginx 基于请求速率的限流 ngx_http_limit_req_module模块基于漏桶算法实现请求速率的限制它可以控制每个客户端 IP 或其他标识的请求频率防止过多请求对服务器造成压力。Nginx的限流功能通过ngx_http_limit_req_module模块实现主要通过limit_req_zone和limit_req指令进行配置。以下是一些常见的限流配置示例和说明帮助你快速理解和应用Nginx的限流功能。1. 基本限流配置以下是一个简单的限流配置示例限制每个客户端IP地址的请求速率http{# 定义一个限流区域 limit_req_zone $binary_remote_addr zonemy_limit:10m rate1r/s;server{listen80;server_name example.com;location/{# 应用限流规则 limit_req zonemy_limit;}}}limit_req_zone 的配置说明$binary_remote_addr基于客户端IP地址进行限流。zonemy_limit:10m定义一个名为my_limit的共享内存区域大小为10MB。rate1r/s每秒最多处理1个请求。limit_req的配置说明zonemy_limit引用limit_req_zone定义的共享内存区域。2. 处理突发流量如果需要允许突发流量可以使用burst参数。例如允许突发5个请求location/{limit_req zonemy_limit burst5;}burst5允许在短时间内额外处理5个请求。超出部分的请求会被延迟处理。3. 延迟处理与立即拒绝默认情况下超出速率的请求会被延迟处理。如果希望立即拒绝超出速率的请求可以使用nodelay参数location/{limit_req zonemy_limit burst5nodelay;}nodelay取消延迟处理超出速率的请求会立即返回错误默认为503。4. 自定义拒绝状态码可以通过limit_req_status指令自定义拒绝请求时返回的状态码。例如返回429状态码Too Many Requestslocation/{limit_req zonemy_limit burst5nodelay;limit_req_status429;}5. 基于不同键的限流除了基于客户端IP地址还可以使用其他键进行限流例如基于请求路径或用户代理http{# 基于请求路径限流 limit_req_zone $request_uri zonepath_limit:10m rate2r/s;# 基于用户代理限流 limit_req_zone $http_user_agent zoneagent_limit:10m rate1r/s;server{listen80;server_name example.com;location/{limit_req zonepath_limit burst3;}location/api/{limit_req zoneagent_limit burst2nodelay;}}}在Nginx中$http_user_agent 是一个内置变量用于获取客户端请求中的 User-Agent 头部信息。假设客户端请求中包含一个自定义头部user-id你可以通过以下配置实现基于user-id的限流基于 user-id 的header限流http{# 定义基于 user-id 的限流区域 limit_req_zone $http_user_id zoneuser_id_limit:10m rate1r/s;server{listen80;server_name example.com;location/{# 应用限流规则 limit_req zoneuser_id_limit burst5nodelay;limit_req_status429;# 返回429TooManyRequests}}}在Nginx中可以通过$http_header_name变量结合limit_req_zone指令实现基于HTTP头部如user-id的限流。4、Nginx 请求处理与限流的核心源码Nginx 核心的请求处理和限流判断在 ngx_http_limit_req_lookup 函数中实现staticngx_int_tngx_http_limit_req_lookup(ngx_http_limit_req_limit_t limit,ngx_uint_t hash,ngx_str_t key,ngx_uint_t ep,ngx_uint_t account){size_t size;ngx_int_t rc,excess;ngx_msec_t now;ngx_msec_int_t ms;ngx_rbtree_node_t node,sentinel;ngx_http_limit_req_ctx_t ctx;ngx_http_limit_req_node_t lr;nowngx_current_msec;ctxlimit-shm_zone-data;nodectx-sh-rbtree.root;sentinelctx-sh-rbtree.sentinel;while(node!sentinel){if(hashnode-key){nodenode-left;continue;}1if(hashnode-key){nodenode-right;continue;}/hashnode-key/lr(ngx_http_limit_req_node_t)node-color;rcngx_memn2cmp(key-data,lr-data,key-len,(size_t)lr-len);/hash 值相同且 key 相同才算是找到/if(rc0){/这个节点最近才访问放到队列首部最不容易被淘汰LRU 思想/ngx_queue_remove(lr-queue);ngx_queue_insert_head(ctx-sh-queue,lr-queue);/*漏桶算法以固定速率接受请求每秒接受 rate 个请求*ms 是距离上次处理这个 key 到现在的时间单位 ms*lr-excess 是上次还遗留着被延迟的请求数1000*excesslr-excess-ctx-rate*ngx_abs(ms)/10001000;*本次还会遗留的请求数就是上次遗留的减去这段时间可以处理掉的加上这个请求本身之前 burst 和 rate 都放大了1000倍/ms(ngx_msec_int_t)(now-lr-last);excesslr-excess-ctx-rate*ngx_abs(ms)/10001000;if(excess0){/全部处理完了/excess0;}epexcess;if((ngx_uint_t)excesslimit-burst){/这段时间处理之后遗留的请求数超出了突发请求限制/returnNGX_BUSY;}if(account){/这个请求到了最后一个“域”的限制*更新上次遗留请求数和上次访问时间*返回 NGX_OK 表示没有达到请求限制的频率/lr-excessexcess;lr-lastnow;returnNGX_OK;}/*count*把这个“域”的 ctx-node 指针指向这个节点*这个在 ngx_http_limit_req_handler 里用到/lr-count;/这一步是为了在 ngx_http_limit_req_account 里更新这些访问过的节点的信息/ctx-nodelr;/返回 NGX_AGAIN会进行下一个“域”的检查/returnNGX_AGAIN;}node(rc0)?node-left:node-right;}/没有在红黑树上找到节点/ep0;/*新建一个节点需要的内存大小包括了红黑树节点大小*ngx_http_limit_req_node_t 还有 key 的长度/sizeoffsetof(ngx_rbtree_node_t,color)offsetof(ngx_http_limit_req_node_t,data)key-len;/先进行 LRU 淘汰传入 n1则最多淘汰2个节点/ngx_http_limit_req_expire(ctx,1);/由于调用 ngx_http_limit_req_lookup 之前已经上过锁这里不用再上/nodengx_slab_alloc_locked(ctx-shpool,size);if(nodeNULL){/分配失败考虑再进行一次 LRU 淘汰及时释放共享内存空间这里 n0最多淘汰3个节点/ngx_http_limit_req_expire(ctx,0);nodengx_slab_alloc_locked(ctx-shpool,size);if(nodeNULL){ngx_log_error(NGX_LOG_ALERT,ngx_cycle-log,0,could not allocate node%s,ctx-shpool-log_ctx);returnNGX_ERROR;}}/设置相关的信息/node-keyhash;lr(ngx_http_limit_req_node_t)node-color;lr-len(u_short)key-len;lr-excess0;ngx_memcpy(lr-data,key-data,key-len);ngx_rbtree_insert(ctx-sh-rbtree,node);ngx_queue_insert_head(ctx-sh-queue,lr-queue);if(account){/同样地如果这是最后一个“域”的检查就更新 last 和 count返回 NGX_OK/lr-lastnow;lr-count0;returnNGX_OK;}/否则就令 count1把节点放到 ctx 上/lr-last0;lr-count1;ctx-nodelr;returnNGX_AGAIN;}5、Nginx 漏桶算法核心逻辑这段代码是 Nginx ngx_http_limit_req_module 模块的核心部分主要功能是检查当前请求是否超过限流阈值。ngx_http_limit_req_module 结合了漏桶算法leaky bucket用于处理请求速率限制和突发流量。这里不对 ngx_http_limit_req_module 的c代码做介绍 下面陈某用java 模拟一下上面的 nginx语言代码大概是下面这样的importjava.util.concurrent.BlockingQueue;importjava.util.concurrent.LinkedBlockingQueue;// 漏桶限流器类publicclassLeakyBucketRateLimiter{// 漏桶容量即漏桶最多能存储的请求数量privatefinalintcapacity;// 漏桶处理请求的速率单位为每秒处理的请求数privatefinalintrate;// 存储请求的队列模拟漏桶privatefinalBlockingQueueIntegerbucket;// 上次处理请求的时间戳单位为毫秒privatelonglastProcessTime;/*构造函数初始化漏桶限流器*paramcapacity 漏桶容量*paramrate 漏桶处理请求的速率*/publicLeakyBucketRateLimiter(intcapacity,intrate){this.capacitycapacity;this.raterate;this.bucketnewLinkedBlockingQueue(capacity);this.lastProcessTimeSystem.currentTimeMillis();}/*尝试添加一个请求到漏桶中*return如果请求被接受返回true否则返回false*/publicsynchronizedbooleanaddRequest(){// 计算从上次处理请求到现在应该流出的请求数量longnowSystem.currentTimeMillis();intleakedCount(int)((now-lastProcessTime)*rate/1000);if(leakedCount0){// 移除已经流出的请求for(inti0;iMath.min(leakedCount,bucket.size());i){bucket.poll();}lastProcessTimenow;}// 判断漏桶是否还有空间if(bucket.size()capacity){// 漏桶未满将请求放入桶中bucket.offer(1);returntrue;}else{// 漏桶已满拒绝请求returnfalse;}}publicstaticvoidmain(String[]args){// 创建一个漏桶限流器容量为 10每秒处理 2 个请求LeakyBucketRateLimiterlimiternewLeakyBucketRateLimiter(10,2);for(inti0;i20;i){if(limiter.addRequest()){System.out.println(Request i accepted);}else{System.out.println(Request i rejected);}try{// 模拟请求间隔 100 毫秒Thread.sleep(100);}catch(InterruptedExceptione){e.printStackTrace();}}}}关键性的代码是//代码1intleakedCount(int)((now-lastProcessTime)*rate/1000);// 代码2if(leakedCount0){// 代码3// 移除已经流出的请求for(inti0;iMath.min(leakedCount,bucket.size());i){bucket.poll();}// 代码4lastProcessTimenow;}先看下面的代码1intleakedCount(int)((now-lastProcessTime)*rate/1000);now 和 lastProcessTimenow 是通过 System.currentTimeMillis() 获取的当前系统时间戳单位为毫秒。它代表当前请求到来时的时间点。lastProcessTime 记录的是上一次处理请求的时间戳同样以毫秒为单位。now - lastProcessTime 时间间隔 计算出从上次处理请求到当前时刻所经过的时间间隔单位是毫秒。rate表示漏桶处理请求的速率单位是每秒处理的请求数。例如若 rate 为 2意味着漏桶每秒可以处理 2 个请求。(now - lastProcessTime) * rate这一步计算在 now - lastProcessTime 这段时间内如果漏桶一直以 rate 的速率处理请求理论上可以处理的请求数量。由于 rate 是每秒处理的请求数而 now - lastProcessTime 是时间间隔毫秒所以这里得到的结果是一个基于毫秒时间间隔的理论请求处理数量。(now - lastProcessTime) * rate / 1000因为 rate 是每秒处理的请求数而 now - lastProcessTime 是毫秒级的时间间隔为了将时间单位统一为秒需要将前面计算的结果除以 10001 秒 1000 毫秒。这样得到的结果就是在 now - lastProcessTime 这段时间内漏桶实际应该处理的请求数量。再看下面的代码2if(leakedCount0){// 移除已经流出的请求for(inti0;iMath.min(leakedCount,bucket.size());i){bucket.poll();}lastProcessTimenow;}当 leakedCount 0 时说明从上次处理请求到现在已经过去了一段时间漏桶应该有请求流出所以需要执行移除请求的操作。如果 leakedCount 为 0说明时间间隔过短还没有到下一次应该处理请求的时间不需要进行移除操作。再看下面的代码3for(inti0;iMath.min(leakedCount,bucket.size());i){bucket.poll();}-Math.min(leakedCount,bucket.size())取 leakedCount 和当前漏桶中实际请求数量bucket.size()的最小值。这是因为在某些情况下计算得出的应该流出的请求数量可能会超过漏桶中实际存在的请求数量。例如计算得出应该流出 5 个请求但漏桶中实际只有 3 个请求那么只能移除 3 个请求。所以使用 Math.min 函数确保不会移除超过漏桶中实际请求数量的请求。bucket.poll()bucket 是一个 BlockingQueue用于存储漏桶中的请求。poll() 方法是 BlockingQueue 接口提供的方法它的作用是移除并返回队列的头部元素。在这个循环中每次调用 bucket.poll() 就会从漏桶中移除一个请求模拟漏桶中请求的流出。再看下面的代码4lastProcessTimenow;lastProcessTime 记录的是上一次处理请求的时间。在完成了移除请求的操作后需要将 lastProcessTime 更新为当前时间now这样下次新的请求到来时就可以基于这个新的时间点来计算从这次处理请求到下一次请求到来时应该流出的请求数量。仔细去看 Nginx 漏桶算法核心逻辑和上面的java 逻辑是差不多的。上面的逻辑和前面的简单例子不同没有用定时器去进行 请求的 漏出而是通过时间范围一次性去减掉 流出的请求。所以漏桶其实很简单的。Nginx 为什么不用令牌桶算法?Nginx 是一个高性能的 HTTP 服务器和反向代理服务器广泛用于负载均衡、API 网关、静态资源服务等场景。在限流方面Nginx 使用的是 漏桶算法Leaky Bucket Algorithm而不是 令牌桶算法Token Bucket Algorithm。Nginx 选择漏桶算法而不是令牌桶算法的原因主要是平滑流量避免突发流量对后端服务造成冲击1. Nginx 限流设计目标平滑流量Nginx 的核心设计目标之一是 平滑流量避免突发流量对后端服务造成冲击。Nginx 作为反向代理或负载均衡器通常需要保护后端服务免受突发流量的影响因此漏桶算法更符合其设计目标。漏桶算法以固定的速率处理请求能够将突发的流量平滑为稳定的输出流量非常适合 Nginx 的使用场景。漏桶算法的特点以固定速率处理请求。能够有效平滑突发流量。- 令牌桶算法的特点允许突发流量适合需要处理突发请求的场景。Nginx 加入了burst 的 突发流量队列 允许了突发流量。2. Nginx 限流防止突发流量对后端服务的冲击Nginx 通常用于保护后端服务如应用服务器、数据库等避免它们被突发流量压垮。漏桶算法能够严格限制请求速率确保后端服务不会过载。漏桶算法的效果严格限制请求速率避免突发流量。适合保护后端服务的场景。- 令牌桶算法的效果允许突发流量可能对后端服务造成压力。Nginx 的设计目标之一是保护后端服务因此漏桶算法更适合其需求。3. Nginx 限流配置简单易于理解Nginx 的限流模块如 ngx_http_limit_req_module使用漏桶算法配置简单且易于理解。用户只需要设置速率和桶大小即可实现限流。漏桶算法的配置只需要设置速率rate和桶大小burst。例如limit_req zoneone burst5 rate10r/s;- 令牌桶算法的配置需要设置令牌生成速率和桶容量。配置相对复杂。Nginx 的设计哲学之一是简单易用漏桶算法的配置方式更符合这一理念。4. 漏桶算法的公平性漏桶算法以固定的速率处理请求确保每个请求都能按照先到先得的原则被处理。这种公平性非常适合 Nginx 的使用场景。漏桶算法的公平性请求按顺序处理避免某些请求长时间得不到处理。- 令牌桶算法的公平性允许突发流量可能导致某些请求被优先处理。Nginx 作为反向代理或负载均衡器需要公平地处理所有请求因此漏桶算法更适合。5. 令牌桶算法的局限性虽然令牌桶算法在某些场景下非常有用但它也存在一些局限性不适合 Nginx 的设计目标允许突发流量令牌桶算法允许突发流量可能对后端服务造成压力。- 实现复杂度较高令牌桶算法需要维护令牌生成逻辑增加了实现的复杂性和性能开销。- 不适合保护后端服务Nginx 的核心目标之一是保护后端服务而令牌桶算法可能无法有效防止突发流量对后端的冲击。Sentinel 什么场景用漏桶什么场景令牌桶 什么场景滑动窗口Sentinel 什么场景用漏桶什么场景令牌桶 什么场景滑动窗口前面讲了了。限流算法常见的有三种实现滑动时间窗口令牌桶算法漏桶算法。Sentinel 什么场景用漏桶什么场景令牌桶 什么场景滑动窗口而sentinel内部比较复杂默认限流模式是基于滑动时间窗口算法针对资源做统计一个资源队列 一个滑动窗口算法统计的数据较少内存使用不高。流控效果为排队等待的限流模式基于漏桶算法需要排队等待效果而流控规则的热点参数限流 是基于令牌桶算法参数较多只需要记录参数对应的请求时间信息单体限流能支持10Wqps高并发吗随着微服务架构的普及系统的服务通常会部署在多台服务器上此时就需要分布式限流来保证整个系统的稳定性。单体限流大部分场景是不行的。单体限流指针对单一服务器的情况通过限制单台服务器在单位时间内处理的请求数量防止服务器过载。于是我们需要 分布式限流。分布式限流包括中心化的限流中心化 本地结合的联邦式限流先看 基于中心化的限流方案 。基于中心化的限流方案通过一个中心化的限流器来控制所有服务器的请求。实现方式(1) 选择一个中心化的组件例如— Redis。(2) 定义限流规则例如可以设置每秒钟允许的最大请求数QPS并将这个值存储在 Redis 中(3) 对于每个请求服务器需要先向 Redis 请求令牌。(4) 如果获取到令牌说明请求可以被处理如果没有获取到令牌说明请求被限流可以返回一个错误信息或者稍后重试。高性能的中心化组件可以使用RedisLua来开发京东的抢购就是使用RedisLua完成的中心化 限流。并且无论是Nginx外部网关还是Zuul内部网关都可以使用RedisLua限流组件。自研中心化限流组件redislua分布式限流组件自研中心化限流组件redislua分布式限流组件---此脚本的环境 redis 内部不是运行在 nginx 内部---方法申请令牌----1failed---1success---paramkey key 限流关键字---paramapply 申请的令牌数量 local functionacquire(key,apply)local timesredis.call(TIME);--times[1]秒数--times[2]微秒数 local curr_mill_secondtimes[1]*1000000times[2];curr_mill_secondcurr_mill_second/1000;local cacheInforedis.pcall(HMGET,key,last_mill_second,curr_permits,max_permits,rate)---局部变量上次申请的时间 local last_mill_secondcacheInfo[1];---局部变量之前的令牌数 local curr_permitstonumber(cacheInfo[2]);---局部变量桶的容量 local max_permitstonumber(cacheInfo[3]);---局部变量令牌的发放速率 local ratecacheInfo[4];---局部变量本次的令牌数 local local_curr_permits0;if(type(last_mill_second)~booleanand last_mill_second~nil)then--计算时间段内的令牌数 local reverse_permitsmath.floor(((curr_mill_second-last_mill_second)/1000)*rate);--令牌总数 local expect_curr_permitsreverse_permitscurr_permits;--可以申请的令牌总数 local_curr_permitsmath.min(expect_curr_permits,max_permits);else--第一次获取令牌 redis.pcall(HSET,key,last_mill_second,curr_mill_second)local_curr_permitsmax_permits;end local result-1;--有足够的令牌可以申请if(local_curr_permits-apply0)then--保存剩余的令牌 redis.pcall(HSET,key,curr_permits,local_curr_permits-apply);--为下次的令牌获取保存时间 redis.pcall(HSET,key,last_mill_second,curr_mill_second)--返回令牌获取成功 result1;else--返回令牌获取失败 result-1;endreturnresult end--eg--/usr/local/redis/bin/redis-cli-a123456--eval/vagrant/LuaDemoProject/src/luaScript/redis/rate_limiter.lua key,acquire11--获取 sha编码的命令--/usr/local/redis/bin/redis-cli-a123456script load$(cat /vagrant/LuaDemoProject/src/luaScript/redis/rate_limiter.lua)--/usr/local/redis/bin/redis-cli-a123456script existscf43613f172388c34a1130a760fc699a5ee6f2a9--/usr/local/redis/bin/redis-cli-a123456evalshacf43613f172388c34a1130a760fc699a5ee6f2a91rate_limiter:seckill:1init11--/usr/local/redis/bin/redis-cli-a123456evalshacf43613f172388c34a1130a760fc699a5ee6f2a91rate_limiter:seckill:1acquire1--local rateLimiterShae4e49e4c7b23f0bf7a2bfee73e8a01629e33324b;---方法初始化限流Key---1success---paramkey key---parammax_permits 桶的容量---paramrate 令牌的发放速率 local functioninit(key,max_permits,rate)local rate_limit_inforedis.pcall(HMGET,key,last_mill_second,curr_permits,max_permits,rate)local org_max_permitstonumber(rate_limit_info[3])local org_raterate_limit_info[4]if(org_max_permitsnil)or(rate~org_rate or max_permits~org_max_permits)then redis.pcall(HMSET,key,max_permits,max_permits,rate,rate,curr_permits,max_permits)endreturn1;end--eg--/usr/local/redis/bin/redis-cli-a123456--eval/vagrant/LuaDemoProject/src/luaScript/redis/rate_limiter.lua key,init11--/usr/local/redis/bin/redis-cli-a123456--eval/vagrant/LuaDemoProject/src/luaScript/redis/rate_limiter.luarate_limiter:seckill:1,init11---方法删除限流Keylocal functiondelete(key)redis.pcall(DEL,key)return1;end--eg--/usr/local/redis/bin/redis-cli--eval/vagrant/LuaDemoProject/src/luaScript/redis/rate_limiter.lua key,delete local keyKEYS[1]local methodARGV[1]ifmethodacquirethenreturnacquire(key,ARGV[2],ARGV[3])elseif methodinitthenreturninit(key,ARGV[2],ARGV[3])elseif methoddeletethenreturndelete(key)else--ignore end第三方中心化限流组件Redisson 中心化限流实现除了自研还可以使用 成熟的第三方中心化限流组件。Redisson 提供了一个高性能的分布式限流组件 RRateLimiter基于 Redis 实现支持令牌桶算法。RRateLimiter 采用令牌桶思想和固定时间窗口trySetRate方法设置桶的大小利用redis key过期机制达到时间窗口目的控制固定时间窗口内允许通过的请求量。spring cloud gateway集成redis限流但属于网关层限流1. 使用 RRateLimiter 的demo首先通过一个简单的示例了解如何使用 RRateLimiter它创建了一个限流器并启动多个线程来获取令牌importorg.redisson.Redisson;// 导入 Redisson 的核心类用于创建 Redisson 客户端importorg.redisson.api.RRateLimiter;// 导入 RRateLimiter 接口用于实现分布式限流importorg.redisson.api.RedissonClient;// 导入 RedissonClient 接口用于与 Redis 进行交互importorg.redisson.config.Config;// 导入 Redisson 的配置类用于配置 Redis 连接importjava.util.concurrent.CountDownLatch;// 导入 CountDownLatch 类用于控制线程同步publicclassRateLimiterDemo{// 定义一个公共类 RateLimiterDemopublicstaticvoidmain(String[]args)throwsInterruptedException{// 主方法程序入口可能抛出 InterruptedExceptionRRateLimiterrateLimitercreateRateLimiter();// 创建一个 RRateLimiter 实例inttotalThreads20;// 定义总线程数为 20CountDownLatchlatchnewCountDownLatch(totalThreads);// 创建一个 CountDownLatch 实例初始计数为 totalThreadslongstartTimeSystem.currentTimeMillis();// 记录开始时间用于计算总耗时for(inti0;itotalThreads;i){// 循环创建并启动 20 个线程newThread(()-{// 创建一个新线程rateLimiter.acquire(1);// 每个线程尝试获取 1 个令牌若令牌不足则阻塞等待latch.countDown();// 线程完成后调用 countDown() 方法减少计数器}).start();// 启动线程}latch.await();// 主线程等待直到所有子线程完成System.out.println(Total elapsed time: (System.currentTimeMillis()-startTime) ms);// 打印总耗时}/*创建并配置RRateLimiter的方法*return配置好的RRateLimiter实例/privatestaticRRateLimitercreateRateLimiter(){// 创建并配置 RRateLimiter 的方法ConfigconfignewConfig();// 创建一个新的 Redisson 配置实例config.useSingleServer()// 配置使用单一 Redis 服务器.setAddress(redis://127.0.0.1:6379)// 设置 Redis 服务器地址.setTimeout(1000000);// 设置连接超时时间毫秒RedissonClientredissonRedisson.create(config);// 根据配置创建一个 Redisson 客户端实例RRateLimiterrateLimiterredisson.getRateLimiter(myRateLimiter);// 获取名为 myRateLimiter 的 RRateLimiter 实例rateLimiter.trySetRate(RRateLimiter.RateType.OVERALL,3,5,RateIntervalUnit.SECONDS);// 初始化限流器设置全局速率为每秒5个令牌returnrateLimiter;// 返回配置好的限流器实例}}以下是对 Redisson 限流组件的源码分析和关键实现原理的总结2. 限流器初始化限流器通过 RedissonClient.getRateLimiter(String name) 获取并通过 trySetRate 方法设置限流规则。RRateLimiterrateLimiterredisson.getRateLimiter(myRateLimiter);rateLimiter.trySetRate(RateType.OVERALL,3,5,RateIntervalUnit.SECONDS);RateType.OVERALL表示所有实例共享同一个限流器。trySetRate 方法通过 Redis 的 EVAL 命令设置限流规则包括令牌生成速率rate、时间间隔interval和限流类型type。rateLimiter.trySetRate(RateType.OVERALL, 3, 5, RateIntervalUnit.SECONDS); 这一句的意思设置全局限流规则所有客户端共享同一个限流器。限流规则每 5 秒内最多允许 3 个请求通过。时间单位时间间隔为秒。Redisson 的限流器支持分布式环境多个服务实例可以共享同一个限流器。RateType.OVERALL所有实例共享同一个限流器。RateType.PER_CLIENT每个客户端实例独立限流。限流规则存储在 Redis 的哈希结构中例如HSET myRateLimiter rate 3 interval 5000 type 0rate令牌生成速率。interval时间间隔毫秒。type限流类型0 表示 OVERALL1 表示 PER_CLIENT。3. trySetRateAsync 设置限流规则源码分析trySetRate方法设置桶的大小OverridepublicRFutureBooleantrySetRateAsync(RateTypetype,longrate,longrateInterval,RateIntervalUnitunit){returncommandExecutor.evalWriteAsync(getName(),LongCodec.INSTANCE,RedisCommands.EVAL_BOOLEAN,redis.call(hsetnx, KEYS[1], rate, ARGV[1]);redis.call(hsetnx, KEYS[1], interval, ARGV[2]);return redis.call(hsetnx, KEYS[1], type, ARGV[3]);,Collections.ObjectsingletonList(getName()),rate,unit.toMillis(rateInterval),type.ordinal());}使用 EVAL 命令原子性地设置限流规则。OverridepublicRFutureBooleantrySetRateAsync(RateTypetype,longrate,longrateInterval,RateIntervalUnitunit)这是一个异步方法用于尝试设置限流器的速率。参数type限流类型RateType可以是 全局限流 或 单客户端限流。rate限流速率表示在 rateInterval 时间范围内允许的请求数量。rateInterval限流时间间隔。unit时间间隔的单位如秒、毫秒等。返回值返回一个 RFuture表示异步操作的结果。true 表示设置成功false 表示设置失败通常是因为限流器已经存在。trySetRateAsync 调用的是 commandExecutor.evalWriteAsync方法。returncommandExecutor.evalWriteAsync(getName(),LongCodec.INSTANCE,RedisCommands.EVAL_BOOLEAN,redis.call(hsetnx, KEYS[1], rate, ARGV[1]);redis.call(hsetnx, KEYS[1], interval, ARGV[2]);return redis.call(hsetnx, KEYS[1], type, ARGV[3]);,Collections.ObjectsingletonList(getName()),rate,unit.toMillis(rateInterval),type.ordinal());commandExecutor.evalWriteAsync执行一个 Lua 脚本用于在 Redis 中原子性地设置限流器的参数。参数getName()限流器的名称作为 Redis 的 Key。LongCodec.INSTANCE用于编码和解码 Long 类型的值。RedisCommands.EVAL_BOOLEAN表示 Lua 脚本的返回类型为布尔值。Lua 脚本具体的限流逻辑。Collections.singletonList(getName())Lua 脚本的 KEYS 参数传入限流器的名称。rate、unit.toMillis(rateInterval)、type.ordinal()Lua 脚本的 ARGV 参数传入限流速率、时间间隔和限流类型。具体的限流 Lua 脚本redis.call(hsetnx,KEYS[1],rate,ARGV[1]);redis.call(hsetnx,KEYS[1],interval,ARGV[2]);returnredis.call(hsetnx,KEYS[1],type,ARGV[3]);作用在 Redis 中原子性地设置限流器的参数。逐行分析(1) redis.call(‘hsetnx’, KEYS[1], ‘rate’, ARGV[1])使用 hsetnx 命令将 限流速率rate写入 Redis 的 Hash 结构中。hsetnx 是 “Hash Set If Not Exists” 的缩写只有在字段不存在时才会设置值。KEYS[1] 是限流器的名称ARGV[1] 是限流速率。(2) redis.call(‘hsetnx’, KEYS[1], ‘interval’, ARGV[2])使用 hsetnx 命令将 限流时间间隔interval写入 Redis 的 Hash 结构中。ARGV[2] 是时间间隔以毫秒为单位。(3) return redis.call(‘hsetnx’, KEYS[1], ‘type’, ARGV[3])使用 hsetnx 命令将限流类型type写入 Redis 的 Hash 结构中。ARGV[3] 是限流类型的枚举值RateType.ordinal()。返回 hsetnx 的结果表示是否设置成功。trySetRateAsync 方法通过 Lua 脚本在 Redis 中原子性地设置限流器的参数。该方法的设计充分考虑了原子性、灵活性和性能是 Redisson 分布式限流功能的核心实现之一。使用 hsetnx 命令确保参数只设置一次避免重复设置。异步执行提高了性能适用于高并发场景。具体的特点如下(1) 原子性操作使用 Lua 脚本确保设置限流参数的原子性避免并发问题。hsetnx 命令保证只有在字段不存在时才会设置值避免覆盖已有配置。(2) 异步执行通过 evalWriteAsync 方法异步执行 Lua 脚本提高性能。(3) 灵活性支持动态设置限流速率、时间间隔和限流类型适用于不同的限流场景。(4) Redis 数据结构使用 Redis 的 Hash 结构存储限流器的参数便于管理和扩展4. 获取令牌限流的核心逻辑是通过 tryAcquire 方法尝试获取令牌。如果获取成功请求通过否则请求被拒绝。booleanallowedrateLimiter.tryAcquire(1);if(!allowed){thrownewRuntimeException(Rate limit exceeded);}tryAcquire 方法的获取令牌如果当前时间窗口内有足够令牌则减少令牌数量并返回成功。如果令牌不足返回失败。5. tryAcquireAsync 方法令牌桶限流源码分析privateTRFutureTtryAcquireAsync(RedisCommandTcommand,Longvalue){returncommandExecutor.evalWriteAsync(getName(),LongCodec.INSTANCE,command,local rate redis.call(hget, KEYS[1], rate);local interval redis.call(hget, KEYS[1], interval);local type redis.call(hget, KEYS[1], type);assert(rate ~ false and interval ~ false and type ~ false, RateLimiter is not initialized)local valueName KEYS[2];if type 1 then valueName KEYS[3];end;local currentValue redis.call(get, valueName);if currentValue ~ false then if tonumber(currentValue) tonumber(ARGV[1]) then return redis.call(pttl, valueName);else redis.call(decrby, valueName, ARGV[1]);return nil;end;else redis.call(set, valueName, rate, px, interval);redis.call(decrby, valueName, ARGV[1]);return nil;end;,Arrays.ObjectasList(getName(),getValueName(),getClientValueName()),value);}lua脚本介绍一下1. 初始化检查 代码首先确保限流器的速率rate、时间间隔interval和类型type已初始化。local rateredis.call(hget,KEYS[1],rate)local intervalredis.call(hget,KEYS[1],interval)local typeredis.call(hget,KEYS[1],type)assert(rate~falseand interval~falseand type~false,RateLimiter is not initialized)说明从 Redis 的 Hash 结构中获取限流器的配置。如果配置不存在抛出异常提示限流器未初始化。2. 获取当前令牌数获取当前可用的令牌数。local currentValueredis.call(get,valueName)说明valueName 是存储当前令牌数的键名。如果 currentValue 为 false表示第一次请求需要初始化令牌数。3. 处理第一次请求初始化令牌数和请求记录。redis.call(set,valueName,rate)redis.call(zadd,permitsName,ARGV[2],struct.pack(fI,ARGV[3],ARGV[1]))redis.call(decrby,valueName,ARGV[1])returnnil说明将当前令牌数设置为最大速率值rate。使用有序集合permitsName记录当前请求的时间戳和令牌数。减少当前令牌数并返回 nil 表示成功。4. 处理非第一次请求检查过期令牌 并更新当前令牌数。--获取已过期的请求初始时间 至 当前时间ARGV[2]-时间间隔interval 准备清理失效的令牌数据 local expiredValuesredis.call(zrangebyscore,permitsName,0,tonumber(ARGV[2])-interval)local released0--初始化拟新增失效令牌数--遍历过期的请求释放相应的令牌fori,v inipairs(expiredValues)dolocal random,permitsstruct.unpack(fI,v)releasedreleasedpermitsendifreleased0then redis.call(zrem,permitsName,unpack(expiredValues))currentValuetonumber(currentValue)released redis.call(set,valueName,currentValue)end查找已过期的请求时间戳小于当前时间减去时间间隔。计算并释放过期请求占用的令牌数。更新当前令牌数。5. 检查令牌是否足够判断当前令牌数是否满足请求。说明如果令牌不足计算需要等待的时间并返回等待时间。如果令牌足够记录当前请求并减少令牌数返回 nil 表示成功。lua 脚本 关键点总结(1) 初始化检查 确保限流器的配置已正确初始化。(2) 第一次请求 初始化令牌数和请求记录。(3) 非第一次请求 检查并释放过期令牌更新当前令牌数。(4) 令牌检查 判断当前令牌数是否满足请求如果不足则返回等待时间。(5) 数据结构使用 Redis 的 Hash 结构存储限流器配置。 - 使用有序集合zset记录请求的时间戳和令牌数。6. 使用场景Redisson 的限流器适用于多种场景例如接口限流限制接口的访问频率。用户限流基于用户 ID 限制请求。IP 限流限制来自特定 IP 的请求。7. 优势高性能基于 Redis 的高性能和原子性操作。分布式支持多个服务实例可以共享同一个限流器。灵活配置支持多种限流类型和时间单位。通过 Redisson 的 RRateLimiter开发者可以轻松实现分布式限流功能满足高并发场景下的需求。中心化限流能支持10Wqps高并发吗Redisson 的 RRateLimiter 是一个基于 Redis 实现的分布式限流组件支持高并发场景。然而不能 支持 10万 QPS 的限流。为什么关键限制(1) Redis 单实例性能RRateLimiter 的限流 QPS 上限取决于 Redis 单实例的性能。如果 Redis 单实例的性能瓶颈在 1 万 QPS 左右那么单个 RRateLimiter 无法突破这一限制。(2) 单限流器的性能瓶颈单个 RRateLimiter 的性能受限于 Redis 的读写能力。即使 Redis 集群的总体性能较高单个限流器的 QPS 上限仍然受限于单个 Redis 实例。Redisson 的 RRateLimiter 本身无法直接支持 10万 QPS 的限流因为其性能受限于单个 Redis 实例的处理能力。不过通过拆分多个限流器和优化分布式架构可以实现更高的总体 QP10Wqps的高扩展、自伸缩、自适应限流策略Sentinel Nacos 结合的 中心协调本地流控结合的 分布式方案实现 10Wqps的高扩展、自伸缩、自适应限流策略架构设计中心协调 本地流控结合中心协调通过 Nacos 作为配置中心集中管理限流规则确保全局一致性。本地流控每个服务实例使用 Sentinel 进行本地限流减少对中心节点的依赖提高性能。分布式架构Sentinel本地流控作为流量控制、熔断降级的中间件负责本地流控和规则检查。Sentinel 提供本地限流能力结合 Nacos 的动态配置功能实现规则的实时更新Nacos 中心协调充当配置中心对限流规则进行集中管理和动态推送。使用 Spring Cloud Alibaba 和 Nacos 实现服务注册与发现确保服务的动态扩展。中心协调借助 Nacos 存储和推送限流规则保证各节点规则的一致性。本地流控每个节点依据本地的 Sentinel 实例执行限流操作降低对中心节点的依赖。中心协调 本地流控结合 特点1 规则动态更新将 Sentinel 的限流规则存储在 Nacos 中通过 Nacos 的配置管理功能动态更新限流规则。Sentinel 支持从 Nacos 拉取规则并实时生效无需重启服务。2 服务扩展使用 Nacos 的服务注册与发现功能动态感知服务实例的增减确保限流规则的全局一致性。在高并发场景下通过水平扩展服务实例分散流量压力。Sentinel 通过nacos 的发布订阅进行 速率调整在 Sentinel 中可以通过与 Nacos 的集成利用 Nacos 的发布订阅功能来实现速率调整以下是一般的步骤引入相关依赖在项目的构建文件如 Maven 的 pom.xml 或 Gradle 的 build.gradle中引入 Sentinel 与 Nacos 相关的依赖dependencygroupIdcom.alibaba.csp/groupIdartifactIdsentinel-datasource-nacos/artifactIdversion${sentinel.version}/version/dependency配置 Nacos 数据源在应用的配置文件如 application.yml 或 application.properties中配置 Sentinel 使用 Nacos 作为数据源来获取流量控制规则等配置信息示例如下spring:cloud:sentinel:datasource:# 定义数据源名称 ds1:nacos:server-addr:nacos-server-address:8848#Nacos服务器地址 dataId:sentinel-rules #Nacos中存储Sentinel规则的DataID groupId:DEFAULT_GROUP #Nacos中存储Sentinel规则的分组 data-type:json # 规则数据格式为JSON定义 Sentinel 规则在 Nacos 的配置管理界面或通过 Nacos 的 API创建一个名为sentinel-rules与配置文件中 dataId 一致的配置项用于存储 Sentinel 的流量控制规则。以下是一个简单的流量控制规则示例[{resource:your_resource_name,limitApp:default,grade:1,count:10,strategy:0,controlBehavior:0,clusterMode:false}]上述规则表示对名为your_resource_name的资源进行流量控制QPS 阈值为 10。其中resource要保护的资源名称。limitApp来源应用default表示所有应用。grade限流阈值类型1 表示 QPS 限流0 表示线程数限流。count限流阈值。strategy流控模式0 为直接模式1 为关联模式2 为链路模式。controlBehavior流控效果0 为快速失败1 为 Warm Up2 为排队等待。clusterMode是否为集群模式。在应用中获取并应用规则在应用启动时Sentinel 会从 Nacos 中获取配置的规则并应用到相应的资源上。当需要调整速率时只需要在 Nacos 中修改sentinel-rules配置项中的count值等相关参数Sentinel 会通过 Nacos 的发布订阅机制实时感知到规则的变化并自动更新应用中的流量控制策略。代码中动态获取规则在代码中可以通过 Sentinel 的 API 来动态获取当前应用的流量控制规则示例代码如下importcom.alibaba.csp.sentinel.slots.block.flow.FlowRule;importcom.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;importjava.util.List;publicclassSentinelRuleUtils{publicstaticvoidprintCurrentRules(){// 获取所有流量控制规则ListFlowRulerulesFlowRuleManager.getRules();for(FlowRulerule:rules){System.out.println(Resource: rule.getResource(), QPS Limit: rule.getCount());}}}你可以在需要的地方调用SentinelRuleUtils.printCurrentRules()方法来查看当前应用的流量控制规则。