Redis ツールセットの電流制限

この記事が役に立ったと思われる場合、または文章がかなり良いと思われる場合は、ケチらずに励まし (いいね/コメント/コレクション) をしてください。

コードリポジトリ

gitee: gitee.com/listen_w/re…
github: github.com/jettwangcj/…

序章

前回の記事:開発を容易にするために、Redis ツール セットを実装する予定です。主に Redis ツール セット開発の MQ (メッセージ キューとしてのストリーム データ構造) と遅延 (遅延キュー) 機能を紹介します。このドキュメントでは、主にRedis を使用して分散電流制限を行う方法の設計スキームを共有します

電流制限リクエスト

次の機能を備えたスロットル ツールがあれば幸いです。

  • スタンドアロン電流制限ではなく分散型電流制限。
  • 開発はできるだけ少なくする。
  • API (リクエスト) のフローを制限したり、特定のメソッドのフローを制限したりすることができ、柔軟に使用できます。
  • 電流制限には、IP 電流制限、インターフェース電流制限など、複数の側面があります。
  • ユーザーにとっては拡張するのに便利です。つまり、現在の制限の次元をユーザーが決定できます。

電流制限ツールに求められる機能を列挙しましたが、Spring Gateway の電流制限方法を参考に、Redis 電流制限ツールセットを設計しました。

デモ

それの使い方?

1. Maven 依存関係を導入します (現在、コードをダウンロードしてプライベート サーバーまたはローカル ウェアハウスにアップロードできます。後で Maven 中央ウェアハウスにプッシュされます)

 <dependency>
        <groupId>cn.org.wangchangjiu</groupId>
        <artifactId>redis-util-spring-boot-starter</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </dependency>

2. 設定ファイル(application.yaml)で電流制限機能を有効にする

redis:
  util:
    limit:
      enable: true

3. IP によってフローを制限したい場合は、構成ファイル (application.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 ディメンションによって制限されます。

    次に、 /** 電流制限構成、つまり次の構成も構成するとします。

     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中对于限流配置如下:

redis:
  util:
      limit:
        configs:
          - path: "/redis/*"     --- 拦截请求以/redis/*开头的这些url
            replenishRate: 1     --- 每秒中增加的令牌数量
            burstCapacity: 5     --- 桶容量
            keyResolver: "ipKeyResolver"  --- 限流维度
        enable: true

请求接口实现: 画像.png

jMeter 压测工具配置:

画像.png

画像.png

jMeter 压测结果: 画像.png

分析一下: 我们配置的桶容量是5个,也就是说初始化会有5个令牌(最高也只有5个,每秒中生产1个,到达5个后会丢弃),由上图 jMeter 我配置的是20个线程,1秒内跑完,1秒内没跑完,没有产生新的令牌,所以只有初始化的5个令牌可以使用,因此只能有5个请求通过。

如何扩展限流维度

如果你只是想 以ip限流或者以接口限流,那使用内置的 ipKeyResolverapiKeyResolver 就可以了,不需要额外的开发,经过上面的配置就行。

但是如果你有别的限流维度如何简单扩展呢?

案例:假如我有这样的一个需求:针对首页接口(/api/service/home),每个用户每秒限流10次,那该怎么做呢?

  • 第一步,你需要开发一个keyResolver(即限流的维度)

    @Bean
    public KeyResolver tokenKeyResolver(){
        return request -> request.getHeader("token");
    }
    
  • 然后,你需要把自己实现的 keyResolver 配置在 yaml

    redis:
      util:
          limit:
            configs:
              - path: "/api/service/home"     --- 拦截请求以/redis/*开头的这些url
                replenishRate: 1     --- 每秒中增加的令牌数量
                burstCapacity: 10     --- 桶容量
                keyResolver: "tokenKeyResolver"  --- 限流维度
            enable: true
    

就上面这样两步就可以扩展限流维度啦。

自定义 token(用户)维度限流演示

jMeter 压测工具配置: 画像.png

画像.png

jMeter 压测结果: 画像.png

分析: burstCapacity = 10 ,初始化10个令牌,3秒钟产生 3 个令牌,共记 13个令牌。

如果我只想对方法限流如何使用?

案例1:我想对某个方法以 IP维度 限流,如何实现?

如下图,在需要被限流的方法上增加注解 @RedisRateLimitConfig 即可; 画像.png

画像.png

所以上面的案例意思是,令牌桶容量是10个,每秒中产生1个令牌,也就是说,每分钟可以调用这个方法 70 次;

案例2:我想对某个方法限流,但是限流的维度(限流key)是与方法参数相关的,如何实现?

画像.png 如上图配置,keyResolver 支持配置某个Bean,也支持 配置SPEL。上面的配置是对于方法参数key为维度限流。

代码分析

代码结构

画像.png

详细分析

自动装配做了些啥?

画像.png 自动装配 RedisLimitAutoConfiguration 主要是创建了一下Bean;

  • limitRedisTemplate: 类型为 RedisTemplate<String, Object> 的Bean,用于发送redis命令,注意修改序列化方式;
  • redisRequestRateLimiterScript:类型为 RedisScript,用于加载 redis 脚本; 画像.png
  • RedisRateLimitAspect:aop 切面Bean,拦截被 @RedisRateLimitConfig 修饰的方法,用于基于方法的限流;
  • RedisLimitHandlerInterceptor:请求拦截器,拦截配置的请求,以及对请求限流;
  • ApiConfigResolver:请求配置解析器,把 yaml 配置文件里的配置解析成目标对象,用于对请求的限流;
  • RedisRateLimiter : redis コア電流制限器。コアは、実行のために lua スクリプトを redis に送信し、リクエスト トークンを取得できるかどうかを判断します。そのメソッド isAllowed は電流制限の結果です。
  • ipKeyResolver : 組み込みの IP ベースの電流制限戦略。
  • apiKeyResolver : 組み込みの API ベースの電流制限戦略。画像.png

リクエストに基づいてレート制限を構成する

具体的なプロセスは次のとおりです。画像.png

コンテナーが開始されると、ApiConfigResolver は yaml 構成ファイルを解析し、現在の制限構成を生成します。画像.png

  1. ユーザーがリクエストを開始します。
  2. リクエストはインターセプターによってインターセプトされ、構成はリクエスト構成パーサーから取得されます。画像.png
  3. インターセプターは、現在のリクエストの電流制限構成を取得します。画像.png
  4. インターセプターは、電流制限設定を通じて redisRateLimiter の isAllowed メソッドを呼び出し、制限されているかどうかを判断します。
  5. redisRateLimiter は、redisTemplate を通じてコマンドを redis に送信し、lua スクリプトを実行します。
  6. redisRateLimiter は戻りトークンを取得し、lua の戻り値が 1 (リクエストが許可されていることを示す) であるかどうかを判断します。 7.画像.pngインターセプターは、許可されているかどうかの redisRateLimiter の戻り値を取得し、許可されていれば通過し、許可されていない場合は 429 を返します。

メソッドに基づいてスロットリングを構成する

画像.pngメソッドベースとリクエストベースは似ていますが、トリガーポイント、つまりコアアスペクトコードが異なります。画像.png

コアLUAスクリプト

これは、springcloud ゲートウェイの電流制限用の 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

追記

他のモジュールの設計の詳細については後で説明します。誰でも使用することを歓迎します。より多くの提案や優れた機能ポイントを提供してください。それらを統合できます。

おすすめ

転載: juejin.im/post/7259686809906118693