<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>

          SpringBoot 2.x 優(yōu)雅解決分布式限流

          共 6677字,需瀏覽 14分鐘

           ·

          2022-01-10 02:12

          程序員的成長之路
          互聯(lián)網(wǎng)/程序員/技術(shù)/資料共享?
          關(guān)注


          閱讀本文大概需要 9?分鐘。

          來自:blog.csdn.net/johnf_nash/article/details/89791808

          某天A君突然發(fā)現(xiàn)自己的接口請求量突然漲到之前的10倍,沒多久該接口幾乎不可使用,并引發(fā)連鎖反應導致整個系統(tǒng)崩潰。如何應對這種情況呢?
          生活給了我們答案:比如老式電閘都安裝了保險絲,一旦有人使用超大功率的設(shè)備,保險絲就會燒斷以保護各個電器不被強電流給燒壞。同理我們的接口也需要安裝上“保險絲”,以防止非預期的請求對系統(tǒng)壓力過大而引起的系統(tǒng)癱瘓,當流量過大時,可以采取拒絕或者引流等機制。

          一、常用的限流算法

          1.計數(shù)器方式(傳統(tǒng)計數(shù)器缺點:臨界問題 可能違背定義固定速率原則)
          2.令牌桶方式
          令牌桶算法的原理是系統(tǒng)會以一個恒定的速度往桶里放入令牌,而如果請求需要被處理,則需要先從桶里獲取一個令牌,當桶里沒有令牌可取時,則拒絕服務。從原理上看,令牌桶算法和漏桶算法是相反的,一個“進水”,一個是“漏水”。
          RateLimiter是guava提供的基于令牌桶算法的實現(xiàn)類,可以非常簡單的完成限流特技,并且根據(jù)系統(tǒng)的實際情況來調(diào)整生成token的速率。
          RateLimiter 是單機(單進程)的限流,是JVM級別的的限流,所有的令牌生成都是在內(nèi)存中,在分布式環(huán)境下不能直接這么用。
          3、漏桶算法
          如上圖所示,我們假設(shè)系統(tǒng)是一個漏桶,當請求到達時,就是往漏桶里“加水”,而當請求被處理掉,就是水從漏桶的底部漏出。水漏出的速度是固定的,當“加水”太快,桶就會溢出,也就是“拒絕請求”。從而使得桶里的水的體積不可能超出桶的容量。
          令牌桶算法與漏桶算法的區(qū)別:
          令牌桶里面裝載的是令牌,然后讓令牌去關(guān)聯(lián)到數(shù)據(jù)發(fā)送,常規(guī)漏桶里面裝載的是數(shù)據(jù),令牌桶允許用戶的正常的持續(xù)突發(fā)量(Bc),就是一次就將桶里的令牌全部用盡的方式來支持續(xù)突發(fā),而常規(guī)的漏桶則不允許用戶任何突發(fā)行。

          二、限流實現(xiàn)

          基于 redis 的分布式限流
          單機版中我們了解到 AtomicInteger、RateLimiter、Semaphore 這幾種解決方案,但它們也僅僅是單機的解決手段,在集群環(huán)境下就透心涼了,后面又講述了 Nginx 的限流手段,可它又屬于網(wǎng)關(guān)層面的策略之一,并不能解決所有問題。例如供短信接口,你無法保證消費方是否會做好限流控制,所以自己在應用層實現(xiàn)限流還是很有必要的。
          導入依賴
          在 pom.xml 中添加上 starter-web、starter-aop、starter-data-redis 的依賴即可,習慣了使用 commons-lang3 和 guava 中的一些工具包…
          <dependencies>
          ????
          ????<dependency>
          ????????<groupId>org.springframework.bootgroupId>
          ????????<artifactId>spring-boot-starter-aopartifactId>
          ????dependency>
          ????<dependency>
          ????????<groupId>org.springframework.bootgroupId>
          ????????<artifactId>spring-boot-starter-webartifactId>
          ????dependency>
          ????<dependency>
          ????????<groupId>org.springframework.bootgroupId>
          ????????<artifactId>spring-boot-starter-data-redisartifactId>
          ????dependency>
          ????<dependency>
          ????????<groupId>com.google.guavagroupId>
          ????????<artifactId>guavaartifactId>
          ????????<version>21.0version>
          ????dependency>
          ????<dependency>
          ????????<groupId>org.apache.commonsgroupId>
          ????????<artifactId>commons-lang3artifactId>
          ????dependency>
          ????<dependency>
          ????????<groupId>org.springframework.bootgroupId>
          ????????<artifactId>spring-boot-starter-testartifactId>
          ????dependency>
          dependencies>
          屬性配置
          在?application.properites?資源文件中添加 redis 相關(guān)的配置項
          spring.redis.host=localhost
          spring.redis.port=6379
          spring.redis.password=battcn
          Limit 注解
          創(chuàng)建一個 Limit 注解,不多說注釋都給各位寫齊全了….
          package?com.johnfnash.learn.springboot.ratelimiter.annotation;

          import?java.lang.annotation.Documented;
          import?java.lang.annotation.ElementType;
          import?java.lang.annotation.Inherited;
          import?java.lang.annotation.Retention;
          import?java.lang.annotation.RetentionPolicy;
          import?java.lang.annotation.Target;

          //?限流
          @Target({ElementType.METHOD,?ElementType.TYPE})
          @Retention(RetentionPolicy.RUNTIME)
          @Inherited
          @Documented
          public?@interface?Limit?{

          ????/**
          ?????*?資源的名稱
          ?????*?@return
          ?????*/

          ????String?name()?default?"";
          ????
          ????/**
          ?????*?資源的key
          ?????*
          ?????*?@return
          ?????*/

          ????String?key()?default?"";

          ????/**
          ?????*?Key的prefix
          ?????*
          ?????*?@return
          ?????*/

          ????String?prefix()?default?"";
          ????
          ????/**
          ?????*?給定的時間段
          ?????*?單位秒
          ?????*
          ?????*?@return
          ?????*/

          ????int?period();

          ????/**
          ?????*?最多的訪問限制次數(shù)
          ?????*
          ?????*?@return
          ?????*/

          ????int?count();
          ????
          ????/**
          ?????*?類型
          ?????*
          ?????*?@return
          ?????*/

          ????LimitType?limitType()?default?LimitType.CUSTOMER;
          }
          package?com.johnfnash.learn.springboot.ratelimiter.annotation;

          //?限制的類型
          public?enum?LimitType?{

          ????/**
          ?????*?自定義key
          ?????*/

          ????CUSTOMER,
          ????/**
          ?????*?根據(jù)請求者IP
          ?????*/

          ????IP;
          ????
          }
          RedisTemplate
          默認情況下 spring-boot-data-redis 為我們提供了StringRedisTemplate 但是滿足不了其它類型的轉(zhuǎn)換,所以還是得自己去定義其它類型的模板….
          package?com.johnfnash.learn.springboot.ratelimiter;

          import?java.io.Serializable;

          import?org.springframework.context.annotation.Bean;
          import?org.springframework.context.annotation.Configuration;
          import?org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
          import?org.springframework.data.redis.core.RedisTemplate;
          import?org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
          import?org.springframework.data.redis.serializer.StringRedisSerializer;

          @Configuration
          public?class?RedisLimiterHelper?{

          ????@Bean
          ????public?RedisTemplate?limitRedisTemplate(LettuceConnectionFactory?factory)?{
          ????????RedisTemplate?template?=?new?RedisTemplate();
          ????????template.setKeySerializer(new?StringRedisSerializer());
          ????????template.setValueSerializer(new?GenericJackson2JsonRedisSerializer());
          ????????template.setConnectionFactory(factory);
          ????????return?template;
          ????}
          ????
          }
          Limit 攔截器(AOP)
          熟悉 Redis 的朋友都知道它是線程安全的,我們利用它的特性可以實現(xiàn)分布式鎖、分布式限流等組件。官方雖然沒有提供相應的API,但卻提供了支持 Lua 腳本的功能,我們可以通過編寫 Lua 腳本實現(xiàn)自己的API,同時他是滿足原子性的….
          下面核心就是調(diào)用 execute 方法傳入我們的 Lua 腳本內(nèi)容,然后通過返回值判斷是否超出我們預期的范圍,超出則給出錯誤提示。
          package?com.johnfnash.learn.springboot.ratelimiter.aop;

          import?java.io.Serializable;
          import?java.lang.reflect.Method;

          import?javax.servlet.http.HttpServletRequest;

          import?org.apache.commons.lang3.StringUtils;
          import?org.aspectj.lang.ProceedingJoinPoint;
          import?org.aspectj.lang.annotation.Around;
          import?org.aspectj.lang.annotation.Aspect;
          import?org.aspectj.lang.reflect.MethodSignature;
          import?org.slf4j.Logger;
          import?org.slf4j.LoggerFactory;
          import?org.springframework.beans.factory.annotation.Autowired;
          import?org.springframework.context.annotation.Configuration;
          import?org.springframework.data.redis.core.RedisTemplate;
          import?org.springframework.data.redis.core.script.DefaultRedisScript;
          import?org.springframework.data.redis.core.script.RedisScript;
          import?org.springframework.web.context.request.RequestContextHolder;
          import?org.springframework.web.context.request.ServletRequestAttributes;

          import?com.google.common.collect.ImmutableList;
          import?com.johnfnash.learn.springboot.ratelimiter.annotation.Limit;
          import?com.johnfnash.learn.springboot.ratelimiter.annotation.LimitType;

          @Aspect
          @Configuration
          public?class?LimitInterceptor?{

          ????private?static?final?Logger?logger?=?LoggerFactory.getLogger(LimitInterceptor.class);;
          ????
          ????private?final?String?REDIS_SCRIPT?=?buildLuaScript();
          ????
          ????@Autowired
          ????private?RedisTemplate?redisTemplate;
          ????
          ????@Around("execution(public?*?*(..))?&&?@annotation(com.johnfnash.learn.springboot.ratelimiter.annotation.Limit)")
          ????public?Object?interceptor(ProceedingJoinPoint?pjp)?{
          ????????MethodSignature?signature?=?(MethodSignature)?pjp.getSignature();
          ????????Method?method?=?signature.getMethod();
          ????????Limit?limitAnno?=?method.getAnnotation(Limit.class);
          ????????LimitType?limitType?=?limitAnno.limitType();
          ????????String?name?=?limitAnno.name();
          ????????
          ????????String?key?=?null;
          ????????int?limitPeriod?=?limitAnno.period();
          ????????int?limitCount?=?limitAnno.count();
          ????????switch?(limitType)?{
          ????????case?IP:
          ????????????key?=?getIpAddress();
          ????????????break;
          ????????case?CUSTOMER:
          ????????????// TODO 如果此處想根據(jù)表達式或者一些規(guī)則生成?請看?一起來學Spring Boot |?第二十三篇:輕松搞定重復提交(分布式鎖)
          ????????????key?=?limitAnno.key();
          ????????????break;
          ????????default:
          ????????????break;
          ????????}
          ????????
          ????????ImmutableList?keys?=?ImmutableList.of(StringUtils.join(limitAnno.prefix(),?key));
          ????????try?{
          ????????????RedisScript?redisScript?=?new?DefaultRedisScript(REDIS_SCRIPT,?Number.class);
          ????????????Number?count?=?redisTemplate.execute(redisScript,?keys,?limitCount,?limitPeriod);
          ????????????logger.info("Access?try?count?is?{}?for?name={}?and?key?=?{}",?count,?name,?key);
          ????????????if(count?!=?null?&&?count.intValue()?<=?limitCount)?{
          ????????????????return?pjp.proceed();
          ????????????}?else?{
          ????????????????throw?new?RuntimeException("You?have?been?dragged?into?the?blacklist");
          ????????????}
          ????????}?catch?(Throwable?e)?{
          ????????????if?(e?instanceof?RuntimeException)?{
          ????????????????throw?new?RuntimeException(e.getLocalizedMessage());
          ????????????}
          ????????????throw?new?RuntimeException("server?exception");
          ????????}
          ????}
          ????
          ????/**
          ?????*?限流?腳本
          ?????*
          ?????*?@return?lua腳本
          ?????*/

          ????private?String?buildLuaScript()?{
          ????????StringBuilder?lua?=?new?StringBuilder();
          ????????lua.append("local?c")
          ???????????.append("\nc?=?redis.call('get',?KEYS[1])")
          ???????????//?調(diào)用不超過最大值,則直接返回
          ???????????.append("\nif?c?and?tonumber(c)?>?tonumber(ARGV[1])?then")
          ???????????.append("\nreturn?c;")
          ???????????.append("\nend")
          ???????????//?執(zhí)行計算器自加
          ???????????.append("\nc?=?redis.call('incr',?KEYS[1])")
          ???????????.append("\nif?tonumber(c)?==?1?then")
          ???????????//?從第一次調(diào)用開始限流,設(shè)置對應鍵值的過期
          ???????????.append("\nredis.call('expire',?KEYS[1],?ARGV[2])")
          ???????????.append("\nend")
          ???????????.append("\nreturn?c;");
          ????????return?lua.toString();
          ????}
          ????
          ????private?static?final?String?UNKNOWN?=?"unknown";

          ????public?String?getIpAddress()?{
          ????????HttpServletRequest?request?=?((ServletRequestAttributes)?RequestContextHolder.getRequestAttributes()).getRequest();
          ????????String?ip?=?request.getHeader("x-forwarded-for");
          ????????if?(ip?==?null?||?ip.length()?==?0?||?UNKNOWN.equalsIgnoreCase(ip))?{
          ????????????ip?=?request.getHeader("Proxy-Client-IP");
          ????????}
          ????????if?(ip?==?null?||?ip.length()?==?0?||?UNKNOWN.equalsIgnoreCase(ip))?{
          ????????????ip?=?request.getHeader("WL-Proxy-Client-IP");
          ????????}
          ????????if?(ip?==?null?||?ip.length()?==?0?||?UNKNOWN.equalsIgnoreCase(ip))?{
          ????????????ip?=?request.getRemoteAddr();
          ????????}
          ????????return?ip;
          ????}
          ????
          }
          控制層
          在接口上添加@Limit()注解,如下代碼會在 Redis 中生成過期時間為 100s 的?key = test?的記錄,特意定義了一個 AtomicInteger 用作測試…
          package?com.johnfnash.learn.springboot.ratelimiter.controller;

          import?java.util.concurrent.atomic.AtomicInteger;

          import?org.springframework.web.bind.annotation.GetMapping;
          import?org.springframework.web.bind.annotation.RestController;

          import?com.johnfnash.learn.springboot.ratelimiter.annotation.Limit;

          @RestController
          public?class?LimiterController?{

          ????private?static?final?AtomicInteger?ATOMIC_INTEGER?=?new?AtomicInteger();

          ????@Limit(key?=?"test",?period?=?100,?count?=?10)
          ????//?意味著?100S?內(nèi)最多允許訪問10次
          ????@GetMapping("/test")
          ????public?int?testLimiter()?{
          ????????return?ATOMIC_INTEGER.incrementAndGet();
          ????}
          ????
          }
          測試
          完成準備事項后,啟動 啟動類 自行測試即可,測試手段相信大伙都不陌生了,如 瀏覽器、postman、junit、swagger,此處基于 postman。
          未達設(shè)定的閥值時,正常響應
          達到設(shè)置的閥值時,錯誤響應

          總結(jié)

          目前很多大佬都寫過關(guān)于 Spring Boot 的教程了,如有雷同,請多多包涵,本教程基于最新的 spring-boot-starter-parent:2.0.3.RELEASE編寫…
          本篇文章核心的 Lua 腳本截取自軍哥的 Aquarius 開源項目,有興趣的朋友可以 fork star ,該項目干貨滿滿…
          全文代碼:
          https://github.com/battcn/spring-boot2-learning/tree/master/chapter27
          注:上面的方式是使用計數(shù)器的限流方式,無法處理臨界的時候,大量請求的的情況。要解決這個問題,可以使用redis中列表類型的鍵來記錄最近N次訪問的時間,一旦鍵中的元素超過N個,就判斷時間最早的元素距現(xiàn)在的時間是否小于M秒。如果是則表示用戶最近M秒的訪問次數(shù)超過了N次;如果不是就將現(xiàn)在的時間加入列表中同時把最早的元素刪除(可以通過腳本功能避免競態(tài)條件)。
          由于需要記錄每次訪問的時間,所以當要限制“M時間最多訪問N次”時,如果“N”的數(shù)值較大,此方法會占用較多的存儲空間,實際使用時還需要開發(fā)者自己去權(quán)衡。
          下面的解決思路的實現(xiàn)如下:
          /**
          ?*?限流?腳本(處理臨界時間大量請求的情況)
          ?*
          ?*?@return?lua腳本
          ?*/

          private?String?buildLuaScript2()?{
          ????StringBuilder?lua?=?new?StringBuilder();
          ????lua.append("local?listLen,?time")
          ???????.append("\nlistLen?=?redis.call('LLEN',?KEYS[1])")
          ???????//?不超過最大值,則直接寫入時間
          ???????.append("\nif?listLen?and?tonumber(listLen)?)
          ????????????.append("\nlocal?a?=?redis.call('TIME');")
          ????????????.append("\nredis.call('LPUSH',?KEYS[1],?a[1]*1000000+a[2])")
          ???????.append("\nelse")
          ???????????//?取出現(xiàn)存的最早的那個時間,和當前時間比較,看是小于時間間隔
          ???????????.append("\ntime?=?redis.call('LINDEX',?KEYS[1],?-1)")
          ???????????.append("\nlocal?a?=?redis.call('TIME');")
          ???????????.append("\nif?a[1]*1000000+a[2]?-?time?)
          ???????????????//?訪問頻率超過了限制,返回0表示失敗
          ???????????????.append("\nreturn?0;")
          ???????????.append("\nelse")???????????????????
          ???????????????.append("\nredis.call('LPUSH',?KEYS[1],?a[1]*1000000+a[2])")
          ???????????????.append("\nredis.call('LTRIM',?KEYS[1],?0,?tonumber(ARGV[1])-1)")
          ???????????.append("\nend")???
          ???????.append("\nend")
          ???????.append("\nreturn?1;");
          ????return?lua.toString();
          }
          調(diào)用的地方的也相應修改如下:
          if(count?!=?null?&&?count.intValue()?==?1)?{
          ????return?pjp.proceed();
          }?else?{
          ????throw?new?RuntimeException("You?have?been?dragged?into?the?blacklist");
          }
          補充,最近執(zhí)行?buildLuaScript2()?中的lua腳本,報錯Write commands not allowed after non deterministic commands.
          這個錯誤的原因大家可以參見這篇文章:
          https://yq.aliyun.com/articles/195914
          大致原因跟redis集群的重放和備份策略有關(guān),相當于我調(diào)用TIME操作,會在主從各執(zhí)行一次,得到的結(jié)果肯定會存在差異,這個差異就給最終邏輯正確性帶來了不確定性。在redis 4.0之后引入了redis.replicate_commands()來放開限制。
          于是,在 buildLuaScript2 的 lua 腳本最前面加上 “redis.replicate_commands();”,錯誤得以解決。

          推薦閱讀:

          字節(jié)工程師薪資排世界第五,中位數(shù) 43 萬美元,2021 全球程序員收入報告出爐!

          巧用Stream優(yōu)化老代碼,太清爽了!

          互聯(lián)網(wǎng)初中高級大廠面試題(9個G)

          內(nèi)容包含Java基礎(chǔ)、JavaWeb、MySQL性能優(yōu)化、JVM、鎖、百萬并發(fā)、消息隊列、高性能緩存、反射、Spring全家桶原理、微服務、Zookeeper、數(shù)據(jù)結(jié)構(gòu)、限流熔斷降級......等技術(shù)棧!

          ?戳閱讀原文領(lǐng)?。?/span>? ? ? ? ? ? ? ??? ??? ? ? ? ? ? ? ? ? ?朕已閱?

          瀏覽 36
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  黄色视频免费网站 | 囯产精品久久久久久久久 | 日韩视频二区在线 | 久久精品国产亚洲AV无码偷窥 | 禁片天堂免费网址 |