天天看點

Redis工具集之限流

作者:小心程式猿QAQ

簡介

前一篇文章:為了友善開發,我打算實作一個Redis 工具集 主要介紹了開發 Redis 工具集的 MQ(Stream資料結構做消息隊列)、delay(延遲隊列)功能,這篇檔案主要分享一下使用 redis 如何做分布式限流的設計方案。

限流訴求

我希望有一個限流工具,它具備以下功能:

  • 分布式限流,不是單機限流;
  • 盡量少的開發;
  • 使用靈活,可以對API(請求)限流,也可以對某一個方法限流;
  • 能夠有多種次元限流,比如按照 IP限流、接口限流等;
  • 友善使用者拓展,也就是限流的次元可以由使用者确定;

我列舉了一個限流工具應該具備的功能,我參考了 spring gateway 的限流方式,設計了 redis 限流工具集;

使用示範

如何使用呢?

1.引入 Maven 依賴(目前可以下載下傳代碼上傳到自己的私服或者本地倉庫,後面會推到 Maven 中央倉庫)

xml複制代碼 <dependency>
        <groupId>cn.org.wangchangjiu</groupId>
        <artifactId>redis-util-spring-boot-starter</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </dependency>
           

2.配置檔案(application.yaml)開啟限流功能

yaml複制代碼redis:
  util:
    limit:
      enable: true
           

3.假如我想以 IP 限流,那麼在 配置檔案(application.yaml)中如下配置

yaml複制代碼redis:
  util:
      limit:
        configs:
          - path: "/redis/*"     --- 攔截請求以/redis/*開頭的這些url
            replenishRate: 1     --- 每秒中增加的令牌數量
            burstCapacity: 5     --- 桶容量
            keyResolver: "ipKeyResolver"  --- 限流次元
        enable: true
           

解釋一下上面的配置:

configs :是每一個限流的配置,你可以配置多個,也就是可以對不同的請求給以不同的限流次元;

path: 表示需要攔截比對哪些 URL,假如說我需要對redis開頭(/redis/*)的請求url,使用IP限流,那麼對于所有以 redis開頭的url都會被以IP次元限流了。 那如果我還配置了一個 /** 的限流配置,即如下配置: yaml複制代碼 redis: util: limit: configs: - path: "/**" replenishRate: 1 burstCapacity: 10 keyResolver: "apiKeyResolver" --- 限流次元 - path: "/redis/*" --- 攔截請求以/redis/*開頭的這些url replenishRate: 1 --- 每秒中增加的令牌數量 burstCapacity: 5 --- 桶容量 keyResolver: "ipKeyResolver" --- 限流次元 enable: true 對于這種情況,如果請求 url:/redis/limit,該被哪個限流方案生效呢?答案是,生效的是 ipKeyResolver, 因為 url:/redis/limit 最比對path: "/redis/*";

以上就是所有限流的開發了,是不是很簡單,也就說隻要簡單配置 yaml 檔案不需要開發其他代碼就可以實作以 ip 限流或者以 請求API限流(apiKeyResolver)。

以IP限流為例,示範效果

yaml中對于限流配置如下:

yaml複制代碼redis:
  util:
      limit:
        configs:
          - path: "/redis/*"     --- 攔截請求以/redis/*開頭的這些url
            replenishRate: 1     --- 每秒中增加的令牌數量
            burstCapacity: 5     --- 桶容量
            keyResolver: "ipKeyResolver"  --- 限流次元
        enable: true
           

請求接口實作:

Redis工具集之限流

jMeter 壓測工具配置:

Redis工具集之限流
Redis工具集之限流

jMeter 壓測結果:

Redis工具集之限流

分析一下: 我們配置的桶容量是5個,也就是說初始化會有5個令牌(最高也隻有5個,每秒中生産1個,到達5個後會丢棄),由上圖 jMeter 我配置的是20個線程,1秒内跑完,1秒内沒跑完,沒有産生新的令牌,是以隻有初始化的5個令牌可以使用,是以隻能有5個請求通過。

如何擴充限流次元

如果你隻是想 以ip限流或者以接口限流,那使用内置的 ipKeyResolver 和 apiKeyResolver 就可以了,不需要額外的開發,經過上面的配置就行。

但是如果你有别的限流次元如何簡單擴充呢?

案例:假如我有這樣的一個需求:針對首頁接口(/api/service/home),每個使用者每秒限流10次,那該怎麼做呢?

  • 第一步,你需要開發一個keyResolver(即限流的次元)
  • typescript複制代碼
  • @Bean public KeyResolver tokenKeyResolver(){ return request -> request.getHeader("token"); }
  • 然後,你需要把自己實作的 keyResolver 配置在 yaml
  • yaml複制代碼
  • redis: util: limit: configs: - path: "/api/service/home" --- 攔截請求以/redis/*開頭的這些url replenishRate: 1 --- 每秒中增加的令牌數量 burstCapacity: 10 --- 桶容量 keyResolver: "tokenKeyResolver" --- 限流次元 enable: true

就上面這樣兩步就可以擴充限流次元啦。

自定義 token(使用者)次元限流示範

jMeter 壓測工具配置:

Redis工具集之限流
Redis工具集之限流

jMeter 壓測結果:

Redis工具集之限流

分析: burstCapacity = 10 ,初始化10個令牌,3秒鐘産生 3 個令牌,共記 13個令牌。

如果我隻想對方法限流如何使用?

案例1:我想對某個方法以 IP次元 限流,如何實作?

如下圖,在需要被限流的方法上增加注解 @RedisRateLimitConfig 即可;

Redis工具集之限流
Redis工具集之限流

是以上面的案例意思是,令牌桶容量是10個,每秒中産生1個令牌,也就是說,每分鐘可以調用這個方法 70 次;

案例2:我想對某個方法限流,但是限流的次元(限流key)是與方法參數相關的,如何實作?

Redis工具集之限流

如上圖配置,keyResolver 支援配置某個Bean,也支援 配置SPEL。上面的配置是對于方法參數key為次元限流。

代碼分析

代碼結構

Redis工具集之限流

詳細分析

自動裝配做了些啥?

Redis工具集之限流

自動裝配 RedisLimitAutoConfiguration 主要是建立了一下Bean;

  • limitRedisTemplate: 類型為 RedisTemplate<String, Object> 的Bean,用于發送redis指令,注意修改序列化方式;
  • redisRequestRateLimiterScript:類型為 RedisScript,用于加載 redis 腳本;
  • RedisRateLimitAspect:aop 切面Bean,攔截被 @RedisRateLimitConfig 修飾的方法,用于基于方法的限流;
  • RedisLimitHandlerInterceptor:請求攔截器,攔截配置的請求,以及對請求限流;
  • ApiConfigResolver:請求配置解析器,把 yaml 配置檔案裡的配置解析成目标對象,用于對請求的限流;
  • RedisRateLimiter:redis核心限流器,核心是将lua腳本發到redis中執行,判斷是否能拿到請求令牌,其方法 isAllowed 是限流的結果;
  • ipKeyResolver:内置基于IP限流政策;
  • apiKeyResolver:内置基于api限流政策;

基于請求配置限流

具體流程如下圖:

Redis工具集之限流

ApiConfigResolver 在容器啟動之後,會解析 yaml 配置檔案,生成限流配置;

Redis工具集之限流
  1. 使用者發起請求;
  2. 請求被攔截器攔截,從請求配置解析器中擷取配置;
  3. 攔截器擷取目前請求的限流配置;
  4. 攔截器通過限流配置調用 redisRateLimiter 的 isAllowed 方法判斷是否被限流;
  5. redisRateLimiter 通過redisTemplate向redis發送指令,執行lua腳本;
  6. redisRateLimiter 擷取傳回令牌,判斷lua傳回值是否為1(表示允許請求) 7.攔截器拿到redisRateLimiter傳回的是否允許,允許則通過,否則傳回 429;

基于方法配置限流

Redis工具集之限流

基于方法和基于請求的類似,隻是觸發點不一樣,核心切面代碼:

Redis工具集之限流

核心lua腳本

這也是 springcloud gateway 限流的lua腳本。

lua複制代碼--生産速率,每秒生産多少個令牌
local rate = tonumber(ARGV[1])
--令牌桶容量
local capacity = tonumber(ARGV[2])
--目前時間(秒級時間戳)
local now = tonumber(ARGV[3])
--每個請求消耗的令牌個數 固定為 1
local requested = tonumber(ARGV[4])

--填充時間=容量/生産速率
local fill_time = capacity/rate
--key過期時間設定為填充時間的2倍
local ttl = math.floor(fill_time*2)

-- 擷取剩餘令牌數量 , KEYS[1] redis key名,用于儲存限流次元下剩餘令牌數量,request_rate_limiter.{id}.tokens
local last_tokens = tonumber(redis.call("get", KEYS[1]))
--不存在key,則初始化令牌數量為最大容量,也就是說的 初始化時令牌數為桶容量
if last_tokens == nil then
  last_tokens = capacity
end

--最近擷取令牌秒級時間戳
local last_refreshed = tonumber(redis.call("get", KEYS[2]))
if last_refreshed == nil then
  last_refreshed = 0
end

--距離上次擷取令牌時間相差多少秒
local delta = math.max(0, now-last_refreshed)

--計算目前令牌數量(考慮delta時間内生成的令牌個數=delta*速率),取 容量和 剩餘令牌+生成令牌數 的最小值,也就是說,達到容量後,就令牌就丢棄了
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))

--目前令牌數量是否大于1
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
local allowed_num = 0

--允許通路,新令牌數量-1,allowed_num=1
if allowed then
  new_tokens = filled_tokens - requested
  allowed_num = 1
end

--儲存令牌個數和最近擷取令牌時間
redis.call("setex", KEYS[1], ttl, new_tokens)
redis.call("setex", KEYS[2], ttl, now)

return allowed_num
           

後記

其他子產品的設計細節後面再說,歡迎大家使用,也請大家多多提建議以及好的功能點,我都可以整合上去。

作者:程式員中的廢物

連結:https://juejin.cn/post/7259686809906118693

繼續閱讀