<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          6種限流實(shí)現(xiàn),附代碼![通俗易懂]

          共 12415字,需瀏覽 25分鐘

           ·

          2023-08-28 05:13

          作者 | 磊哥

          來源 | Java中文社群(ID:javacn666)

          轉(zhuǎn)載請(qǐng)聯(lián)系授權(quán)(微信ID:GG_Stone

          限流是一種控制訪問速率的策略,用于限制系統(tǒng)、服務(wù)或API接口的請(qǐng)求頻率或數(shù)量。它的目的是為了保護(hù)系統(tǒng)免受過多請(qǐng)求的影響,防止系統(tǒng)因過載而崩潰或變得不可用。限流是一種重要的性能優(yōu)化和資源保護(hù)機(jī)制。

          限流的好處有以下幾個(gè):

          • 保護(hù)系統(tǒng)穩(wěn)定性:如果系統(tǒng)接受太多請(qǐng)求,超出了其處理能力,可能導(dǎo)致系統(tǒng)崩潰或響應(yīng)時(shí)間急劇增加,從而影響用戶體驗(yàn)。限流可以幫助控制請(qǐng)求速率,確保系統(tǒng)穩(wěn)定運(yùn)行。
          • 保護(hù)系統(tǒng)可用性:有些資源可能是有限的,如數(shù)據(jù)庫連接、網(wǎng)絡(luò)帶寬、內(nèi)存等。通過限制對(duì)這些資源的訪問,可以防止它們被耗盡,從而保護(hù)系統(tǒng)的可用性。
          • 防止惡意攻擊:限流可以減少惡意攻擊和濫用系統(tǒng)資源的風(fēng)險(xiǎn)。例如,防止 DDoS(分布式拒絕服務(wù))攻擊或惡意爬蟲訪問網(wǎng)站。
          • 公平分配資源:對(duì)于多個(gè)客戶或用戶,限流可以確保資源公平分配。每個(gè)客戶都有限制的訪問機(jī)會(huì),而不會(huì)被某個(gè)客戶壟斷。
          • 避免雪崩效應(yīng):當(dāng)系統(tǒng)中的一個(gè)組件或服務(wù)發(fā)生故障時(shí),可能會(huì)導(dǎo)致大量請(qǐng)求涌入其他正常的組件或服務(wù),進(jìn)一步加劇系統(tǒng)負(fù)載,限流可以防止這種雪崩效應(yīng)。

          限流分類

          限流的實(shí)現(xiàn)方案有很多種,磊哥這里稍微理了一下,限流的分類如下所示:

          1. 合法性驗(yàn)證限流:比如驗(yàn)證碼、IP 黑名單等,這些手段可以有效的防止惡意攻擊和爬蟲采集。
          2. 容器限流:比如 Tomcat、Nginx 等限流手段,其中 Tomcat 可以設(shè)置最大線程數(shù)(maxThreads),當(dāng)并發(fā)超過最大線程數(shù)會(huì)排隊(duì)等待執(zhí)行;而 Nginx 提供了兩種限流手段:一是控制速率,二是控制并發(fā)連接數(shù)。
          3. 服務(wù)端限流:比如我們?cè)诜?wù)器端通過限流算法實(shí)現(xiàn)限流,此項(xiàng)也是我們本文介紹的重點(diǎn)。

          合法性驗(yàn)證限流為最常規(guī)的業(yè)務(wù)代碼,就是普通的驗(yàn)證碼和 IP 黑名單系統(tǒng),本文就不做過多的敘述了,我們重點(diǎn)來看下后兩種限流的實(shí)現(xiàn)方案:容器限流和服務(wù)端限流。

          一、容器限流

          1.1 Tomcat 限流

          Tomcat 8.5 版本的最大線程數(shù)在 conf/server.xml 配置中,如下所示:

                
                
                  <Connector port="8080" protocol="HTTP/1.1"
                    connectionTimeout="20000"
                    maxThreads="150"
                    redirectPort="8443" />

          其中 maxThreads 就是 Tomcat 的最大線程數(shù),當(dāng)請(qǐng)求的并發(fā)大于此值(maxThreads)時(shí),請(qǐng)求就會(huì)排隊(duì)執(zhí)行,這樣就完成了限流的目的。

          小貼士:maxThreads 的值可以適當(dāng)?shù)恼{(diào)大一些,此值默認(rèn)為 150(Tomcat 版本 8.5.42),但這個(gè)值也不是越大越好,要看具體的硬件配置,需要注意的是每開啟一個(gè)線程需要耗用 1MB 的 JVM 內(nèi)存空間用于作為線程棧之用,并且線程越多 GC 的負(fù)擔(dān)也越重。最后需要注意一下,操作系統(tǒng)對(duì)于進(jìn)程中的線程數(shù)有一定的限制,Windows 每個(gè)進(jìn)程中的線程數(shù)不允許超過 2000,Linux 每個(gè)進(jìn)程中的線程數(shù)不允許超過 1000。

          1.2 Nginx 限流

          Nginx 提供了兩種限流手段:一是控制速率,二是控制并發(fā)連接數(shù)。

          控制速率

          我們需要使用 limit_req_zone 用來限制單位時(shí)間內(nèi)的請(qǐng)求數(shù),即速率限制,示例配置如下:

                
                limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s;
          server { 
              location / { 
                  limit_req zone=mylimit;
              }
          }

          以上配置表示,限制每個(gè) IP 訪問的速度為 2r/s,因?yàn)?Nginx 的限流統(tǒng)計(jì)是基于毫秒的,我們?cè)O(shè)置的速度是 2r/s,轉(zhuǎn)換一下就是 500ms 內(nèi)單個(gè) IP 只允許通過 1 個(gè)請(qǐng)求,從 501ms 開始才允許通過第 2 個(gè)請(qǐng)求。

          我們使用單 IP 在 10ms 內(nèi)發(fā)并發(fā)送了 6 個(gè)請(qǐng)求的執(zhí)行結(jié)果如下:9809ce39e8a829666654b6b02e80f804.webp

          從以上結(jié)果可以看出他的執(zhí)行符合我們的預(yù)期,只有 1 個(gè)執(zhí)行成功了,其他的 5 個(gè)被拒絕了(第 2 個(gè)在 501ms 才會(huì)被正常執(zhí)行)。速率限制升級(jí)版上面的速率控制雖然很精準(zhǔn)但是應(yīng)用于真實(shí)環(huán)境未免太苛刻了,真實(shí)情況下我們應(yīng)該控制一個(gè) IP 單位總時(shí)間內(nèi)的總訪問次數(shù),而不是像上面那么精確但毫秒,我們可以使用 burst 關(guān)鍵字開啟此設(shè)置,示例配置如下:

                
                limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s;
          server { 
              location / { 
                  limit_req zone=mylimit burst=4;
              }
          }

          burst=4 表示每個(gè) IP 最多允許4個(gè)突發(fā)請(qǐng)求,如果單個(gè) IP 在 10ms 內(nèi)發(fā)送 6 次請(qǐng)求的結(jié)果如下:071b39e41e512112b7cc80438ca11c10.webp從以上結(jié)果可以看出,有 1 個(gè)請(qǐng)求被立即處理了,4 個(gè)請(qǐng)求被放到 burst 隊(duì)列里排隊(duì)執(zhí)行了,另外 1 個(gè)請(qǐng)求被拒絕了。

          控制并發(fā)數(shù)

          利用 limit_conn_zone 和 limit_conn 兩個(gè)指令即可控制并發(fā)數(shù),示例配置如下:

                
                limit_conn_zone $binary_remote_addr zone=perip:10m;
          limit_conn_zone $server_name zone=perserver:10m;
          server {
              ...
              limit_conn perip 10;
              limit_conn perserver 100;
          }

          其中 limit_conn perip 10 表示限制單個(gè) IP 同時(shí)最多能持有 10 個(gè)連接;limit_conn perserver 100 表示 server 同時(shí)能處理并發(fā)連接的總數(shù)為 100 個(gè)。

          小貼士:只有當(dāng) request header 被后端處理后,這個(gè)連接才進(jìn)行計(jì)數(shù)。

          二、服務(wù)端限流

          服務(wù)端限流需要配合限流的算法來執(zhí)行,而算法相當(dāng)于執(zhí)行限流的“大腦”,用于指導(dǎo)限制方案的實(shí)現(xiàn)。

          有人看到「算法」兩個(gè)字可能就暈了,覺得很深?yuàn)W,其實(shí)并不是,算法就相當(dāng)于操作某個(gè)事務(wù)的具體實(shí)現(xiàn)步驟匯總,其實(shí)并不難懂,不要被它的表象給嚇到哦~

          限流的常見實(shí)現(xiàn)算法有以下三種:

          1. 時(shí)間窗口算法
          2. 漏桶算法
          3. 令牌算法

          接下來我們分別看來。

          2.1 時(shí)間窗口算法

          所謂的滑動(dòng)時(shí)間算法指的是以當(dāng)前時(shí)間為截止時(shí)間,往前取一定的時(shí)間,比如往前取 60s 的時(shí)間,在這 60s 之內(nèi)運(yùn)行最大的訪問數(shù)為 100,此時(shí)算法的執(zhí)行邏輯為,先清除 60s 之前的所有請(qǐng)求記錄,再計(jì)算當(dāng)前集合內(nèi)請(qǐng)求數(shù)量是否大于設(shè)定的最大請(qǐng)求數(shù) 100,如果大于則執(zhí)行限流拒絕策略,否則插入本次請(qǐng)求記錄并返回可以正常執(zhí)行的標(biāo)識(shí)給客戶端。

          滑動(dòng)時(shí)間窗口如下圖所示:4577c83531c0a72bbdea7eb4efdefc0b.webp其中每一小個(gè)表示 10s,被紅色虛線包圍的時(shí)間段則為需要判斷的時(shí)間間隔,比如 60s 秒允許 100 次請(qǐng)求,那么紅色虛線部分則為 60s。

          我們可以借助 Redis 的有序集合 ZSet 來實(shí)現(xiàn)時(shí)間窗口算法限流,實(shí)現(xiàn)的過程是先使用 ZSet 的 key 存儲(chǔ)限流的 ID,score 用來存儲(chǔ)請(qǐng)求的時(shí)間,每次有請(qǐng)求訪問來了之后,先清空之前時(shí)間窗口的訪問量,統(tǒng)計(jì)現(xiàn)在時(shí)間窗口的個(gè)數(shù)和最大允許訪問量對(duì)比,如果大于等于最大訪問量則返回 false 執(zhí)行限流操作,負(fù)責(zé)允許執(zhí)行業(yè)務(wù)邏輯,并且在 ZSet 中添加一條有效的訪問記錄,具體實(shí)現(xiàn)代碼如下。

          我們借助 Jedis 包來操作 Redis,實(shí)現(xiàn)在 pom.xml 添加 Jedis 框架的引用,配置如下:

                
                <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
          <dependency>
              <groupId>redis.clients</groupId>
              <artifactId>jedis</artifactId>
              <version>3.3.0</version>
          </dependency>

          具體的 Java 實(shí)現(xiàn)代碼如下:

                
                import redis.clients.jedis.Jedis;

          public class RedisLimit {
              // Redis 操作客戶端
              static Jedis jedis = new Jedis("127.0.0.1"6379);

              public static void main(String[] args) throws InterruptedException {
                  for (int i = 0; i < 15; i++) {
                      boolean res = isPeriodLimiting("java"310);
                      if (res) {
                          System.out.println("正常執(zhí)行請(qǐng)求:" + i);
                      } else {
                          System.out.println("被限流:" + i);
                      }
                  }
                  // 休眠 4s
                  Thread.sleep(4000);
                  // 超過最大執(zhí)行時(shí)間之后,再?gòu)陌l(fā)起請(qǐng)求
                  boolean res = isPeriodLimiting("java"310);
                  if (res) {
                      System.out.println("休眠后,正常執(zhí)行請(qǐng)求");
                  } else {
                      System.out.println("休眠后,被限流");
                  }
              }

              /**
               * 限流方法(滑動(dòng)時(shí)間算法)
               * @param key      限流標(biāo)識(shí)
               * @param period   限流時(shí)間范圍(單位:秒)
               * @param maxCount 最大運(yùn)行訪問次數(shù)
               * @return
               */

              private static boolean isPeriodLimiting(String key, int period, int maxCount) {
                  long nowTs = System.currentTimeMillis(); // 當(dāng)前時(shí)間戳
                  // 刪除非時(shí)間段內(nèi)的請(qǐng)求數(shù)據(jù)(清除老訪問數(shù)據(jù),比如 period=60 時(shí),標(biāo)識(shí)清除 60s 以前的請(qǐng)求記錄)
                  jedis.zremrangeByScore(key, 0, nowTs - period * 1000);
                  long currCount = jedis.zcard(key); // 當(dāng)前請(qǐng)求次數(shù)
                  if (currCount >= maxCount) {
                      // 超過最大請(qǐng)求次數(shù),執(zhí)行限流
                      return false;
                  }
                  // 未達(dá)到最大請(qǐng)求數(shù),正常執(zhí)行業(yè)務(wù)
                  jedis.zadd(key, nowTs, "" + nowTs); // 請(qǐng)求記錄 +1
                  return true;
              }
          }

          以上程序的執(zhí)行結(jié)果為:

          正常執(zhí)行請(qǐng)求:0

          正常執(zhí)行請(qǐng)求:1

          正常執(zhí)行請(qǐng)求:2

          正常執(zhí)行請(qǐng)求:3

          正常執(zhí)行請(qǐng)求:4

          正常執(zhí)行請(qǐng)求:5

          正常執(zhí)行請(qǐng)求:6

          正常執(zhí)行請(qǐng)求:7

          正常執(zhí)行請(qǐng)求:8

          正常執(zhí)行請(qǐng)求:9

          被限流:10

          被限流:11

          被限流:12

          被限流:13

          被限流:14

          休眠后,正常執(zhí)行請(qǐng)求

          此實(shí)現(xiàn)方式存在的缺點(diǎn)有兩個(gè):

          • 使用 ZSet 存儲(chǔ)有每次的訪問記錄,如果數(shù)據(jù)量比較大時(shí)會(huì)占用大量的空間,比如 60s 允許 100W 訪問時(shí);
          • 此代碼的執(zhí)行非原子操作,先判斷后增加,中間空隙可穿插其他業(yè)務(wù)邏輯的執(zhí)行,最終導(dǎo)致結(jié)果不準(zhǔn)確。

          2.1 漏桶算法

          漏桶算法的靈感源于漏斗,如下圖所示:

          9acbb63008c5545ed2e03ac4e3279b75.webpimage.png

          滑動(dòng)時(shí)間算法有一個(gè)問題就是在一定范圍內(nèi),比如 60s 內(nèi)只能有 10 個(gè)請(qǐng)求,當(dāng)?shù)谝幻霑r(shí)就到達(dá)了 10 個(gè)請(qǐng)求,那么剩下的 59s 只能把所有的請(qǐng)求都給拒絕掉,而漏桶算法可以解決這個(gè)問題。

          漏桶算法類似于生活中的漏斗,無論上面的水流倒入漏斗有多大,也就是無論請(qǐng)求有多少,它都是以均勻的速度慢慢流出的。當(dāng)上面的水流速度大于下面的流出速度時(shí),漏斗會(huì)慢慢變滿,當(dāng)漏斗滿了之后就會(huì)丟棄新來的請(qǐng)求;當(dāng)上面的水流速度小于下面流出的速度的話,漏斗永遠(yuǎn)不會(huì)被裝滿,并且可以一直流出。

          漏桶算法的實(shí)現(xiàn)步驟是,先聲明一個(gè)隊(duì)列用來保存請(qǐng)求,這個(gè)隊(duì)列相當(dāng)于漏斗,當(dāng)隊(duì)列容量滿了之后就放棄新來的請(qǐng)求,然后重新聲明一個(gè)線程定期從任務(wù)隊(duì)列中獲取一個(gè)或多個(gè)任務(wù)進(jìn)行執(zhí)行,這樣就實(shí)現(xiàn)了漏桶算法。

          上面我們演示 Nginx 的控制速率其實(shí)使用的就是漏桶算法,當(dāng)然我們也可以借助 Redis 很方便的實(shí)現(xiàn)漏桶算法。

          我們可以使用 Redis 4.0 版本中提供的 Redis-Cell 模塊,該模塊使用的是漏斗算法,并且提供了原子的限流指令,而且依靠 Redis 這個(gè)天生的分布式程序就可以實(shí)現(xiàn)比較完美的限流了。Redis-Cell 實(shí)現(xiàn)限流的方法也很簡(jiǎn)單,只需要使用一條指令 cl.throttle 即可,使用示例如下:

                
                > cl.throttle mylimit 15 30 60
          1)(integer)0 # 0 表示獲取成功,1 表示拒絕
          2)(integer)15 # 漏斗容量
          3)(integer)14 # 漏斗剩余容量
          4)(integer)-1 # 被拒絕之后,多長(zhǎng)時(shí)間之后再試(單位:秒)-1 表示無需重試
          5)(integer)2 # 多久之后漏斗完全空出來

          其中 15 為漏斗的容量,30 / 60s 為漏斗的速率。

          2.3 令牌算法

          在令牌桶算法中有一個(gè)程序以某種恒定的速度生成令牌,并存入令牌桶中,而每個(gè)請(qǐng)求需要先獲取令牌才能執(zhí)行,如果沒有獲取到令牌的請(qǐng)求可以選擇等待或者放棄執(zhí)行,如下圖所示:

          3b5be318798a5aebf57d76c3c2cb5f02.webp我們可以使用 Google 開源的 guava 包,很方便的實(shí)現(xiàn)令牌桶算法,首先在 pom.xml 添加 guava 引用,配置如下:

                
                <!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
          <dependency>
              <groupId>com.google.guava</groupId>
              <artifactId>guava</artifactId>
              <version>28.2-jre</version>
          </dependency>

          具體實(shí)現(xiàn)代碼如下:

                
                import com.google.common.util.concurrent.RateLimiter;

          import java.time.Instant;

          /**
           * Guava 實(shí)現(xiàn)限流
           */

          public class RateLimiterExample {
              public static void main(String[] args) {
                  // 每秒產(chǎn)生 10 個(gè)令牌(每 100 ms 產(chǎn)生一個(gè))
                  RateLimiter rt = RateLimiter.create(10);
                  for (int i = 0; i < 11; i++) {
                      new Thread(() -> {
                          // 獲取 1 個(gè)令牌
                          rt.acquire();
                          System.out.println("正常執(zhí)行方法,ts:" + Instant.now());
                      }).start();
                  }
              }
          }

          以上程序的執(zhí)行結(jié)果為:

          正常執(zhí)行方法,ts:2023-05-15T14:46:37.175Z

          正常執(zhí)行方法,ts:2023-05-15T14:46:37.237Z

          正常執(zhí)行方法,ts:2023-05-15T14:46:37.339Z

          正常執(zhí)行方法,ts:2023-05-15T14:46:37.442Z

          正常執(zhí)行方法,ts:2023-05-15T14:46:37.542Z

          正常執(zhí)行方法,ts:2023-05-15T14:46:37.640Z

          正常執(zhí)行方法,ts:2023-05-15T14:46:37.741Z

          正常執(zhí)行方法,ts:2023-05-15T14:46:37.840Z

          正常執(zhí)行方法,ts:2023-05-15T14:46:37.942Z

          正常執(zhí)行方法,ts:2023-05-15T14:46:38.042Z

          正常執(zhí)行方法,ts:2023-05-15T14:46:38.142Z

          從以上結(jié)果可以看出令牌確實(shí)是每 100ms 產(chǎn)生一個(gè),而 acquire() 方法為阻塞等待獲取令牌,它可以傳遞一個(gè) int 類型的參數(shù),用于指定獲取令牌的個(gè)數(shù)。它的替代方法還有 tryAcquire(),此方法在沒有可用令牌時(shí)就會(huì)返回 false 這樣就不會(huì)阻塞等待了。當(dāng)然 tryAcquire() 方法也可以設(shè)置超時(shí)時(shí)間,未超過最大等待時(shí)間會(huì)阻塞等待獲取令牌,如果超過了最大等待時(shí)間,還沒有可用的令牌就會(huì)返回 false。

          注意:使用 guava 實(shí)現(xiàn)的令牌算法屬于程序級(jí)別的單機(jī)限流方案,而上面使用 Redis-Cell 的是分布式的限流方案。

          小結(jié)

          本文提供了 6 種具體的實(shí)現(xiàn)限流的手段,他們分別是:Tomcat 使用 maxThreads 來實(shí)現(xiàn)限流;Nginx 提供了兩種限流方式,一是通過 limit_req_zone 和 burst 來實(shí)現(xiàn)速率限流,二是通過 limit_conn_zonelimit_conn 兩個(gè)指令控制并發(fā)連接的總數(shù)。最后我們講了時(shí)間窗口算法借助 Redis 的有序集合可以實(shí)現(xiàn),還有漏桶算法可以使用 Redis-Cell 來實(shí)現(xiàn),以及令牌算法可以解決 Google 的 guava 包來實(shí)現(xiàn)。

          需要注意的是借助 Redis 實(shí)現(xiàn)的限流方案可用于分布式系統(tǒng),而 guava 實(shí)現(xiàn)的限流只能應(yīng)用于單機(jī)環(huán)境。如果你嫌棄服務(wù)器端限流麻煩,甚至可以在不改代碼的情況下直接使用容器限流(Nginx 或 Tomcat),但前提是能滿足你的業(yè)務(wù)需求。

          好了,本節(jié)到這里就結(jié)束了,下期我們?cè)贂?huì)~

          參考 & 鳴謝

          https://www.cnblogs.com/biglittleant/p/8979915.html

          推薦閱讀:

          刪除重復(fù)記錄但保留其中一行數(shù)據(jù)的sql寫法

          一條sql搞定這個(gè)需求,面試官直呼內(nèi)行

          MySQL如何進(jìn)行表之間的關(guān)聯(lián)更新

          高頻面試題:多線程順序打印ABC字符20次

          一網(wǎng)打盡:MySQL索引失效的場(chǎng)景大搜羅

          這個(gè)設(shè)計(jì)模式的用法,一般人我不告訴他

          瀏覽 35
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  中文字幕资源站 | 黄色色片在线观看 | 97人人爽人人爽人人爽人人爽 | 日本特一级免费 | 美女扒开尿口让 |