当社では、非常に強力な Redis 分散電流リミッターを 6 年間使用しています。

当社では、非常に強力な Redis 分散電流リミッターを 6 年間使用しています。

1. 電流制限とは何ですか?なぜ流れを制限する必要があるのでしょうか?

地下鉄の駅に入るのに列に並ばなければならないような、首都の地下鉄に乗ったことがあるだろうか。なぜこんなに長い列に並ばなければならないのですか?答えは流れを制限することです!地下鉄の乗車定員には限りがあるため、一度に多くの人が乗り込むと、ホームが混雑し、電車が過積載になり、安全上のリスクが生じます。同様に、私たちのプログラムにはリクエストを処理する能力が限られています。リクエスト数が処理制限を超えるとクラッシュします。最悪の衝突を避けるために、全員の駅への入場時間を遅らせなければなりません。電流制限はシステムの高可用性を確保するための重要な手段です。 ! !

[[324330]]

インターネット企業によって生成されるトラフィック量が膨大であるため、システムがオンラインになるとトラフィックのピーク評価が行われます。特に、さまざまなフラッシュセール プロモーションの場合、システムが大量のトラフィックによって圧倒されないようにするために、システム トラフィックが特定のしきい値に達すると、トラフィックの一部が拒否されます。

電流制限により、システムは短期間でユーザーが利用できなくなります (この時間はミリ秒単位です)。一般的に、システムの処理能力を測定するために使用する指標は、1 秒あたりの QPS または TPS です。システムの 1 秒あたりのトラフィックしきい値が 1000 であると仮定すると、理論的には 1 秒以内に 1001 番目のリクエストが到着すると、このリクエストは制限されます。

2. 電流制限ソリューション

1. カウンター
Java では、AtomicInteger や Semaphore などのアトミック カウンターを使用して、単純な電流制限を実行することもできます。

  1. // 現在の制限数 
  2. プライベート 最大カウント= 10 ;
  3. // 指定された時間内 
  4. プライベート 長い間隔 = 60 ;
  5. // アトミックカウンター 
  6. プライベートAtomicInteger atomicInteger = new AtomicInteger( 0 );
  7. // 開始時間 
  8. プライベート 長い開始時間 = System.currentTimeMillis();
  9.  
  10. 公共 ブール制限( int最大数、 int間隔){
  11. アトミック整数を追加します
  12. (atomicInteger.get() == 1 )の場合{
  13. 開始時間 = System.currentTimeMillis();
  14. アトミック整数を追加します
  15. 戻る 真実;
  16. }
  17. // 間隔時間が経過した場合は、カウントを再開します 
  18. System.currentTimeMillis() - 開始時間 > 間隔 * 1000 の場合{
  19. 開始時間 = System.currentTimeMillis();
  20. アトミック整数.set( 1 );
  21. 戻る 真実;
  22. }
  23. // 間隔内で現在の制限数を超えていないか確認する 
  24. (atomicInteger.get() > maxCount)の場合{
  25. 戻る 間違い;
  26. }
  27. 戻る 真実;
  28. }

2. リーキーバケットアルゴリズム

リーキーバケットアルゴリズムの考え方は非常にシンプルです。水をリクエストに例え、水漏れするバケツをシステムの処理能力の限界に例えてみましょう。まず水が漏れているバケツに入り、バケツの中の水は一定の割合で流れ出ます。流出量が流入量より少ない場合、漏れバケツの容量が限られているため、流入する後続の水は直接あふれ(要求を拒否)、流量制限を実現します。

3. トークンバケットアルゴリズム

トークン バケット アルゴリズムの原理も比較的単純です。病院で医師の診察を受けるために登録するのと理解できます。番号を取得して初めて診断を受けることができます。

システムはトークン バケットを維持し、一定の割合でトークンをバケットに入れます。リクエストが届いて処理する必要がある場合は、まずバケットからトークンを取得する必要があります。バケット内にトークンがない場合、リクエストは拒否されます。トークン バケット アルゴリズムは、バケットの容量とトークンの発行レートを制御することでリクエストを制限します。

4. Redis + Lua

多くの学生は Lua が何であるかを知りません。私の意見では、Lua スクリプトは MySQL データベースのストアド プロシージャに似ています。一連のコマンドを実行し、すべてのコマンドの実行が成功するか失敗するかのいずれかになるため、アトミック性が実現されます。 Lua スクリプトは、ビジネス ロジックを含むコード ブロックとして理解することもできます。

Lua 自体はプログラミング言語です。 Redis は電流制限 API を公式に提供していませんが、Lua スクリプトの機能をサポートしています。これは、複雑なトークン バケット アルゴリズムまたはリーキー バケット アルゴリズムを実装するために使用できます。これは、分散システムで電流制限を実装する主な方法の 1 つでもあります。

Redis トランザクションと比較した場合、Lua スクリプトの利点は次のとおりです。

  1. ネットワーク オーバーヘッドを削減: Redis に複数のリクエストを送信する代わりに、Lua スクリプトを 1 回実行することで、ネットワーク転送を削減します。
  2. アトミック操作: Redisは、同時実行性を気にすることなく、Luaスクリプト全体をコマンドとしてアトミックに実行します。
  3. 再利用: Lua スクリプトが実行されると、Redis に永続的に保存され、他のクライアントで再利用できるようになります。

Lua スクリプトの一般的なロジックは次のとおりです。

  1. -- スクリプトを呼び出すときに渡される最初のキー値を取得します (現在の制限キーとして使用されます)
  2. ローカルキー = KEYS[ 1 ]
  3. -- スクリプトを呼び出すときに渡される最初のパラメータ値を取得します (現在の制限サイズ)
  4. ローカル制限 = tonumber(ARGV[ 1 ])
  5.  
  6. -- 現在のフローサイズを取得します
  7. ローカルの currentLimit = tonumber(redis.call( 'get' , key) または"0" )
  8.  
  9. -- 電流制限を超えているかどうか
  10. 現在の制限 + 1 > 制限の場合
  11. -- 返品(拒否)
  12. 戻る  0  
  13. それ以外 
  14. -- 値+1を超えない 
  15. redis.call( "INCRBY" , キー, 1 )
  16. -- 有効期限を設定する
  17. redis.call( "EXPIRE" , キー, 2 )
  18. -- 戻る(リリース)
  19. 戻る  1  
  20. 終わり
  • 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 です。

  1. <依存関係>
  2. <依存関係>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-web</artifactId>
  5. </依存関係>
  6. <依存関係>
  7. <groupId>org.springframework.boot</groupId>
  8. <artifactId>spring-boot-starter-data-redis</artifactId>
  9. </依存関係>
  10. <依存関係>
  11. <groupId>org.springframework.boot</groupId>
  12. <artifactId>spring-boot-starter-aop</artifactId>
  13. </依存関係>
  14. <依存関係>
  15. <groupId>com.google.guava</groupId>
  16. <artifactId>グアバ</artifactId>
  17. <バージョン> 21.0 </バージョン>
  18. </依存関係>
  19. <依存関係>
  20. <groupId>org.springframework.boot</groupId>
  21. <artifactId>spring-boot-starter-test</artifactId>
  22. </依存関係>
  23. <依存関係>
  24. <グループ ID>org.apache.commons</グループ ID>
  25. <artifactId>コモンズ-lang3</artifactId>
  26. </依存関係>
  27.  
  28. <依存関係>
  29. <groupId>org.springframework.boot</groupId>
  30. <artifactId>spring-boot-starter-test</artifactId>
  31. <scope>テスト</scope>
  32. <除外事項>
  33. <除外>
  34. <groupId>org.junit.vintage</groupId>
  35. <artifactId>junit-vintage-engine</artifactId>
  36. </除外>
  37. </除外>
  38. </依存関係>
  39. </依存関係>

3. application.propertiesを構成する

application.properties ファイルで、事前設定された Redis サービス アドレスとポートを構成します。

  1. spring.redis.host= 127.0 0.1  
  2.  
  3. spring.redis.ポート = 6379  

4. RedisTemplateインスタンスを構成する

  1. @構成 
  2. 公共 クラスRedisLimiterHelper {
  3.  
  4. @ビーン 
  5. パブリックRedisTemplate<String, Serializable> limitRedisTemplate(LettuceConnectionFactory redisConnectionFactory) {
  6. RedisTemplate<String, Serializable> テンプレート = new RedisTemplate<>();
  7. テンプレート.setKeySerializer(新しいStringRedisSerializer());
  8. テンプレート.setValueSerializer(新しいGenericJackson2JsonRedisSerializer());
  9. テンプレート。setConnectionFactory(redisConnectionFactory);
  10. 戻りテンプレート;
  11. }
  12. }

電流制限型列挙クラス

  1. /**
  2. * @著者 fu
  3. * @description 電流制限タイプ
  4. * @日付 2020/4/8 13:47
  5. */  
  6. 公共 列挙型制限タイプ{
  7.  
  8. /**
  9. * カスタムキー
  10. */  
  11. お客様、
  12.  
  13. /**
  14. * リクエスト元IP
  15. */  
  16. 知的財産;
  17. }

5. カスタム注釈

@Limit アノテーションをカスタマイズします。アノテーション タイプは ElementType.METHOD で、メソッドに作用します。

Period はリクエスト制限期間を示し、count は期間内にリリースできるリクエストの数を示します。 limitType は、要求された IP アドレスまたはカスタム キーに基づく現在の制限のタイプを表します。 limitType 属性が渡されない場合、メソッド名がデフォルトのキーとして使用されます。

  1. /**
  2. * @著者 fu
  3. * @description カスタム電流制限注釈
  4. * @日付 2020/4/8 13:15
  5. */  
  6. @ターゲット({ElementType.METHOD, ElementType.TYPE})
  7. @保持(保持ポリシー.RUNTIME)
  8. @継承 
  9. @文書化済み 
  10. 公共  @インターフェース制限{
  11.  
  12. /**
  13. * 名前
  14. */  
  15. 文字列名()デフォルト  "" ;
  16.  
  17. /**
  18. * 鍵
  19. */  
  20. 文字列キー()デフォルト  "" ;
  21.  
  22. /**
  23. * キープレフィックス
  24. */  
  25. 文字列プレフィックス()デフォルト  "" ;
  26.  
  27. /**
  28. * 指定された時間範囲(秒単位)
  29. */  
  30. 期間();
  31.  
  32. /**
  33. * 一定期間内の最大訪問回数
  34. */  
  35. 整数カウント();
  36.  
  37. /**
  38. * 電流制限の種類(ユーザー定義キーまたはリクエストIP)
  39. */  
  40. LimitType limitType()のデフォルトLimitType.CUSTOMER;
  41. }

6. アスペクトコードの実装

  1. /**
  2. * @著者 fu
  3. * @description 電流制限アスペクトの実装
  4. * @日付 2020/4/8 13:04
  5. */  
  6. @側面 
  7. @構成 
  8. 公共 クラスLimitInterceptor {
  9.  
  10. プライベート 静的 最終的なLogger ロガー = LoggerFactory.getLogger(LimitInterceptor. class );
  11.  
  12. プライベート 静的 最終的な文字列 UNKNOWN = "不明" ;
  13.  
  14. プライベート 最終的なRedisTemplate<String, Serializable> limitRedisTemplate;
  15.  
  16. オートワイヤード 
  17. パブリックLimitInterceptor(RedisTemplate<String, Serializable> limitRedisTemplate) {
  18. これは.limitRedisTemplate = limitRedisTemplate; です。
  19. }
  20.  
  21. /**
  22. * @param pjp
  23. * @著者 fu
  24. * @description アスペクト
  25. * @日付 2020/4/8 13:04
  26. */  
  27. @Around ( "実行(public * *(..)) && @annotation(com.xiaofu.limit.api.Limit)" )
  28. パブリックオブジェクトインターセプター(ProceedingJoinPoint pjp) {
  29. メソッド署名署名 = (メソッド署名) pjp.getSignature();
  30. メソッド method = signature.getMethod();
  31. 制限 limitAnnotation = method.getAnnotation( Limit.class );
  32. 制限タイプ limitType = limitAnnotation.limitType();
  33. 文字列名 = limitAnnotation.name();
  34. 文字列キー;
  35. 制限期間= limitAnnotation.period();
  36. 制限カウント= limitAnnotation.count();
  37.  
  38. /**
  39. * 電流制限タイプに応じて異なるキーを取得します。渡さない場合は、メソッド名をキーとして使用します
  40. */  
  41. スイッチ(制限タイプ) {
  42. ケースIP:
  43. キー = getIpAddress();
  44. 壊す;
  45. ケース顧客:
  46. キー = limitAnnotation.key();
  47. 壊す;
  48. デフォルト
  49. キー = StringUtils.upperCase(method.getName());
  50. }
  51.  
  52. ImmutableList<String> キー = ImmutableList.of(StringUtils.join(limitAnnotation.prefix(), key));
  53. 試す{
  54. 文字列 luaScript = buildLuaScript();
  55. RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number. class );
  56. 数値 count = limitRedisTemplate.execute(redisScript, keys, limitCount, limitPeriod);
  57. logger.info( "name={}、key = {} のアクセス試行回数は {} です" 、count、name、key);
  58. (count != null && count.intValue() <= limitCount)の場合{
  59. pjp.proceed()を返します
  60. }それ以外{
  61. 投げる  new RuntimeException( "ブラックリストに引き込まれました" );
  62. }
  63. }キャッチ(Throwable e) {
  64. if (eインスタンスのRuntimeException) {
  65. 投げる 新しいRuntimeException(e.getLocalizedMessage());
  66. }
  67. 投げる 新しいRuntimeException( "サーバー例外" );
  68. }
  69. }
  70.  
  71. /**
  72. * @著者 fu
  73. * @description Redis Luaの電流制限スクリプトを書く
  74. * @日付 2020/4/8 13:24
  75. */  
  76. パブリック文字列buildLuaScript() {
  77. StringBuilder lua =新しいStringBuilder();
  78. lua.append( "ローカルc" );
  79. lua.append( "\nc = redis.call('get',KEYS[1])" );
  80. // 呼び出しが最大値を超えない場合は、直接戻ります 
  81. lua.append( "\ncかつtonumber(c) > tonumber(ARGV[1])の場合" );
  82. lua.append( "\nreturn c;" );
  83. lua.append( "\nend" );
  84. // 計算機を実行して追加する 
  85. lua.append( "\nc = redis.call('incr',KEYS[1])" );
  86. lua.append( "\nif tonumber(c) == 1 then" );
  87. //最初の呼び出しから電流制限を開始し、対応するキー値の有効期限を設定します 
  88. lua.append( "\nredis.call('expire',KEYS[1],ARGV[2])" );
  89. lua.append( "\nend" );
  90. lua.append( "\nreturn c;" );
  91. lua.toString()を返します
  92. }
  93.  
  94.  
  95. /**
  96. * @著者 fu
  97. * @description ID アドレスを取得する
  98. * @日付 2020/4/8 13:24
  99. */  
  100. パブリック文字列 getIpAddress() {
  101. HttpServletRequest リクエスト = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
  102. 文字列 ip = request.getHeader( "x-forwarded-for" );
  103. if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
  104. ip = request.getHeader( "プロキシクライアントIP" );
  105. }
  106. if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
  107. ip = request.getHeader( "WL-Proxy-Client-IP" );
  108. }
  109. if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
  110. ip = リクエスト.getRemoteAddr();
  111. }
  112. IPを返します
  113. }
  114. }

7. 制御層の実装

電流制限が必要なインターフェース メソッドに @Limit アノテーションを適用します。以下では、メソッドに @Limit アノテーションを設定し、10 秒以内に 3 つのリクエストのみがリリースされるようにします。ここでは、直感性のために AtomicInteger を使用してカウントします。

  1. /**
  2. * @著者: fu
  3. * @説明:
  4. */  
  5. @レストコントローラ 
  6. 公共 クラスリミッターコントローラ{
  7.  
  8. プライベート 静的 最終的なAtomicInteger ATOMIC_INTEGER_1 =新しいAtomicInteger();
  9. プライベート 静的 最終的なAtomicInteger ATOMIC_INTEGER_2 =新しいAtomicInteger();
  10. プライベート 静的 最終的なAtomicInteger ATOMIC_INTEGER_3 =新しいAtomicInteger();
  11.  
  12. /**
  13. * @著者 fu
  14. * @説明
  15. * @日付 2020/4/8 13:42
  16. */  
  17. @Limit (キー = "limitTest" 、期間 = 10 、カウント = 3 )
  18. @GetMapping ( "/limitTest1" )
  19. 公共  intテストリミッター1() {
  20.  
  21. ATOMIC_INTEGER_1.incrementAndGet()を返します
  22. }
  23.  
  24. /**
  25. * @著者 fu
  26. * @説明
  27. * @日付 2020/4/8 13:42
  28. */  
  29. @Limit (キー = "customer_limit_test" 、期間 = 10 、カウント = 3 、limitType = LimitType.CUSTOMER)
  30. @GetMapping ( "/limitTest2" )
  31. 公共  intテストリミッター2() {
  32.  
  33. ATOMIC_INTEGER_2.incrementAndGet()を返します
  34. }
  35.  
  36. /**
  37. * @著者 fu
  38. * @説明
  39. * @日付 2020/4/8 13:42
  40. */  
  41. @Limit (キー = "ip_limit_test" 、期間 = 10 、カウント = 3 、limitType = LimitType.IP)
  42. @GetMapping ( "/limitTest3" )
  43. 公共  intテストリミッター3() {
  44.  
  45. ATOMIC_INTEGER_3.incrementAndGet()を返します
  46. }
  47.  
  48. }

8. テスト

テストの「期待値」: 3 つの連続した要求が成功し、4 番目の要求が拒否されます。次に、期待通りの効果が得られるか確認してみましょう。リクエストアドレスは http://127.0.0.1:8080/limitTest1 です。 Postman を使用してテストします。 Postman URL をブラウザに直接貼り付けた場合も同様です。

4 番目のリクエストが行われたときに、アプリケーションがリクエストを直接拒否したことがわかり、Springboot + aop + lua の電流制限ソリューションが正常に構築されたことがわかります。

上記の springboot + aop + Lua 電流制限の実装は比較的単純で、電流制限とは何かを誰もが理解できるようにすることを目的としています。簡単な電流制限機能の作り方。面接の前にこれが何なのか知っておく必要があります。上記では電流制限を実装するためのいくつかのソリューションについて説明しましたが、選択は特定のビジネス シナリオに基づいて行う必要があり、単に使用するためにだけ使用すべきではありません。

<<:  なぜクラウドネイティブなのか?スピード、安定性、フルサイクル開発

>>:  Zoom、コアオンライン会議サービスにOracle Cloud Infrastructureを採用

推薦する

張小龍がユーザーエクスペリエンスについて語る: ユーザーエクスペリエンスの目標は「自然」にすること

張小龍すべてのプロダクト マネージャーとユーザー エクスペリエンス デザイナーに推奨: 言葉で説明す...

Baidu Green Dream Algorithm 2.0は、本物のソフト記事を識別する方法を教えます

みなさんこんにちは、シャオシです。2009年にSEOの仕事を始めたばかりの頃を覚えています。当時は外...

ArmorShark-openstack/1g メモリ/3 コア/30g SSD/2T トラフィック/月間 6 ナイフ/年間 48 ドル

ArmorSharkは、OpenStackクラウドプラットフォームを基盤レイヤーとしてKVMを使用し...

virtovo-$3.5/Kvm/512m メモリ/50g ハードディスク/1T トラフィック/G ポート/フロリダ

ヴィルトーヴォ、これは良いですね!なぜ良いと言うのですか?なぜなら、以前誰かがトラフィック攻撃を実行...

ドメイン名転送はPRをハイジャックできるか?PRハイジャックに関するいくつかの考察

LoveBlogEarn はブログにメッセージを残し、ドメイン名の転送によって PR を向上できるか...

忘れられた URL の最適化のヒント

多くのウェブマスターはウェブサイトの URL の最適化を見落としがちですが、ウェブサイトの最適化には...

後発のテンセントクラウドの不安

これまで外部には知られていなかったテンセントクラウドの収益データが、つい最近テンセントの財務報告書で...

ウェブページのキーワード密度に関する7つのよくある問題

検索エンジンのランキングの基礎の 1 つは、キーワードと Web ページの関連性です。機械アルゴリズ...

Appleの技術コンサルタントと議論しないでください

あなたは大手の多国籍企業を信頼しており、その企業のクラウド サービスを利用しています。ところが、ある...

ブログをさらに発展させるための5つのこと

皆さんは独立系ブログサイトをよくご存知だと思います。例えば、Moonlight Blog、Lu So...

医療業界の現状とSEOの実態の解釈

私はSEOに携わるようになってからずっと医療業界で働いています。医療業界に長く携わってきて、一番感じ...

vpsdime-年会費8ドル/メモリ128GB/ハードディスク5GB/トラフィック250GB/ポート1GB

私は、年間 8 ドルで利用できる vpsdime の 128M メモリ搭載 VPS を推奨します。世...

画像ショッピング検索Taotaosou:ゼロから1億円を儲けた負け組スタートアップ

文/Jincuodao(WeChat公式アカウント:ijincuodao)以前も似たような商品を手掛...

【WEO初投稿】WeChat検索ランキング結果分析

先週、WeChat Labは「検索」機能を発表し、大きな話題を呼んだ。この時点で、WeChatのトラ...