1. 電流制限とは何ですか?なぜ流れを制限する必要があるのでしょうか?地下鉄の駅に入るのに列に並ばなければならないような、首都の地下鉄に乗ったことがあるだろうか。なぜこんなに長い列に並ばなければならないのですか?答えは流れを制限することです!地下鉄の乗車定員には限りがあるため、一度に多くの人が乗り込むと、ホームが混雑し、電車が過積載になり、安全上のリスクが生じます。同様に、私たちのプログラムにはリクエストを処理する能力が限られています。リクエスト数が処理制限を超えるとクラッシュします。最悪の衝突を避けるために、全員の駅への入場時間を遅らせなければなりません。電流制限はシステムの高可用性を確保するための重要な手段です。 ! ! [[324330]] インターネット企業によって生成されるトラフィック量が膨大であるため、システムがオンラインになるとトラフィックのピーク評価が行われます。特に、さまざまなフラッシュセール プロモーションの場合、システムが大量のトラフィックによって圧倒されないようにするために、システム トラフィックが特定のしきい値に達すると、トラフィックの一部が拒否されます。 電流制限により、システムは短期間でユーザーが利用できなくなります (この時間はミリ秒単位です)。一般的に、システムの処理能力を測定するために使用する指標は、1 秒あたりの QPS または TPS です。システムの 1 秒あたりのトラフィックしきい値が 1000 であると仮定すると、理論的には 1 秒以内に 1001 番目のリクエストが到着すると、このリクエストは制限されます。 2. 電流制限ソリューション1. カウンター Java では、AtomicInteger や Semaphore などのアトミック カウンターを使用して、単純な電流制限を実行することもできます。 -
- プライベート 最大カウント= 10 ;
-
- プライベート 長い間隔 = 60 ;
-
- プライベートAtomicInteger atomicInteger = new AtomicInteger( 0 );
-
- プライベート 長い開始時間 = System.currentTimeMillis();
-
- 公共 ブール制限( int最大数、 int間隔){
- アトミック整数を追加します。
- (atomicInteger.get() == 1 )の場合{
- 開始時間 = System.currentTimeMillis();
- アトミック整数を追加します。
- 戻る 真実;
- }
-
- System.currentTimeMillis() - 開始時間 > 間隔 * 1000 の場合{
- 開始時間 = System.currentTimeMillis();
- アトミック整数.set( 1 );
- 戻る 真実;
- }
-
- (atomicInteger.get() > maxCount)の場合{
- 戻る 間違い;
- }
- 戻る 真実;
- }
2. リーキーバケットアルゴリズム リーキーバケットアルゴリズムの考え方は非常にシンプルです。水をリクエストに例え、水漏れするバケツをシステムの処理能力の限界に例えてみましょう。まず水が漏れているバケツに入り、バケツの中の水は一定の割合で流れ出ます。流出量が流入量より少ない場合、漏れバケツの容量が限られているため、流入する後続の水は直接あふれ(要求を拒否)、流量制限を実現します。 3. トークンバケットアルゴリズム トークン バケット アルゴリズムの原理も比較的単純です。病院で医師の診察を受けるために登録するのと理解できます。番号を取得して初めて診断を受けることができます。 システムはトークン バケットを維持し、一定の割合でトークンをバケットに入れます。リクエストが届いて処理する必要がある場合は、まずバケットからトークンを取得する必要があります。バケット内にトークンがない場合、リクエストは拒否されます。トークン バケット アルゴリズムは、バケットの容量とトークンの発行レートを制御することでリクエストを制限します。 4. Redis + Lua 多くの学生は Lua が何であるかを知りません。私の意見では、Lua スクリプトは MySQL データベースのストアド プロシージャに似ています。一連のコマンドを実行し、すべてのコマンドの実行が成功するか失敗するかのいずれかになるため、アトミック性が実現されます。 Lua スクリプトは、ビジネス ロジックを含むコード ブロックとして理解することもできます。 Lua 自体はプログラミング言語です。 Redis は電流制限 API を公式に提供していませんが、Lua スクリプトの機能をサポートしています。これは、複雑なトークン バケット アルゴリズムまたはリーキー バケット アルゴリズムを実装するために使用できます。これは、分散システムで電流制限を実装する主な方法の 1 つでもあります。 Redis トランザクションと比較した場合、Lua スクリプトの利点は次のとおりです。 - ネットワーク オーバーヘッドを削減: Redis に複数のリクエストを送信する代わりに、Lua スクリプトを 1 回実行することで、ネットワーク転送を削減します。
- アトミック操作: Redisは、同時実行性を気にすることなく、Luaスクリプト全体をコマンドとしてアトミックに実行します。
- 再利用: Lua スクリプトが実行されると、Redis に永続的に保存され、他のクライアントで再利用できるようになります。
Lua スクリプトの一般的なロジックは次のとおりです。 - -- スクリプトを呼び出すときに渡される最初のキー値を取得します (現在の制限キーとして使用されます)
- ローカルキー = KEYS[ 1 ]
- -- スクリプトを呼び出すときに渡される最初のパラメータ値を取得します (現在の制限サイズ)
- ローカル制限 = tonumber(ARGV[ 1 ])
-
- -- 現在のフローサイズを取得します
- ローカルの currentLimit = tonumber(redis.call( 'get' , key) または"0" )
-
- -- 電流制限を超えているかどうか
- 現在の制限 + 1 > 制限の場合
- -- 返品(拒否)
- 戻る 0
- それ以外
- -- 値+1を超えない
- redis.call( "INCRBY" , キー, 1 )
- -- 有効期限を設定する
- redis.call( "EXPIRE" , キー, 2 )
- -- 戻る(リリース)
- 戻る 1
- 終わり
- KEYS[1]を通じて渡されたキーパラメータを取得する
- 渡された制限パラメータをARGV[1]経由で取得する
- redis.call メソッドは、キャッシュからキーに関連付けられた値を取得します。 nullの場合は0を返します
- 次に、キャッシュに記録された値が制限サイズより大きいかどうかを判断します。超過した場合は、電流が制限されていることを意味し、0を返します。
- 制限を超えていない場合は、キーのキャッシュ値が 1 増加し、有効期限が 1 秒後に設定され、キャッシュ値 + 1 が返されます。
この方法はこの記事で推奨されている解決策であり、具体的な実装については後ほど詳しく説明します。 5. ゲートウェイ層の電流制限 電流制限は、多くの場合、Nginx、Openresty、kong、zuul、Spring Cloud Gateway などのゲートウェイ レイヤーで行われます。Spring Cloud Gateway の電流制限の基本的な実装原理は、組み込みの Lua 電流制限スクリプトを介して、Redis + Lua に基づいています。 3. Redis + Lua 電流制限の実装次に、カスタム アノテーション、AOP、Redis + Lua を使用して電流制限を実装します。手順はさらに詳細になります。初心者がすぐに始められるように、ここではもう少し詳しく説明します。経験豊富なベテランの皆様、どうか我慢してください。 1. 環境整備 Springboot プロジェクト作成アドレス: https://start.spring.io、非常に便利で実用的なツールです。 2. 依存パッケージを導入する pom ファイルに次の依存パッケージを追加します。重要なものは、spring-boot-starter-data-redis と spring-boot-starter-aop です。 - <依存関係>
- <依存関係>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- </依存関係>
- <依存関係>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- </依存関係>
- <依存関係>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-aop</artifactId>
- </依存関係>
- <依存関係>
- <groupId>com.google.guava</groupId>
- <artifactId>グアバ</artifactId>
- <バージョン> 21.0 </バージョン>
- </依存関係>
- <依存関係>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-test</artifactId>
- </依存関係>
- <依存関係>
- <グループ ID>org.apache.commons</グループ ID>
- <artifactId>コモンズ-lang3</artifactId>
- </依存関係>
-
- <依存関係>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-test</artifactId>
- <scope>テスト</scope>
- <除外事項>
- <除外>
- <groupId>org.junit.vintage</groupId>
- <artifactId>junit-vintage-engine</artifactId>
- </除外>
- </除外>
- </依存関係>
- </依存関係>
3. application.propertiesを構成する application.properties ファイルで、事前設定された Redis サービス アドレスとポートを構成します。 - spring.redis.host= 127.0 。 0.1
-
- spring.redis.ポート = 6379
4. RedisTemplateインスタンスを構成する - @構成
- 公共 クラスRedisLimiterHelper {
-
- @ビーン
- パブリックRedisTemplate<String, Serializable> limitRedisTemplate(LettuceConnectionFactory redisConnectionFactory) {
- RedisTemplate<String, Serializable> テンプレート = new RedisTemplate<>();
- テンプレート.setKeySerializer(新しいStringRedisSerializer());
- テンプレート.setValueSerializer(新しいGenericJackson2JsonRedisSerializer());
- テンプレート。setConnectionFactory(redisConnectionFactory);
- 戻りテンプレート;
- }
- }
電流制限型列挙クラス -
- 公共 列挙型制限タイプ{
-
-
-
- お客様、
-
-
-
- 知的財産;
- }
5. カスタム注釈 @Limit アノテーションをカスタマイズします。アノテーション タイプは ElementType.METHOD で、メソッドに作用します。 Period はリクエスト制限期間を示し、count は期間内にリリースできるリクエストの数を示します。 limitType は、要求された IP アドレスまたはカスタム キーに基づく現在の制限のタイプを表します。 limitType 属性が渡されない場合、メソッド名がデフォルトのキーとして使用されます。 -
-
- @ターゲット({ElementType.METHOD, ElementType.TYPE})
- @保持(保持ポリシー.RUNTIME)
- @継承
- @文書化済み
- 公共 @インターフェース制限{
-
-
-
- 文字列名()デフォルト "" ;
-
-
-
- 文字列キー()デフォルト "" ;
-
-
-
- 文字列プレフィックス()デフォルト "" ;
-
-
-
- 期間();
-
-
-
- 整数カウント();
-
-
-
- LimitType limitType()のデフォルトLimitType.CUSTOMER;
- }
6. アスペクトコードの実装 -
- @側面
- @構成
- 公共 クラスLimitInterceptor {
-
- プライベート 静的 最終的なLogger ロガー = LoggerFactory.getLogger(LimitInterceptor. class );
-
- プライベート 静的 最終的な文字列 UNKNOWN = "不明" ;
-
- プライベート 最終的なRedisTemplate<String, Serializable> limitRedisTemplate;
-
- オートワイヤード
- パブリックLimitInterceptor(RedisTemplate<String, Serializable> limitRedisTemplate) {
- これは.limitRedisTemplate = limitRedisTemplate; です。
- }
-
-
-
- @Around ( "実行(public * *(..)) && @annotation(com.xiaofu.limit.api.Limit)" )
- パブリックオブジェクトインターセプター(ProceedingJoinPoint pjp) {
- メソッド署名署名 = (メソッド署名) pjp.getSignature();
- メソッド method = signature.getMethod();
- 制限 limitAnnotation = method.getAnnotation( Limit.class );
- 制限タイプ limitType = limitAnnotation.limitType();
- 文字列名 = limitAnnotation.name();
- 文字列キー;
- 制限期間= limitAnnotation.period();
- 制限カウント= limitAnnotation.count();
-
-
-
- スイッチ(制限タイプ) {
- ケースIP:
- キー = getIpAddress();
- 壊す;
- ケース顧客:
- キー = limitAnnotation.key();
- 壊す;
- デフォルト:
- キー = StringUtils.upperCase(method.getName());
- }
-
- ImmutableList<String> キー = ImmutableList.of(StringUtils.join(limitAnnotation.prefix(), key));
- 試す{
- 文字列 luaScript = buildLuaScript();
- RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number. class );
- 数値 count = limitRedisTemplate.execute(redisScript, keys, limitCount, limitPeriod);
- logger.info( "name={}、key = {} のアクセス試行回数は {} です" 、count、name、key);
- (count != null && count.intValue() <= limitCount)の場合{
- pjp.proceed()を返します。
- }それ以外{
- 投げる new RuntimeException( "ブラックリストに引き込まれました" );
- }
- }キャッチ(Throwable e) {
- if (eインスタンスのRuntimeException) {
- 投げる 新しいRuntimeException(e.getLocalizedMessage());
- }
- 投げる 新しいRuntimeException( "サーバー例外" );
- }
- }
-
-
-
- パブリック文字列buildLuaScript() {
- StringBuilder lua =新しいStringBuilder();
- lua.append( "ローカルc" );
- lua.append( "\nc = redis.call('get',KEYS[1])" );
-
- lua.append( "\ncかつtonumber(c) > tonumber(ARGV[1])の場合" );
- lua.append( "\nreturn c;" );
- lua.append( "\nend" );
-
- lua.append( "\nc = redis.call('incr',KEYS[1])" );
- lua.append( "\nif tonumber(c) == 1 then" );
-
- lua.append( "\nredis.call('expire',KEYS[1],ARGV[2])" );
- lua.append( "\nend" );
- lua.append( "\nreturn c;" );
- lua.toString()を返します。
- }
-
-
-
-
- パブリック文字列 getIpAddress() {
- HttpServletRequest リクエスト = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
- 文字列 ip = request.getHeader( "x-forwarded-for" );
- if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
- ip = request.getHeader( "プロキシクライアント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 = リクエスト.getRemoteAddr();
- }
- IPを返します。
- }
- }
7. 制御層の実装 電流制限が必要なインターフェース メソッドに @Limit アノテーションを適用します。以下では、メソッドに @Limit アノテーションを設定し、10 秒以内に 3 つのリクエストのみがリリースされるようにします。ここでは、直感性のために AtomicInteger を使用してカウントします。 -
-
- @レストコントローラ
- 公共 クラスリミッターコントローラ{
-
- プライベート 静的 最終的なAtomicInteger ATOMIC_INTEGER_1 =新しいAtomicInteger();
- プライベート 静的 最終的なAtomicInteger ATOMIC_INTEGER_2 =新しいAtomicInteger();
- プライベート 静的 最終的なAtomicInteger ATOMIC_INTEGER_3 =新しいAtomicInteger();
-
-
-
- @Limit (キー = "limitTest" 、期間 = 10 、カウント = 3 )
- @GetMapping ( "/limitTest1" )
- 公共 intテストリミッター1() {
-
- ATOMIC_INTEGER_1.incrementAndGet()を返します。
- }
-
-
-
- @Limit (キー = "customer_limit_test" 、期間 = 10 、カウント = 3 、limitType = LimitType.CUSTOMER)
- @GetMapping ( "/limitTest2" )
- 公共 intテストリミッター2() {
-
- ATOMIC_INTEGER_2.incrementAndGet()を返します。
- }
-
-
-
- @Limit (キー = "ip_limit_test" 、期間 = 10 、カウント = 3 、limitType = LimitType.IP)
- @GetMapping ( "/limitTest3" )
- 公共 intテストリミッター3() {
-
- ATOMIC_INTEGER_3.incrementAndGet()を返します。
- }
-
- }
8. テスト テストの「期待値」: 3 つの連続した要求が成功し、4 番目の要求が拒否されます。次に、期待通りの効果が得られるか確認してみましょう。リクエストアドレスは http://127.0.0.1:8080/limitTest1 です。 Postman を使用してテストします。 Postman URL をブラウザに直接貼り付けた場合も同様です。 4 番目のリクエストが行われたときに、アプリケーションがリクエストを直接拒否したことがわかり、Springboot + aop + lua の電流制限ソリューションが正常に構築されたことがわかります。 上記の springboot + aop + Lua 電流制限の実装は比較的単純で、電流制限とは何かを誰もが理解できるようにすることを目的としています。簡単な電流制限機能の作り方。面接の前にこれが何なのか知っておく必要があります。上記では電流制限を実装するためのいくつかのソリューションについて説明しましたが、選択は特定のビジネス シナリオに基づいて行う必要があり、単に使用するためにだけ使用すべきではありません。 |