1.現在の制限とは何ですか?なぜ電流を制限するのですか?
帝国都市の地下鉄、つまり地下鉄の駅に行くときに列を作らなければならない種類の地下鉄をこれまでに行ったことがあるかどうかはわかりません。なぜこのように円を描く必要があるのですか。答えは電流を制限することです!地下鉄の輸送能力は限られているため、一度に多くの人が混雑すると、プラットホームが混雑し、列車が過負荷になり、特定の安全上のリスクが発生します。同様に、プログラムも同じで、リクエストを処理する機能が制限されています。リクエストの数が処理制限を超えると、クラッシュします。最悪のクラッシュ状況にならないようにするために、私たちは誰もが駅に入る時間を遅らせることしかできません。
電流制限は、システムの高可用性を確保するための重要な手段です。!!
インターネット会社の膨大なトラフィックのため、システムはオンラインになり、特にさまざまなスパイクプロモーション活動のように、ピークトラフィック評価を行います。システムが膨大なトラフィックに圧倒されないようにするために、システムトラフィックが特定のしきい値に達すると、一部のトラフィックが拒否されます。
現在の制限により、ユーザーは短期間(この時間はミリ秒のオーダーです)利用できなくなります。一般に、システムの処理能力を測定するためのインジケーターは、QPSまたはTPS /秒です。システムの1秒あたりのフローしきい値が理論上1000であると仮定します。 1001リクエストが1秒以内に届くと、リクエストは抑制されます。
2番目に、現在の制限プログラム
1.カウンター
Javaは、アトミックカウンターAtomicInteger、Semaphore semaphoreを使用して、単純な現在の制限を行うこともできます。
// 限流的个数
private int maxCount = 10;
// 指定的时间内
private long interval = 60;
// 原子类计数器
private AtomicInteger atomicInteger = new AtomicInteger(0);
// 起始时间
private long startTime = System.currentTimeMillis();
public boolean limit(int maxCount, int interval) {
atomicInteger.addAndGet(1);
if (atomicInteger.get() == 1) {
startTime = System.currentTimeMillis();
atomicInteger.addAndGet(1);
return true;
}
// 超过了间隔时间,直接重新开始计数
if (System.currentTimeMillis() - startTime > interval * 1000) {
startTime = System.currentTimeMillis();
atomicInteger.set(1);
return true;
}
// 还在间隔时间内,check有没有超过限流的个数
if (atomicInteger.get() > maxCount) {
return false;
}
return true;
}
2.
リーキーバケットアルゴリズムリーキーバケットアルゴリズムの考え方は非常に単純です。私たちは、要求としての水とシステムの処理能力の限界としてのリーキーバケットを比較します。水は最初にリーキーバケットに入り、リーキーバケット内の水は特定の速度で流出します。流量が流入流量よりも少ない場合、漏れているバケットの容量が限られているため、後続の流入水が直接オーバーフロー(要求を拒否)して流量制限を達成します。
3.
トークンバケットアルゴリズムトークンバケットアルゴリズムの原理も比較的単純で、登録された病院として診察を受けることができ、番号を取得して初めて医師を診断できます。
システムはトークンバケットを維持し、一定の速度でトークンをバケットに入れます。リクエストが届いて処理したい場合は、まずバケットからトークンを取得する必要があります。 )、バケットでトークンを使用できない場合、リクエストはサービスを拒否されます。トークンバケットアルゴリズムは、バケットの容量とトークンの発行レートを制御することで、リクエストの制限を実現します。
4. Redis + Lua
多くの学生はLuaが何であるかを知りませんか?個人的には、LuaスクリプトとMySQLデータベースには同様のストアドプロシージャがあり、それらは一連のコマンドを実行し、すべてのコマンドは正常に実行されるか、または不可分性を達成するために失敗します。Luaスクリプトは、ビジネスロジックを備えたコードの一部として理解することもできます。
Lua自体はプログラミング言語です。Redisは電流制限に対応するAPIを直接提供していませんが、Luaスクリプトの機能をサポートしています。これは、分散システムにも実装されている複雑なトークンバケットまたはリークバケットアルゴリズムの実装に使用できます。電流を制限する主な方法の1つ。
Redisトランザクションと比較すると、Luaスクリプトの利点は次のとおりです。
ネットワークのオーバーヘッドを削減する:Luaスクリプトを使用し、Redisに複数のリクエストを送信する必要がなく、1回実行します。ネットワーク転送の
アトミック操作を削減します。Redisは、同時
再利用を心配せずに、Luaスクリプト全体をコマンド、アトミックとして実行します:Luaスクリプトが実行されると、 Redisに永続的に保存され、他のクライアントは
Luaスクリプトをほぼ次のように再利用できます。
-スクリプトを呼び出すときに渡される最初のキー値を取得(現在の制限キーとして使用)
ローカルキー= KEYS [1]-
スクリプトを呼び出すときに渡される最初のパラメーター値を取得(現在のサイズを
制限)ローカル制限= tonumber(ARGV [1])
- 現在のトラフィックサイズを取得ローカルcurentLimit = tonumber(redis.call( 'get'、key)または "0")
-- 是否超出限流
if curentLimit + 1 > limit then
-- 返回(拒绝)
return 0
else
-- 没有超出 value + 1
redis.call("INCRBY", key, 1)
-- 设置过期时间
redis.call("EXPIRE", key, 2)
-- 返回(放行)
return 1
end
KEYS [1]を介して着信キーパラメータを取得します。ARGV
[1]
redis.callメソッドを介して着信制限パラメータを取得し、キャッシュからキーに関連する値を取得します。それがnullの場合は0を返し
、キャッシュ内のレコードを判断します。値が制限サイズより大きくなるかどうか。制限を超える場合は、電流が制限されていることを意味し
ます。0を超えない場合、キーのキャッシュ値は+1になり、有効期限は1秒後に設定され、キャッシュ値+1
が返されます。この記事で推奨されるソリューションについては、後で詳しく説明します。
5.ゲートウェイレイヤーの電流制限と電流
制限は、Nginx、Openresty、kong、zuul、Spring Cloud Gatewayなどのゲートウェイレイヤーで行われることが多く、Spring Cloudゲートウェイゲートウェイの電流制限の基本的な実装原理はRedis + Luaに基づいています。組み込みのLua電流制限スクリプト。
3つ目は、Redis + Luaの電流制限の実装
カスタムアノテーションaop、Redis + Luaを使用して電流制限を実現するための手順です。BaiBaiをすぐに開始できるように、経験豊富なベテランがさらに注意を払います。
1.環境の準備
Springbootプロジェクト作成アドレス:https : //start.spring.io、非常に便利で実用的なツール。
2.
pomファイルに依存関係パッケージを導入し、次の依存関係パッケージを追加します重要なものは、spring-boot-starter-data-redisとspring-boot-starter-aopです。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>21.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
3. application.properties
を設定して、application.propertiesファイルに事前に構築されているredisサービスのアドレスとポートを設定します。
spring.redis.host=127.0.0.1
spring.redis.port=6379
4、配置RedisTemplate实例
@Configuration
public class RedisLimiterHelper {
@Bean
public RedisTemplate<String, Serializable> limitRedisTemplate(LettuceConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Serializable> template = new RedisTemplate<>();
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
制限型列挙クラス
/**
* @author fu
* @description 限流类型
* @date 2020/4/8 13:47
*/
public enum LimitType {
/**
* 自定义key
*/
CUSTOMER,
/**
* 请求者IP
*/
IP;
}
5.カスタムアノテーション
@Limit アノテーションを定義します。アノテーションタイプはElementType.METHODで、メソッドに作用します。
periodは要求の制限期間で、countはその期間中に要求を解放できる回数です。limitTypeは、現在の制限のタイプを表します。要求されたIPに応じてキーをカスタマイズできます。limitType属性を渡さない場合、メソッド名がデフォルトでデフォルトのキーとして使用されます。
/**
* @author fu
* @description 自定义限流注解
* @date 2020/4/8 13:15
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Limit {
/**
* 名字
*/
String name() default "";
/**
* key
*/
String key() default "";
/**
* Key的前缀
*/
String prefix() default "";
/**
* 给定的时间范围 单位(秒)
*/
int period();
/**
* 一定时间内最多访问次数
*/
int count();
/**
* 限流的类型(用户自定义key 或者 请求ip)
*/
LimitType limitType() default LimitType.CUSTOMER;
}
6.アスペクトコードの実装
/ **
- @author fu
- @description電流制限アスペクトの実装
-
@date 2020/4/8 13:04 br /> * /
@Aspect
@Configuration
public class LimitInterceptor {プライベート静的最終ロガーロガー= LoggerFactory.getLogger(LimitInterceptor.class);
プライベート静的最終文字列UNKNOWN = "不明";
最終的なプライベートRedisTemplate <String、Serializable> limitRedisTemplate;
@Autowired
public LimitInterceptor(RedisTemplate <String、Serializable> limitRedisTemplate){
this.limitRedisTemplate = limitRedisTemplate;
}/** * @param pjp * @author fu * @description 切面 * @date 2020/4/8 13:04 */ @Around("execution(public * *(..)) && @annotation(com.xiaofu.limit.api.Limit)") public Object interceptor(ProceedingJoinPoint pjp) { MethodSignature signature = (MethodSignature) pjp.getSignature(); Method method = signature.getMethod(); Limit limitAnnotation = method.getAnnotation(Limit.class); LimitType limitType = limitAnnotation.limitType(); String name = limitAnnotation.name(); String key; int limitPeriod = limitAnnotation.period(); int limitCount = limitAnnotation.count(); /** * 根据限流类型获取不同的key ,如果不传我们会以方法名作为key */ switch (limitType) { case IP: key = getIpAddress(); break; case CUSTOMER: key = limitAnnotation.key(); break; default: key = StringUtils.upperCase(method.getName()); } ImmutableList<String> keys = ImmutableList.of(StringUtils.join(limitAnnotation.prefix(), key)); try { String luaScript = buildLuaScript(); RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class); Number count = limitRedisTemplate.execute(redisScript, keys, limitCount, limitPeriod); logger.info("Access try count is {} for name={} and key = {}", count, name, key); if (count != null && count.intValue() <= limitCount) { return pjp.proceed(); } else { throw new RuntimeException("You have been dragged into the blacklist"); } } catch (Throwable e) { if (e instanceof RuntimeException) { throw new RuntimeException(e.getLocalizedMessage()); } throw new RuntimeException("server exception"); }
}
/ **- @author fu
- @description write redis lua current limit script
- @date 2020/4/8 13:24
* /
public String buildLuaScript(){
StringBuilder lua = new StringBuilder();
lua.append( "local c");
lua.append( "\ nc = redis.call( 'get '、KEYS [1]) ");
//呼び出しは最大値を超えず、直接
lua.append(" \ nif c and tonumber(c)> tonumber(ARGV [1])then ");
lua.append( "\ nreturn C;");
lua.append( "\ Nendの");
//実行は計算から、追加
( "\ NC = redis.call( 'INCR'、KEYS [1] lua.append)");
のLUA .append( "\ nif tonumber(c)== 1 then");
//最初の呼び出しから電流を制限し、対応するキー値の有効期限を設定します
lua.append( "\ nredis.call( 'expire'、KEYS [ 1]、ARGV [2]) ");
lua.append(" \ nend ");
lua.append(" \ nreturn c; ");
return lua.toString();
}
/ **
- @author fu
- @descriptionはIDアドレスを取得します
-
@date 2020/4/8 13:24
* /
public String getIpAddress(){
HttpServletRequest request =((ServletRequestAttributes)RequestContextHolder.getRequestAttributes())。getRequest();
String ip = request.getHeader( "x-forwarded-for");
if(ip == null || ip.length()== 0 || UNKNOWN.equalsIgnoreCase(ip)){
ip = request.getHeader( "Proxy-Client-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 = request.getRemoteAddr();
}
return ip;
}
}
7、制御制层実際现
制限する必要のあるインターフェースメソッドに@Limitアノテーションを適用します。以下では、@ Limitアノテーションをメソッドに設定します。10秒以内に解放できるリクエストは10のみです。ここでは、AtomicIntegerがカウントに使用されます。/ **
- @著者:fu
- @説明:br /> * /
@RestController
public class LimiterController {
プライベート静的最終AtomicInteger ATOMIC_INTEGER_1 = new AtomicInteger();
プライベート静的最終AtomicInteger ATOMIC_INTEGER_2 = new AtomicInteger();
プライベート静的最終AtomicInteger ATOMIC_INTEGER_3 = new AtomicInteger();/ **
- @author fu
- @説明
-
@date 2020/4/8 13:42
* /
@Limit(key = "limitTest"、period = 10、count = 3)br /> @ GetMapping( "/ limitTest1")
public int testLimiter1(){return ATOMIC_INTEGER_1.incrementAndGet();
}
/ **
- @author fu
- @説明
-
@date 2020/4/8 13:42
* /
@Limit(key = "customer_limit_test"、period = 10、count = 3、limitType = LimitType.CUSTOMER)br /> @ GetMapping( "/ limitTest2")
public int testLimiter2( ){return ATOMIC_INTEGER_2.incrementAndGet();
}
/ **
- @author fu
- @説明
-
@date 2020/4/8 13:42
* /
@Limit(key = "ip_limit_test"、period = 10、count = 3、limitType = LimitType.IP)br /> @ GetMapping( "/ limitTest3")
public int testLimiter3( ){return ATOMIC_INTEGER_3.incrementAndGet();
}
}
8.テスト
テストの期待:3つの連続した要求が成功する可能性があり、4番目の要求は拒否されます。次に、効果が期待できるかどうかを確認します。リクエストアドレス:http : //127.0.0.1 : 8080 / limitTest1。テストにはpostmanを使用します。ブラウザに直接ポストされたpostman URLがあるかどうかは同じです。
4番目のリクエストが行われたときに、アプリケーションがリクエストを直接拒否したことがわかります。これは、Springboot + aop + lua電流制限ソリューションが正常に構築されたことを示しています。
上記のspringboot + aop + Luaの電流制限の実装を要約すると、電流制限とは何かを全員に知らせるように設計されていますか?簡単な電流制限機能を実行する方法、インタビューはこれが何であるかを知っている必要があります。上記は、電流制限を実現するためのいくつかの解決策ですが、どれを選択するかは特定のビジネスシナリオと組み合わせる必要がありますが、使用することはできません。