欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Spring Boot高可用限流三種實現(xiàn)解決方案

 更新時間:2023年08月22日 10:06:23   作者:shepherd111  
限流是對某一時間窗口內(nèi)的請求數(shù)進行限制,保持系統(tǒng)的可用性和穩(wěn)定性,本文就介紹了Spring Boot高可用限流三種實現(xiàn)解決方案,具有一定的參考價值,感興趣的可以了解一下

1.什么是限流

限流是對某一時間窗口內(nèi)的請求數(shù)進行限制,保持系統(tǒng)的可用性和穩(wěn)定性,防止因流量暴增而導(dǎo)致的系統(tǒng)運行緩慢或宕機。

為什么需要限流

其實限流思想在生活中隨處可見,例如景區(qū)限流,防止人滿為患。熱門餐飲需要排隊就餐等?;氐交ヂ?lián)網(wǎng)網(wǎng)絡(luò)上,同樣也是這個道理,例如某某明星公布了戀情,訪問從平時的50萬增加到了500萬,系統(tǒng)最多可以支撐200萬訪問,那么就要執(zhí)行限流規(guī)則,保證系統(tǒng)是一個可用的狀態(tài),不至于服務(wù)器崩潰導(dǎo)致所有請求不可用。還有12306購票系統(tǒng),每年的618,雙11等場景都需要限流來抗住高并發(fā)的瞬時流量,保證系統(tǒng)能正常服務(wù),不被沖垮宕機癱瘓。

2.限流算法

計數(shù)器算法

計數(shù)器限流算法是最為簡單粗暴的解決方案,主要用來限制總并發(fā)數(shù),比如數(shù)據(jù)庫連接池大小、線程池大小、接口訪問并發(fā)數(shù)等都是使用計數(shù)器算法。一般我們會限制一秒鐘能夠通過的請求數(shù)。比如我們規(guī)定,對于A接口來說,我們1分鐘的訪問次數(shù)不能超過1000個。那么我們可以這么做:在一開始的時候,我們可以設(shè)置一個計數(shù)器counter,每當(dāng)一個請求過來的時候, counter就加1,如果counter的值大于1000并且該請求與第一個請求的間隔時間還在1分鐘之內(nèi),那么說明請求數(shù)過多; 如果該請求與第一個請求的間隔時間大于1分鐘,且counter的值還在限流范圍內(nèi),那么就重置 counter。

代碼實現(xiàn)如下:

/**
 * 固定窗口時間算法
 * @return
*/
public class Counter {
  public long timeStamp = System.currentTimeMillis(); // 當(dāng)前時間
  public int reqCount = 0; // 初始化計數(shù)器
  public final int limit = 1000; // 時間窗口內(nèi)最大請求數(shù)
  public final long interval = 1000 * 60; // 時間窗口ms
?
  public boolean limit() {
    long now = System.currentTimeMillis();
    if (now < timeStamp + interval) {
      // 在時間窗口內(nèi)
      reqCount++;
      // 判斷當(dāng)前時間窗口內(nèi)是否超過最大請求控制數(shù)
      return reqCount <= limit;
    } else {
      timeStamp = now;
      // 超時后重置
      reqCount = 1;
      return true;
    }
  }
}

但是,這種上面的固定時間窗口算法有一個很明顯的臨界問題:假設(shè)限流閥值為5個請求,單位時間窗口是1s,如果我們在單位時間內(nèi)的前0.8-1s和1-1.2s,分別并發(fā)5個請求。雖然都沒有超過閥值,但是如果算0.8-1.2s,則并發(fā)數(shù)高達10,已經(jīng)超過單位時間1s不超過5閥值的定義啦。

滑動窗口限流算法

滑動窗口限流解決固定窗口臨界值的問題。它將單位時間周期分為n個小周期,分別記錄每個小周期內(nèi)接口的訪問次數(shù),并且根據(jù)時間滑動刪除過期的小周期。如下圖所示:展示了滑動窗口算法對時間區(qū)間的劃分

假設(shè)單位時間還是1s,滑動窗口算法把它劃分為5個小周期,也就是滑動窗口(單位時間)被劃分為5個小格子。每格表示0.2s。每過0.2s,時間窗口就會往右滑動一格。然后呢,每個小周期,都有自己獨立的計數(shù)器,如果請求是0.83s到達的,0.8~1.0s對應(yīng)的計數(shù)器就會加1。

假設(shè)我們1s內(nèi)的限流閥值還是5個請求,0.81.0s內(nèi)(比如0.9s的時候)來了5個請求,落在黃色格子里。時間過了1.0s這個點之后,又來5個請求,落在紫色格子里。如果是固定窗口算法,是不會被限流的,但是滑動窗口的話,每過一個小周期,它會右移一個小格。過了1.0s這個點后,會右移一小格,當(dāng)前的單位時間段是0.21.2s,這個區(qū)域的請求已經(jīng)超過限定的5了,已觸發(fā)限流啦,實際上,紫色格子的請求都被拒絕啦。

TIPS: 當(dāng)滑動窗口的格子周期劃分的越多,那么滑動窗口的滾動就越平滑,限流的統(tǒng)計就會越精確。

 /**
     * 單位時間劃分的小周期(單位時間是1分鐘,10s一個小格子窗口,一共6個格子)
     */
    private int SUB_CYCLE = 10;
    /**
     * 每分鐘限流請求數(shù)
     */
    private int thresholdPerMin = 100;
    /**
     * 計數(shù)器, k-為當(dāng)前窗口的開始時間值秒,value為當(dāng)前窗口的計數(shù)
     */
    private final TreeMap<Long, Integer> counters = new TreeMap<>();
   /**
     * 滑動窗口時間算法實現(xiàn)
     */
    boolean slidingWindowsTryAcquire() {
        long currentWindowTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) / SUB_CYCLE * SUB_CYCLE; //獲取當(dāng)前時間在哪個小周期窗口
        int currentWindowNum = countCurrentWindow(currentWindowTime); //當(dāng)前窗口總請求數(shù)
        //超過閥值限流
        if (currentWindowNum >= thresholdPerMin) {
            return false;
        }
        //計數(shù)器+1
        counters.get(currentWindowTime)++;
        return true;
    }
   /**
    * 統(tǒng)計當(dāng)前窗口的請求數(shù)
    */
    private int countCurrentWindow(long currentWindowTime) {
        //計算窗口開始位置
        long startTime = currentWindowTime - SUB_CYCLE* (60s/SUB_CYCLE-1);
        int count = 0;
        //遍歷存儲的計數(shù)器
        Iterator<Map.Entry<Long, Integer>> iterator = counters.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<Long, Integer> entry = iterator.next();
            // 刪除無效過期的子窗口計數(shù)器
            if (entry.getKey() < startTime) {
                iterator.remove();
            } else {
                //累加當(dāng)前窗口的所有計數(shù)器之和
                count =count + entry.getValue();
            }
        }
        return count;
    }

滑動窗口算法雖然解決了固定窗口的臨界問題,但是一旦到達限流后,請求都會直接暴力被拒絕

漏桶算法

漏桶算法思路很簡單,我們把水比作是請求,漏桶比作是系統(tǒng)處理能力極限,水先進入到漏桶里,漏桶里的水按一定速率流出,當(dāng)流出的速率小于流入的速率時,由于漏桶容量有限,后續(xù)進入的水直接溢出(拒絕請求),以此實現(xiàn)限流。

令牌桶算法

令牌桶算法則是一個存放固定容量令牌的桶,按照固定速率往桶里添加令牌。桶中存放的令牌數(shù)有最大上限,超出之后就被丟棄或者拒絕。當(dāng)流量或者網(wǎng)絡(luò)請求到達時,每個請求都要獲取一個令牌,如果能夠獲取到,則直接處理,并且令牌桶刪除一個令牌。如果獲取不同,該請求就要被限流,要么直接丟棄,要么在緩沖區(qū)等待。

令牌桶和漏桶算法區(qū)別:

1)令牌桶是按照固定速率往桶中添加令牌,請求是否被處理需要看桶中令牌是否足夠,當(dāng)令牌數(shù)減為零時則拒絕新的請求;漏桶則是按照常量固定速率流出請求,流入請求速率任意,當(dāng)流入的請求數(shù)累積到漏桶容量時,則新流入的請求被拒絕;

2)令牌桶限制的是平均流入速率,允許突發(fā)請求,只要有令牌就可以處理,支持一次拿3個令牌,4個令牌;漏桶限制的是常量流出速率,即流出速率是一個固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2,從而平滑突發(fā)流入速率;

3)令牌桶允許一定程度的突發(fā),而漏桶主要目的是平滑流出速率;

3.guava實現(xiàn)限流

Google開源工具包Guava提供了限流工具類RateLimiter,該類基于令牌桶算法實現(xiàn)流量限制,使用十分方便,而且十分高效。RateLimiter提供了令牌桶算法實現(xiàn):平滑突發(fā)限流(SmoothBursty)和平滑預(yù)熱限流(SmoothWarmingUp)。
依賴包

     <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>29.0-jre</version>
        </dependency>

使用示例

/**
 * @author fjzheng
 * @version 1.0
 * @date 2021/9/1 10:38
 */
@Slf4j
@RestController
@RequestMapping("/test")
@ResponseResultBody
public class TestController {
    /**
     * 限流策略 :1秒鐘2個請求
     */
    private final RateLimiter limiter = RateLimiter.create(2.0);
    private DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    @GetMapping("/rateLimit")
    public String testLimiter() {
        //500毫秒內(nèi),沒拿到令牌,就直接進入服務(wù)降級
        boolean tryAcquire = limiter.tryAcquire(50, TimeUnit.MILLISECONDS);
        if (!tryAcquire) {
            log.warn("進入服務(wù)降級,時間{}", LocalDateTime.now().format(dtf));
            return "當(dāng)前排隊人數(shù)較多,請稍后再試!";
        }
?
        log.info("獲取令牌成功,時間{}", LocalDateTime.now().format(dtf));
        return "請求成功";
    }
?
}

以上用到了RateLimiter的2個核心方法:create()、tryAcquire(),以下為詳細(xì)說明

  • acquire() 獲取一個令牌, 該方法會阻塞直到獲取到這一個令牌, 返回值為獲取到這個令牌花費的時間
  • acquire(int permits) 獲取指定數(shù)量的令牌, 該方法也會阻塞, 返回值為獲取到這 permits 個令牌花費的時間
  • tryAcquire() 判斷時候能獲取到令牌, 如果不能獲取立即返回 false
  • tryAcquire(int permits) 獲取指定數(shù)量的令牌, 如果不能獲取立即返回 false
  • tryAcquire(long timeout, TimeUnit unit) 判斷能否在指定時間內(nèi)獲取到令牌, 如果不能獲取立即返回 false
  • tryAcquire(int permits, long timeout, TimeUnit unit) 判斷能否在指定時間內(nèi)獲取到permits個令牌,如果不能則返回false。

這時候調(diào)用上面的測試接口,一秒鐘只能通過2個接口,同時每隔0.5s產(chǎn)生一個令牌,調(diào)用過于頻繁賊會被限制。但是上面的使用方式不夠優(yōu)雅,因為我們需要在每個需要限流的接口重復(fù)使用tryAcquire()方法,然后根據(jù)是否獲取到令牌做邏輯判斷

優(yōu)雅使用

所謂優(yōu)雅就是統(tǒng)一處理,使用aop實現(xiàn)一個攔截器即可,如下所示:
首先我們的創(chuàng)建一個注解,該注解可以配置限流信息

/**
 * @author fjzheng
 * @version 1.0
 * @date 2021/10/25 00:06
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@Documented
public @interface RateLimit {
    /**
     * 資源的key,唯一
     * 作用:不同的接口,不同的流量控制
     */
    String key() default "";
?
    /**
     * 最多的訪問限制次數(shù)
     */
    double permitsPerSecond () ;
?
    /**
     * 獲取令牌最大等待時間
     */
    long timeout();
?
    /**
     * 獲取令牌最大等待時間,單位(例:分鐘/秒/毫秒) 默認(rèn):毫秒
     */
    TimeUnit timeunit() default TimeUnit.MILLISECONDS;
?
    /**
     * 得不到令牌的提示語
     */
    String msg() default "系統(tǒng)繁忙,請稍后再試.";
?
}

然后使用aop攔截帶有RateLimit注解的方法,進行統(tǒng)一處理即可。

/**
 * @author fjzheng
 * @version 1.0
 * @date 2021/10/25 00:10
 */
@Slf4j
@Component
@Aspect
public class RateLimitAop {
    /**
     * 不同的接口,不同的流量控制
     * map的key為 Limiter.key
     */
    private final Map<String, RateLimiter> limitMap = Maps.newConcurrentMap();
?
    @Around("@annotation(com.shepherd.mall.seckill.annotation.RateLimit)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        //拿limit的注解
        RateLimit limit = method.getAnnotation(RateLimit.class);
        if (limit != null) {
            //key作用:不同的接口,不同的流量控制
            String key=limit.key();
            RateLimiter rateLimiter = null;
            //驗證緩存是否有命中key
            if (!limitMap.containsKey(key)) {
                // 創(chuàng)建令牌桶
                rateLimiter = RateLimiter.create(limit.permitsPerSecond());
                limitMap.put(key, rateLimiter);
                log.info("新建了令牌桶={},容量={}",key,limit.permitsPerSecond());
            }
            rateLimiter = limitMap.get(key);
            // 拿令牌
            boolean acquire = rateLimiter.tryAcquire(limit.timeout(), limit.timeunit());
            // 拿不到命令,直接返回異常提示
            if (!acquire) {
                log.debug("令牌桶={},獲取令牌失敗",key);
                this.responseFail(limit.msg());
                return null;
            }
        }
        return joinPoint.proceed();
    }
?
    /**
     * 直接向前端拋出異常
     * @param msg 提示信息
     */
    private void responseFail(String msg)  {
        HttpServletResponse response=((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
        ResponseVO<Object> responseVO = ResponseVO.failure(400, msg);
        WebUtil.writeJson(response, responseVO);
    }
}

優(yōu)雅使用示例

/**
 * @author fjzheng
 * @version 1.0
 * @date 2021/9/1 10:38
 */
@Slf4j
@RestController
@RequestMapping("/test")
@ResponseResultBody
public class TestController {
    @GetMapping("/limit2")
    @RateLimit(key = "limit2", permitsPerSecond = 1, timeout = 50, timeunit = TimeUnit.MILLISECONDS, msg = "當(dāng)前排隊人數(shù)較多,請稍后再試!")
    public String limit2() {
        log.info("令牌桶l(fā)imit2獲取令牌成功");
        return "ok";
    }
}

其測試結(jié)果和上面的示例是差不多的。
guava的rateLimit限流只能使用與單機版,如果是分布式系統(tǒng),部署多個節(jié)點就不行了

4.分布式限流

分布式限流服務(wù)最關(guān)鍵是將限流服務(wù)做成原子化的,防止redis在分步執(zhí)行命令時部分成功執(zhí)行部分失敗問題導(dǎo)致邏輯錯誤。lua腳本可以保證操作的原子性

redis+lua實現(xiàn)

local key = "rate.limit:" .. KEYS[1]
local limit = tonumber(ARGV[1])
local expire_time = ARGV[2]
?
local is_exists = redis.call("EXISTS", key)
if is_exists == 1 then
    if redis.call("INCR", key) > limit then
        return 0
    else
        return 1
    end
else
    redis.call("SET", key, 1)
    redis.call("EXPIRE", key, expire_time)
    return 1
end

nginx+lua實現(xiàn)

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 --計數(shù)器
?
    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 0
    end
    if current == nil then
       limit_counter:set(key, 1, 1) --第一次需要設(shè)置過期時間,設(shè)置key的值為1,
--過期時間為1秒
    else
        limit_counter:incr(key, 1) --第二次開始加1即可
    end
    lock:unlock()
    return 1
end
ngx.print(acquire())

對于Nginx接入層限流可以使用Nginx自帶了兩個模塊:連接數(shù)限流模塊ngx_http_limit_conn_module和漏桶算法實現(xiàn)的請求限流模塊ngx_http_limit_req_module。
控制并發(fā)數(shù):ngx_http_limit_conn_module

http {
    include       mime.types;
    default_type  application/octet-stream;
?
    #cache
    lua_shared_dict dis_cache 128m;
?
    #限流設(shè)置
    limit_req_zone $binary_remote_addr zone=contentRateLimit:10m rate=2r/s;
?
    #根據(jù)IP地址來限制,存儲內(nèi)存大小10M
    limit_conn_zone $binary_remote_addr zone=addr:1m;
?
    sendfile        on;
    #tcp_nopush     on;
?
    #keepalive_timeout  0;
    keepalive_timeout  65;
?
    #gzip  on;
?
    server {
        listen       80;
        server_name  localhost;
        location /brand {
            limit_conn addr 2;
            proxy_pass http://192.168.211.1:18081;
        }
?
        location /update_content {
            content_by_lua_file /root/lua/update_content.lua;
        }
?
        location /read_content {
            limit_req zone=contentRateLimit burst=4 nodelay;
            content_by_lua_file /root/lua/read_content.lua;
        }
    }
}
  • limit_conn_zone $binary_remote_addr zone=addr:10m; 表示限制根據(jù)用戶的IP地址來顯示,設(shè)置存儲地址為的內(nèi)存大小10M
  • limit_conn addr 2; 表示 同一個地址只允許連接2次。

控制速率:ngx_http_limit_req_module

user  root root;
worker_processes  1;
?
events {
    worker_connections  1024;
}
?
http {
    include       mime.types;
    default_type  application/octet-stream;
?
    #cache
    lua_shared_dict dis_cache 128m;
?
    #限流設(shè)置
    limit_req_zone $binary_remote_addr zone=contentRateLimit:10m rate=2r/s;
?
    sendfile        on;
    #tcp_nopush     on;
?
    #keepalive_timeout  0;
    keepalive_timeout  65;
?
    #gzip  on;
?
    server {
        listen       80;
        server_name  localhost;
?
        location /update_content {
            content_by_lua_file /root/lua/update_content.lua;
        }
?
        location /read_content {
            limit_req zone=contentRateLimit burst=4 nodelay;
            content_by_lua_file /root/lua/read_content.lua;
        }
    }
}

burst 譯為突發(fā)、爆發(fā),表示在超過設(shè)定的處理速率后能額外處理的請求數(shù),當(dāng) rate=10r/s 時,將1s拆成10份,即每100ms可處理1個請求。
此處,burst=4 ,若同時有4個請求到達,Nginx 會處理第一個請求,剩余3個請求將放入隊列,然后每隔500ms從隊列中獲取一個請求進行處理。若請求數(shù)大于4,將拒絕處理多余的請求,直接返回503.

不過,單獨使用 burst 參數(shù)并不實用。假設(shè) burst=50 ,rate依然為10r/s,排隊中的50個請求雖然每100ms會處理一個,但第50個請求卻需要等待 50 * 100ms即 5s,這么長的處理時間自然難以接受。

因此,burst 往往結(jié)合 nodelay 一起使用。

到此這篇關(guān)于Spring Boot高可用限流三種實現(xiàn)解決方案的文章就介紹到這了,更多相關(guān)Spring Boot高可用限流內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 解決springboot文件上傳提示臨時文件夾不存在問題

    解決springboot文件上傳提示臨時文件夾不存在問題

    這篇文章主要介紹了解決springboot文件上傳提示臨時文件夾不存在問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-05-05
  • Springboot AOP對指定敏感字段數(shù)據(jù)加密存儲的實現(xiàn)

    Springboot AOP對指定敏感字段數(shù)據(jù)加密存儲的實現(xiàn)

    本篇文章主要介紹了利用Springboot+AOP對指定的敏感數(shù)據(jù)進行加密存儲以及對數(shù)據(jù)中加密的數(shù)據(jù)的解密的方法,代碼詳細(xì),具有一定的價值,感興趣的小伙伴可以了解一下
    2021-11-11
  • Java單元測試工具之JUnit的使用

    Java單元測試工具之JUnit的使用

    本篇文章主要詳細(xì)介紹單元測試工具JUnit的使用,文章中有詳細(xì)的代碼實例,有一定的參考價值,需要的朋友可以參考閱讀
    2023-04-04
  • PowerJob的TimingStrategyHandler工作流程源碼解讀

    PowerJob的TimingStrategyHandler工作流程源碼解讀

    這篇文章主要為大家介紹了PowerJob的TimingStrategyHandler工作流程源碼解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2024-01-01
  • Java線程狀態(tài)及切換、關(guān)閉線程的正確姿勢分享

    Java線程狀態(tài)及切換、關(guān)閉線程的正確姿勢分享

    這篇文章主要給大家介紹了關(guān)于Java線程狀態(tài)及切換、關(guān)閉線程的正確姿勢,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者使用Java具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-10-10
  • MyBatis常用標(biāo)簽大全

    MyBatis常用標(biāo)簽大全

    這篇文章主要介紹了MyBatis常用標(biāo)簽大全的相關(guān)資料,非常不錯,具有參考借鑒價值,需要的朋友可以參考下
    2016-12-12
  • 有關(guān)Java中的BeanInfo介紹

    有關(guān)Java中的BeanInfo介紹

    Java的BeanInfo在工作中并不怎么用到,我也是在學(xué)習(xí)spring源碼的時候,發(fā)現(xiàn)SpringBoot啟動時候會設(shè)置一個屬叫"spring.beaninfo.ignore",網(wǎng)上一些地方說這個配置的意思是是否跳過java BeanInfo的搜索,但是BeanInfo又是什么呢?本文我們將對此做一個詳細(xì)介紹
    2021-09-09
  • SpringBoot整合JWT實戰(zhàn)教程

    SpringBoot整合JWT實戰(zhàn)教程

    JWT(JSON?Web?Token)是一種用于身份驗證和授權(quán)的開放標(biāo)準(zhǔn)(RFC?7519),它使用JSON格式傳輸信息,可以在不同系統(tǒng)之間安全地傳遞數(shù)據(jù),這篇文章主要介紹了SpringBoot整合JWT實戰(zhàn)教程,需要的朋友可以參考下
    2023-06-06
  • Springboot項目啟動不加載resources目錄下的文件問題

    Springboot項目啟動不加載resources目錄下的文件問題

    這篇文章主要介紹了Springboot項目啟動不加載resources目錄下的文件問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-08-08
  • Java函數(shù)式編程(四):在集合中查找元素

    Java函數(shù)式編程(四):在集合中查找元素

    這篇文章主要介紹了Java函數(shù)式編程(四):在集合中查找元素,本文是系列文章的第4篇,其它篇章請參閱相關(guān)文章,需要的朋友可以參考下
    2014-09-09

最新評論