內容概要:
1、爲什麼要限流
2、分佈式限流解決方案
3、Guava實現令牌限流和漏桶限流
4、SpringBoot結合Redis實現分佈式限流
5、SpringCloud GateWay網關限流---微服務SprignCloud 6、Nginx限流
爲什麼要限流
目標
學習在項目開發中爲什麼要使用限流技術,以及限流的作用。
概述
在分佈式領域,我們難免會遇到併發量突增,對後端服務造成高壓力,嚴重甚至會導致系統宕機。爲避 免這種問題,我們通常會爲接口添加限流、降級、熔斷等能力,從而使接口更爲健壯。Java領域常見的 開源組件有Netflix的hystrix,阿里系開源的sentinel等,都是蠻不錯的限流熔斷框架。
圖解
在開發高併發系統時有三把利器用來保護系統:緩存、降級和限流。緩存的目的是提升系統訪問速度和 增大系統能處理的容量,可謂是抗高併發流量的銀彈;而降級是當服務出問題或者影響到核心流程的性 能則需要暫時屏蔽掉,待高峯或者問題解決後再打開;而有些場景並不能用緩存和降級來解決,比如稀 缺資源(秒殺、搶購)、寫服務(如評論、下單)、頻繁的複雜查詢(評論的最後幾頁),因此需有一 種手段來限制這些場景的併發/請求量,即限流。
解決一個問題:保護、保證系統一定可用。
解決方案
擴容
增加物理服務的硬件和設備。
緩存
緩存比較好理解,在大型高併發系統中,如果沒有緩存數據庫將分分鐘被爆,系統也會瞬間癱瘓。使用 緩存不單單能夠提升系統訪問速度、提高併發訪問量,也是保護數據庫、保護系統的有效方式。大型網 站一般主要是“讀”,緩存的使用很容易被想到。在大型“寫”系統中,緩存也常常扮演者非常重要的角
色。比如累積一些數據批量寫入,內存裏面的緩存隊列(生產消費),以及HBase寫數據的機制等等也 都是通過緩存提升系統的吞吐量或者實現系統的保護措施。甚至消息中間件,你也可以認爲是一種分佈 式的數據緩存。
降級
服務降級是當服務器壓力劇增的情況下,根據當前業務情況及流量對一些服務和頁面有策略的降級,以 此釋放服務器資源以保證核心任務的正常運行。降級往往會指定不同的級別,面臨不同的異常等級執行 不同的處理。根據服務方式:可以拒接服務,可以延遲服務,也有時候可以隨機服務。根據服務範圍: 可以砍掉某個功能,也可以砍掉某些模塊。總之服務降級需要根據不同的業務需求採用不同的降級策 略。主要的目的就是服務雖然有損但是總比沒有好。
限流
限流可以認爲服務降級的一種,限流就是限制系統的輸入和輸出流量已達到保護系統的目的。一般來說 系統的吞吐量是可以被測算的,爲了保證系統的穩定運行,一旦達到的需要限制的閾值,就需要限制流 量並採取一些措施以完成限制流量的目的。比如:延遲處理,拒絕處理,或者部分拒絕處理等等。
限流的目的
限流的目的是通過對併發訪問/請求進行限速或者一個時間窗口內的的請求進行限速來保護系統,一旦 達到限制速率則可以拒絕服務(定向到錯誤頁或告知資源沒有了)、排隊或等待(比如秒殺、評論、下 單)、降級(返回兜底數據或默認數據,如商品詳情頁庫存默認有貨)。
一般開發高併發系統常見的限流有:限制總併發數(比如數據庫連接池、線程池)、限制瞬時併發數
(如nginx的limit_conn模塊,用來限制瞬時併發連接數)、限制時間窗口內的平均速率(如Guava的RateLimiter、nginx的limit_req模塊,限制每秒的平均速率);其他還有如限制遠程接口調用速率、限 制MQ的消費速率。另外還可以根據網絡連接數、網絡流量、CPU或內存負載等來限流。
先有緩存這個銀彈,後有限流來應對618、雙十一高併發流量,在處理高併發問題上可以說是如虎添 翼,不用擔心瞬間流量導致系統掛掉或雪崩,最終做到有損服務而不是不服務;限流需要評估好,不可 亂用,否則會正常流量出現一些奇怪的問題而導致用戶抱怨。
保護、保證系統和網站的正常運行。
使用場景
秒殺、搶購,年底查詢密集,評論查詢。
SpringBoot結合Redis實現分佈式限流演進
目標
爲什麼使用分佈式限流解決方案,整個過程是如何來的。
步驟
1:搭建springboot框架
2:導入web依賴,和guava依賴,以及redis依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
3:最簡單的限流講解---計數器限流
package com.itheima.limiting.web; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class OrderController { // 搶購,搶購數量,也就限流的閾值 static long limit = 10; // 計算器,代表請求的用戶數量 private long count = 0; @GetMapping("/makeorder") public String makeOrder(){ long c = ++count; if(c > limit){ return "搶購結束,下次在來! count = " + c; } return "恭喜,搶購成功,count = " + c; } }
存在問題:
1:當前count沒有標記成爲static。需要標記成static嗎?答案是不需要,因爲 @RestController 標記當前類是單例的, 所以在內存中count只有一份,不會出現所謂的多份問題。而是共用的一個計數器。
2:往往在分佈式集羣的項目中,項目是部署多多臺,是多個 jvm。每個jvm都又自己的計數器,這個時候就會引發高併發帶來的線程安全問題。
3:那可以使用volatile 嗎?答案是不可以。因爲volatile只保證成員變量在線程見的可見性,它不保證線程安全。
4:如果線程不安全可以使用synchronized ,這種是沒問題的,可以解決線程安全的問題。但是同時帶
@GetMapping("/makeorder") public synchronized String makeOrder(){ long c = ++count; if(c > limit){ return "搶購結束,下次在來! count = " + c; 6 } return "恭喜,搶購成功,count = " + c; 8 }
來的隱患就是:性能低下,這個是必然的,如果使用了synchronized關鍵字,就是上了鎖,代碼的執行 就編程了串行,一大推的阻塞,如果這個時候又1w的併發,這個時候處理都會造成大量的阻塞。而且 性能極低。在一般的開發中我們都會認爲和代碼是不適合的。也滿足不了我們高併發的需要。那麼進行 優化,如何解決呢?原子類。
5:如果在分佈式環境下呢?
可以使用分佈式緩存來解決這個問題:
爲什麼不用Lock和synchronized,在單機環境下,是沒有問題,但是往往在開發中,大部分情況下是集 羣環境,這個適合每個電腦都是獨立JVM環境,你是沒有辦法去控制別人jvm內存中的東西,所以我們 要計數器進行共享。
爲什麼選擇Redis呢?而不是memcache操作呢?
答案很簡單:Redis單線程。(基於linux的io模型來架構的,EPOLL 異步IO BIO NIO EPOLL ) jedis redission
代碼如下:
引入依賴:
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.1.0</version> </dependency>
@GetMapping("makeorder2") public String makeOrder2(String name){ // 從jedis上獲取自增值 try(Jedis jedis = new Jedis("localhost",6379)){ long c = jedis.incr(name); if(c > limit){ return "搶購結束,下次在來! count = " + c; 8 } return "恭喜,搶購成功,count = " + c; 10 } 11 }
測試:啓動8080和8081兩個端口:訪問
http://localhost:8081/makeorder2?name=lisi 和 http://localhost:8080/makeorder2?name=lisi 模 擬同一個用戶在集羣環境下的併發問是否共享計數器。答案很明顯是可以的。
限流接口的時間窗請求數--時間窗限流
概述
即一個時間窗口內的請求數,如需限制某個接口/服務每秒/每分鐘/每天的請求數/調用量。
如果實現呢?
Guava---單機系統
使用LoadingCache限流
package com.itheima.limiting.web; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import redis.clients.jedis.Jedis; import java.util.Calendar; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; @RestController public class OrderGuavaController { // 搶購,搶購數量,也就限流的閾值 static long limit = 5; // 限時間窗請求數,限制5r/s LoadingCache<Long,AtomicLong> loadingCache = CacheBuilder.newBuilder().expireAfterAccess(2, TimeUnit.SECONDS) .build(new CacheLoader<Long, AtomicLong>() { @Override public AtomicLong load(Long aLong) throws Exception { return new AtomicLong(0); } }); @GetMapping("/makeorder3") public String makeOrder() throws Exception{ // 當前秒 long currentTime = System.currentTimeMillis()/1000L; //long c = atomicLong.incrementAndGet(); long c = loadingCache.get(currentTime).incrementAndGet(); if(c > limit){ return "搶購結束,下次在來! count = " + c; } return "恭喜,搶購成功,count = " + c; } }
package com.itheima.limiting.limiter; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicLong; public class MyLimiter { private ConcurrentMap<Long, AtomicLong> map = new ConcurrentHashMap<> (); private long before; public MyLimiter(long before){ super(); this.before = before; } public AtomicLong get(long key){ if(!this.map.containsKey(key)){ synchronized (map){ if(!this.map.containsKey(key)){ map.put(key,new AtomicLong(0L)); // 移除指定秒數的計數器 this.removeBeforeKey(key); } } } return this.map.get(key); } private void removeBeforeKey(long ckey){ for(Long key : this.map.keySet()){ // 把哪些超過時間的key,從map中移除出去。 if(key + before < ckey){ this.map.remove(key); } } } }
package com.itheima.limiting.web; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.itheima.limiting.limiter.MyLimiter; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import redis.clients.jedis.Jedis; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; @RestController public class OrderRedisController { // 搶購,搶購數量,也就限流的閾值 static long limit = 5; public String makeorder6(String name) throws Exception{ try (Jedis jedis = new Jedis("localhost",6379)){ long c = jedis.incr(name); if(c > limit){ return (System.currentTimeMillis()/1000)+"搶購結束,下次在來! count = " + c; }else{ if(c==1){ // 設置過期時間 jedis.expire(name,1); } } return (System.currentTimeMillis()/1000) + "恭喜,搶購成功,count = " + c; } } }
local key = KEYS[1] --限流Key(一秒一個) local limit = tonumber(ARGV[1]) --限流大小 local expire = ARGV[2] --過期時間 -- 獲取當前計數器的值 local current = tonumber(redis.call('get',key) or "0") -- 如果超過限制大小 if current + 1 > limit then return 0 Redis處理類 測試用例 else current = tonumber(redis.call('INCRBY',key,"1")) --請求數+1 if current == 1 then --如果是第一次訪問需要設置過期時間 redis.call("expire",key,expire) --設置過期時間 end end return 1 --返回1代表不限流
package com.itheima.limiting.limiter; import com.google.common.io.Files; import org.springframework.core.io.ClassPathResource; import redis.clients.jedis.Jedis; import java.nio.charset.Charset; public class JedisLuaLimiter { private String luascript; private String key; private String limit; private String expire; public JedisLuaLimiter(String key,String limit,String expire,String scriptFile){ super(); this.key = key; this.limit = limit; this.expire = expire; try{ this.luascript = Files.asCharSource(new ClassPathResource(scriptFile).getFile(), Charset.defaultCharset()).read(); }catch(Exception ex){ ex.printStackTrace(); } } // 嘗試獲取 public boolean tryAcqure(){ Jedis jedis = new Jedis("localhost",6379); return (Long)jedis.eval(this.luascript,1,key,limit,expire) == 1L; } }
測試用例
package com.itheima.limiting; import com.itheima.limiting.web.OrderRedisController; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.TimeUnit; @SpringBootTest class LimitingApplicationTests { @Autowired private OrderRedisController orderRedisController; @Test void contextLoads() throws Exception { // 倒計數鎖存器 CountDownLatch countDownLatch = new CountDownLatch(50); // 循環屏障 CyclicBarrier cyclicBarrier = new CyclicBarrier(50); for (int i = 0; i < 50; i++) { new Thread(()->{ try{ cyclicBarrier.await(); }catch(Exception ex){ ex.printStackTrace(); } try { System.out.println(Thread.currentThread().getName() + "===" + orderRedisController.makeorder6("aicode111")); }catch (Exception ex){ ex.printStackTrace(); } countDownLatch.countDown(); }).start(); } try{ countDownLatch.await(); }catch(Exception ex){ ex.printStackTrace(); } TimeUnit.SECONDS.sleep(1); System.out.println(Thread.currentThread().getName() + "===" + orderRedisController.makeorder6("aicode111")); } }
上圖中,紅色的部分代表超出消息處理能力的部分。把紅色部分的消息平均到之後的空閒時間去處理, 這樣既可以保證系統負載處在一個穩定的水位,又可以儘可能地處理更多消息。通過配置流控規則,可 以達到消息勻速處理的效果。
AHAS 流控降級的排隊等待功能,可以把驟增的大量請求勻速分配,以固定的間隔時間讓請求通過,起到“削峯填谷”的效果,從而避免流量驟增造成系統負載過高的情況。堆積的請求將會被排隊處理,當請 求的預計排隊時間超過最大超時時長時,AHAS 將拒絕這部分超時的請求。
例如:配置勻速模式下請求 QPS 爲 5,則每 200 ms 處理一條請求,多餘的處理任務將排隊;同時設置了超時時間爲 5s,則預計排隊時長超過 5s 的處理任務將被直接拒絕。具體操作步驟,參見新建流控規則。
示意圖如下:
問題
突發請求,流量整形,整形爲勻速請求處理,(比如 5r/s 時間間隔200毫秒處理一個請求,平滑速率).
解決方案:令牌桶算法
package com.itheima.limiting.web; import com.google.common.util.concurrent.RateLimiter; import com.itheima.limiting.limiter.JedisLuaLimiter; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class OrderGuavaRateLimitController { // 平滑限流請求 RateLimiter rateLimiter = RateLimiter.create(4); //@GetMapping("/makeorder7") public String makeorder7() throws Exception{ if(!rateLimiter.tryAcquire()){
測試代碼
package com.itheima.limiting; import com.itheima.limiting.web.OrderGuavaRateLimitController; import com.itheima.limiting.web.OrderRedisController; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.TimeUnit; @SpringBootTest class LimitingApplicationTests { @Autowired private OrderRedisController orderRedisController; @Autowired private OrderGuavaRateLimitController orderGuavaRateLimitController; @Test void contextLoads() throws Exception { // 倒計數鎖存器 CountDownLatch countDownLatch = new CountDownLatch(50); // 循環屏障 CyclicBarrier cyclicBarrier = new CyclicBarrier(50); for (int i = 0; i < 50; i++) { new Thread(()->{ try{ cyclicBarrier.await(); }catch(Exception ex){ ex.printStackTrace(); } try { //System.out.println(Thread.currentThread().getName() + "===" + orderRedisController.makeorder6("aicode111")); System.out.println(Thread.currentThread().getName() + "===" + orderGuavaRateLimitController.makeorder7()); }catch (Exception ex){ ex.printStackTrace(); } 自定義令牌桶算法 countDownLatch.countDown(); }).start(); } try{ countDownLatch.await(); }catch(Exception ex){ ex.printStackTrace(); } TimeUnit.SECONDS.sleep(1); // System.out.println(Thread.currentThread().getName() + "===" + orderRedisController.makeorder6("aicode111")); System.out.println(Thread.currentThread().getName() + "===" + orderGuavaRateLimitController.makeorder7()); } }
package com.itheima.limiting.limiter; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.Semaphore; public class MyRateLimiter implements AutoCloseable{ // 定義信號量 併發協同工具 private Semaphore semaphore; // 限制數量 private int limit; // 定時器 private Timer timer; public MyRateLimiter(int limit){ super(); this.limit = limit; this.semaphore = new Semaphore(limit); this.timer = new Timer(); // 放入令牌的時間間隔 long period = 1000L/limit; // 通過定時器。定時放入令牌 timer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { if(semaphore.availablePermits() < limit){ semaphore.release(); 漏桶算法 SpringBoot 結合Aop完成限流策略 原理 首先解釋下爲何採用Redis作爲限流組件的核心。 通俗地講,假設一個用戶(用IP判斷)每秒訪問某服務接口的次數不能超過10次,那麼我們可以在 Redis中創建一個鍵,並設置鍵的過期時間爲60秒。 當一個用戶對此服務接口發起一次訪問就把鍵值加1,在單位時間(此處爲1s)內當鍵值增加到10的時 候,就禁止訪問服務接口。PS:在某種場景中添加訪問時間間隔還是很有必要的。我們本次不考慮間隔 時間,只關注單位時間內的訪問次數。 需求 } } },period,period); } public void acquire() throws InterruptedException{ this.semaphore.acquire(); } public boolean tryAcquire(){ return this.semaphore.tryAcquire(); } public int availablePermits(){ return this.semaphore.availablePermits(); } @Override public void close(){ System.out.println("自動來關閉了.............."); this.timer.cancel(); } }
1 @GetMapping("/makeorder")
2 public synchronized String makeOrder(){
3 long c = ++count;
4 if(c > limit){
5 return "搶購結束,下次在來! count = " + c; 6}
7return"恭喜,搶購成功,count = " + c; 8}
SpringBoot 結合Aop完成限流策略
首先解釋下爲何採用Redis作爲限流組件的核心。
通俗地講,假設一個用戶(用IP判斷)每秒訪問某服務接口的次數不能超過10次,那麼我們可以在
Redis中創建一個鍵,並設置鍵的過期時間爲60秒。
當一個用戶對此服務接口發起一次訪問就把鍵值加1,在單位時間(此處爲1s)內當鍵值增加到10的時 候,就禁止訪問服務接口。PS:在某種場景中添加訪問時間間隔還是很有必要的。我們本次不考慮間隔 時間,只關注單位時間內的訪問次數。
原理已經講過了,說下需求。
1. 基於Redis的incr及過期機制開發
1. Redis整合
由於我們是基於Redis進行的限流操作,因此需要整合Redis的類庫,上面已經講到,我們是基於
Springboot進行的開發,因此這裏可以直接整合RedisTemplate。
1.1 座標引入
這裏我們引入spring-boot-starter-redis的依賴。
到這裏,我們正式開始手寫限流組件的進程。
項目基於maven構建,主要依賴Spring-boot-starter,我們主要在springboot上進行開發,因此自定義 的開發包可以直接依賴下面這個座標,方便進行包管理。版本號自行選擇穩定版。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>1.4.2.RELEASE</version> </dependency>
2、Redis整合
由於我們是基於Redis進行的限流操作,因此需要整合Redis的類庫,上面已經講到,我們是基於
Springboot進行的開發,因此這裏可以直接整合RedisTemplate。
1.1 座標引入
這裏我們引入spring-boot-starter-redis的依賴。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-redis</artifactId> <version>1.4.2.RELEASE</version> </dependency>
@Configuration @EnableCaching public class RedisCacheConfig { private static final Logger LOGGER = LoggerFactory.getLogger(RedisCacheConfig.class); @Bean public CacheManager cacheManager(RedisTemplate<?, ?> redisTemplate) { CacheManager cacheManager = new RedisCacheManager(redisTemplate); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Springboot Redis cacheManager 加載完成"); } return cacheManager; } @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); //使用Jackson2JsonRedisSerializer來序列化和反序列化redis的value值 (默認使用JDK的序列化方式) Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper mapper = new ObjectMapper(); mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); serializer.setObjectMapper(mapper); template.setValueSerializer(serializer); //使用StringRedisSerializer來序列化和反序列化redis的key值 template.setKeySerializer(new StringRedisSerializer()); template.afterPropertiesSet(); LOGGER.info("Springboot RedisTemplate 加載完成"); return template; } }
#單機模式redis spring.redis.host=127.0.0.1 spring.redis.port=6379 spring.redis.pool.maxActive=8 spring.redis.pool.maxWait=-1 spring.redis.pool.maxIdle=8 spring.redis.pool.minIdle=0 spring.redis.timeout=10000 spring.redis.password=
#哨兵集羣模式 # database name spring.redis.database=0 # server password 密碼,如果沒有設置可不配 spring.redis.password= # pool settings ...池配置 spring.redis.pool.max-idle=8 spring.redis.pool.min-idle=0 spring.redis.pool.max-active=8 spring.redis.pool.max-wait=-1 # name of Redis server 哨兵監聽的Redis server的名稱 spring.redis.sentinel.master=mymaster # comma-separated list of host:port pairs 哨兵的配置列表 spring.redis.sentinel.nodes=127.0.0.1:26379,127.0.0.1:26479,127.0.0.1:26579
/** * @author snowalker * @version 1.0 * @date 2018/10/27 1:25 * @className RateLimiter * @desc 限流注解 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RateLimiter { /** * 限流key * @return */ String key() default "rate:limiter"; /** * 單位時間限制通過請求數 * @return */long limit() default 10; /** * 過期時間,單位秒 * @return */ long expire() default 1; }
@Aspect @Component public class RateLimterHandler { private static final Logger LOGGER = LoggerFactory.getLogger(RateLimterHandler.class); @Autowired RedisTemplate redisTemplate; private DefaultRedisScript<Long> getRedisScript; @PostConstruct public void init() { getRedisScript = new DefaultRedisScript<>(); getRedisScript.setResultType(Long.class); getRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("rateLimter.lua"))); LOGGER.info("RateLimterHandler[分佈式限流處理器]腳本加載完成"); }
@Pointcut("@annotation(com.snowalker.shield.ratelimiter.core.annotation.RateL imiter)") public void rateLimiter() {}
1 @Around("@annotation(rateLimiter)") 這段代碼的邏輯爲,獲取 @RateLimiter 註解配置的屬性:key、limit、expire,並通過 redisTemplate.execute(RedisScript script, List keys, Object... args) 方法傳遞給Lua腳本進行限 流相關操作,邏輯很清晰。 這裏我們定義如果腳本返回狀態爲0則爲觸發限流,1表示正常請求。 5. Lua腳本 public Object around(ProceedingJoinPoint proceedingJoinPoint, RateLimiter rateLimiter) throws Throwable { if (LOGGER.isDebugEnabled()) { LOGGER.debug("RateLimterHandler[分佈式限流處理器]開始執行限流操 作"); } Signature signature = proceedingJoinPoint.getSignature(); if (!(signature instanceof MethodSignature)) { throw new IllegalArgumentException("the Annotation @RateLimter must used on method!"); } /** * 獲取註解參數 */ // 限流模塊key String limitKey = rateLimiter.key(); Preconditions.checkNotNull(limitKey); // 限流閾值 long limitTimes = rateLimiter.limit(); // 限流超時時間 long expireTime = rateLimiter.expire(); if (LOGGER.isDebugEnabled()) { LOGGER.debug("RateLimterHandler[分佈式限流處理器]參數值爲- limitTimes={},limitTimeout={}", limitTimes, expireTime); } /** * 執行Lua腳本 */ List<String> keyList = new ArrayList(); // 設置key值爲註解中的值 keyList.add(limitKey); /** * 調用腳本並執行 */ Long result = (Long) redisTemplate.execute(getRedisScript, keyList, expireTime, limitTimes); if (result == 0) { String msg = "由於超過單位時間=" + expireTime + "-允許的請求次數 =" + limitTimes + "[觸發限流]"; LOGGER.debug(msg); return "false"; } if (LOGGER.isDebugEnabled()) { LOGGER.debug("RateLimterHandler[分佈式限流處理器]限流執行結果- result={},請求[正常]響應", result); } return proceedingJoinPoint.proceed(); } }
5. Lua腳本
--獲取KEY local key1 = KEYS[1] local val = redis.call('incr', key1) local ttl = redis.call('ttl', key1) --獲取ARGV內的參數並打印 local expire = ARGV[1] local times = ARGV[2] redis.log(redis.LOG_DEBUG,tostring(times)) redis.log(redis.LOG_DEBUG,tostring(expire)) redis.log(redis.LOG_NOTICE, "incr "..key1.." "..val); if val == 1 then redis.call('expire', key1, tonumber(expire)) else if ttl == -1 then redis.call('expire', key1, tonumber(expire)) end end if val > tonumber(times) then return 0 end return 1
測試
@Controller public class TestController { private static final Logger LOGGER = LoggerFactory.getLogger(TestController.class); @ResponseBody @RequestMapping("ratelimiter") @RateLimiter(key = "ratedemo:1.0.0", limit = 5, expire = 100) public String sendPayment(HttpServletRequest request) throws Exception { return "正常請求"; } }
2018-10-28 00:00:00.602 DEBUG 17364 --- [nio-8888-exec-1] c.s.s.r.core.handler.RateLimterHandler : RateLimterHandler[分佈式限流處理器]開始執行限流操作 2018-10-28 00:00:00.688 DEBUG 17364 --- [nio-8888-exec-1] c.s.s.r.core.handler.RateLimterHandler : RateLimterHandler[分佈式限流處理器]限流執行結果-result=1,請求[正常]響應 2018-10-28 00:00:00.860 DEBUG 17364 --- [nio-8888-exec-3] c.s.s.r.core.handler.RateLimterHandler : RateLimterHandler[分佈式限流處理器]開始執行限流操作 2018-10-28 00:00:01.183 DEBUG 17364 --- [nio-8888-exec-4] c.s.s.r.core.handler.RateLimterHandler : RateLimterHandler[分佈式限流處理器]開始執行限流操作 2018-10-28 00:00:01.520 DEBUG 17364 --- [nio-8888-exec-3] c.s.s.r.core.handler.RateLimterHandler : RateLimterHandler[分佈式限流處理器]限流執行結果-result=1,請求[正常]響應 2018-10-28 00:00:01.521 DEBUG 17364 --- [nio-8888-exec-4] c.s.s.r.core.handler.RateLimterHandler : RateLimterHandler[分佈式限流處理器]限流執行結果-result=1,請求[正常]響應 2018-10-28 00:00:01.557 DEBUG 17364 --- [nio-8888-exec-5] c.s.s.r.core.handler.RateLimterHandler : RateLimterHandler[分佈式限流處理器]開始執行限流操作 2018-10-28 00:00:01.558 DEBUG 17364 --- [nio-8888-exec-5] c.s.s.r.core.handler.RateLimterHandler : RateLimterHandler[分佈式限流處理器]限流執行結果-result=1,請求[正常]響應 2018-10-28 00:00:01.774 DEBUG 17364 --- [nio-8888-exec-7] c.s.s.r.core.handler.RateLimterHandler : RateLimterHandler[分佈式限流處理器]開始執行限流操作 2018-10-28 00:00:02.111 DEBUG 17364 --- [nio-8888-exec-8] c.s.s.r.core.handler.RateLimterHandler : RateLimterHandler[分佈式限流處理器]開始 2018-10-28 00:00:02.169 DEBUG 17364 --- [nio-8888-exec-7] c.s.s.r.core.handler.RateLimterHandler : RateLimterHandler[分佈式限流處理器]限流執行結果-result=1,請求[正常]響應 2018-10-28 00:00:02.169 DEBUG 17364 --- [nio-8888-exec-8] c.s.s.r.core.handler.RateLimterHandler : 由於超過單位時間=100-允許的請求次數=5[觸發限流] 2018-10-28 00:00:02.276 DEBUG 17364 --- [io-8888-exec-10] c.s.s.r.core.handler.RateLimterHandler : RateLimterHandler[分佈式限流處理器]開始執行限流操作 2018-10-28 00:00:02.276 DEBUG 17364 --- [io-8888-exec-10] c.s.s.r.core.handler.RateLimterHandler : RateLimterHandler[分佈式限流處理器]參數值爲-limitTimes=5,limitTimeout=100 2018-10-28 00:00:02.278 DEBUG 17364 --- [io-8888-exec-10] c.s.s.r.core.handler.RateLimterHandler : 由於超過單位時間=100-允許的請求次數=5[觸發限流] 2018-10-28 00:00:02.445 DEBUG 17364 --- [nio-8888-exec-2] c.s.s.r.core.handler.RateLimterHandler : RateLimterHandler[分佈式限流處理器]開始執行限流操作 2018-10-28 00:00:02.445 DEBUG 17364 --- [nio-8888-exec-2] c.s.s.r.core.handler.RateLimterHandler : RateLimterHandler[分佈式限流處理器]參數值爲-limitTimes=5,limitTimeout=100 2018-10-28 00:00:02.446 DEBUG 17364 --- [nio-8888-exec-2] c.s.s.r.core.handler.RateLimterHandler : 由於超過單位時間=100-允許的請求次數=5[觸發限流] 2018-10-28 00:00:02.628 DEBUG 17364 --- [nio-8888-exec-4] c.s.s.r.core.handler.RateLimterHandler : RateLimterHandler[分佈式限流處理器]開始執行限流操作 2018-10-28 00:00:02.628 DEBUG 17364 --- [nio-8888-exec-4] c.s.s.r.core.handler.RateLimterHandler : RateLimterHandler[分佈式限流處理器]參數值爲-limitTimes=5,limitTimeout=100 2018-10-28 00:00:02.629 DEBUG 17364 --- [nio-8888-exec-4] c.s.s.r.core.handler.RateLimterHandler : 由於超過單位時間=100-允許的請求次數=5[觸發限流]
總結
5、Hystrix或Gateway網關限流
6、Nginx限流
漏桶算法
limit_req_zone 參數配置
Syntax: limit_req zone=name [burst=number] [nodelay];
Default: —
Context: http, server, location
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
第一個參數:$binary_remote_addr 表示通過remote_addr這個標識來做限制,“binary_”的目的 是縮寫內存佔用量,是限制同一客戶端ip地址。 第二個參數:zone=one:10m表示生成一個大小爲10M,名字爲one的內存區域,用來存儲訪問的 頻次信息。 第三個參數:rate=1r/s表示允許相同標識的客戶端的訪問頻次,這裏限制的是每秒1次,還可以有 比如30r/m的。
limit_req zone=one burst=5 nodelay;
例子:
http { limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s; server { location /search/ { limit_req zone=one burst=5 nodelay; } }
limit_req_zone $anti_spider zone=one:10m rate=10r/s; limit_req zone=one burst=100 nodelay; if ($http_user_agent ~* "googlebot|bingbot|Feedfetcher-Google") { set $anti_spider $http_user_agent; }
其他參數
Syntax: limit_req_log_level info | notice | warn | error; Default: limit_req_log_level error; Context: http, server, location
Syntax: limit_req_status code; Default: limit_req_status 503; Context: http, server, location
ngx_http_limit_conn_module 參數配置
Syntax: limit_conn zone number; Default: — Context: http, server, location limit_conn_zone $binary_remote_addr zone=addr:10m; server { location /download/ { limit_conn addr 1; }
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; }
Syntax: limit_conn_zone key zone=name:size;
Default: —
Context: http
limit_conn_zone $binary_remote_addr zone=addr:10m;
Syntax: limit_conn_log_level info | notice | warn | error;
Default:
limit_conn_log_level error;
Context: http, server, location
Syntax: limit_conn_status code; Default: limit_conn_status 503; Context: http, server, location
實戰
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s; server { location / { limit_req zone=mylimit; } }
實例二 burst緩存處理
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s; server { location / { limit_req zone=mylimit burst=4; } }
實例三 nodelay降低排隊時間
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s; server { location / { limit_req zone=mylimit; } } limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s; server { location / { limit_req zone=mylimit burst=4; } }
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s; server { location / { limit_req zone=mylimit burst=4 nodelay; limit_req_status 598; } }