Luaは電流制限を分散springboot + AOP +ベストプラクティス

Javaはアーキテクチャのいくつかの側面、インタビューデータ(マイクロサービス、クラスタリング、分散、ミドルウェアなど)をコンパイルし、プログラマ番号[何か]の公共パートナーを心配する必要はほとんどありません、ないルーチン自己を受けていません


まず、電流制限は何ですか?なぜ制限?

あなたはなぜサークルでラウンド行く長い行列を入れて、ラインアップする必要が地下鉄の駅のようなものの中に、ある地下鉄の帝国を行っていない場合は分からないのですか?答えは順序であります限流トリップ地下鉄の容量が限られているので、あまりにも多くの人々を絞るための外観は、プラットフォーム満員電車の過負荷が発生します、特定のセキュリティリスクがあります。リクエストを処理する能力が限られている。同様に、私たちのプログラムは同じで、もう一度その限度を超えて処理する要求に崩壊します。最悪の場合は表示されませんをクラッシュさせるために、それはあなたが停止する時間を遅らせることができます。

ここに画像を挿入説明
電流制限は、システムの高可用性を確保するための重要な手段です!

オンラインの巨大なインターネット企業の交通システムは、システムの流量が流れのスワップ一部を拒否し、一定のしきい値に達したときにシステムが巨大なトラフィックに圧倒されていないことを確実にするためになり、特に様々なスパイクプロモーション活動として、ピーク時のトラフィックを評価するでしょうが原因。

電流制限は、システムが利用できない(この時間はミリ秒である)、我々は一般に、システム容量の測定を短時間でユーザーにつながる毎秒であるQPSまたはTPS、システムは、毎秒1,000ことを理論的に1秒と仮定すると、流れ閾値最初の要求が入ってきたときに、その要求が制限されます1001があります。

第二に、電流制限スキーム

1、カウンタ

Javaベースの内部カウンタも原子することができAtomicIntegerSemaphoreセマフォは、単純な流量制限を行います。

// 限流的个数
    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、トークンバケットアルゴリズム

原理トークンバケットアルゴリズムは、我々は医師の診察をする病院として理解することができ、比較的簡単です、あなただけの医療相談の後に番号を取得することができます。

システムは、トークン(維持tokenバケット(INトークンに一定の速度で、)バケットtoken)要求がある場合、次に処理されることを望まになり、最初の取得する必要がトークンバケット(token)場合ノートークンバケット(token)要求はサービスを拒否されます、が望ましいです。制限要求を達成するために、ドラムの容量、発行されたトークンの割合を制御するトークンバケットアルゴリズム。

ここに画像を挿入説明

+テイク4つのRedis

多くの学生が知らないLua何ですか?理解し、LuaスクリプトとMySQLデータベースのストアドプロシージャが非常に似ている、彼らは一連のコマンドを実行し、いずれかのすべてのコマンドを実行し、すべて成功するか、失敗し、原子性を達成するためです。また、することができますLuaスクリプトは、いくつかのビジネス・ロジックのコードブロックを有するものとして理解しました。

そしてLua、それ自体が、プログラミング言語でありredis、公式には直接、適切な電流制限を提供しなかったAPIが、彼がサポートするLuaスクリプトの機能を、あなたは複雑なトークンバケットを実装するために使用することができたり、リーキーバケットアルゴリズムは制限メインを達成するための分散型システムであり、いずれかの方法。

比較するとRedis総務、Lua脚本利点:

  • 使い方:ネットワークのオーバーヘッドを削減LuaせずにスクリプトをRedis実行したら、リクエスト数回、ネットワークトラフィックを削減
  • アトミック操作:Redis全体のLuaコマンドとしてスクリプト、原子、同時実行を気にせず
  • 多重化:Luaスクリプトを実行したら、永久に保存されるRedis他のクライアント,,再利用可能に

Luaスクリプトロジックは、実質的に、次のとおりです。

-- 获取调用脚本时传入的第一个key值(用作限流的 key)
local key = KEYS[1]
-- 获取调用脚本时传入的第一个参数值(限流大小)
local limit = tonumber(ARGV[1])

-- 获取当前流量大小
local curentLimit = tonumber(redis.call('get', key) or "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]、着信取得limitパラメータを
  • redis.callこの方法は、キャッシュからgetkeyあれば、関連する値nullが0を返します。
  • 次いで超えた場合、キャッシュに記録された値は、サイズ制限よりも大きくなるか否かが判断される戻り、流れが制限されていることを示し、0
  • 超えていない場合は、1のキー値のキャッシュ、および第二時間後に期限切れに設定され、キャッシュされた値を返します1

このアプローチは、このペーパー・プログラムに推奨され、私たちは後ろに精巧な具体的な実現を行います。

図5に示すように、ゲートウェイ制限層

制限は、しばしばのような、この層ゲートウェイで行うNginxOpenrestykongzuulSpring Cloud Gateway、など、およびなどspring cloud - gatewayの原則基礎となる実装を制限するゲートウェイをベースにRedis + Lua内蔵されたことで、Luaスクリプトを制限する方法。

ここに画像を挿入説明

三、Redisの+ Luaは達成制限します

私たちの下には自定义注解aopRedis + Lua電流制限のために、手順がより見えると寛容白迅速ここ始める少し長いったらしい、経験豊富なベテランを作るために、詳細に説明されるだろう。

1、環境の準備

springboot:アドレスを作成するプロジェクトstart.spring.io、非常に便利で実用的なツールを。

ここに画像を挿入説明

2、依存関係の導入

POMファイルには、次の依存関係を追加し、それがより重要であるspring-boot-starter-data-redisspring-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ショーはperiod、リリース期間が要求の数を許可していること。limitTypeよると、電流制限の代表的な種類请求的IP自定义key場合合格していない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
 */
@Aspect
@Configuration
public class LimitInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(LimitInterceptor.class);

    private static final String UNKNOWN = "unknown";

    private final 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 编写 redis Lua 限流脚本
     * @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");
        // 执行计算器自加
        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;");
        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秒のみ解放3个要求、ここで少し視覚的でAtomicInteger計数が。

/**
 * @Author: fu
 * @Description:
 */
@RestController
public class LimiterController {

    private static final AtomicInteger ATOMIC_INTEGER_1 = new AtomicInteger();
    private static final AtomicInteger ATOMIC_INTEGER_2 = new AtomicInteger();
    private static final AtomicInteger ATOMIC_INTEGER_3 = new AtomicInteger();

    /**
     * @author fu
     * @description
     * @date 2020/4/8 13:42
     */
    @Limit(key = "limitTest", period = 10, count = 3)
    @GetMapping("/limitTest1")
    public int testLimiter1() {

        return ATOMIC_INTEGER_1.incrementAndGet();
    }

    /**
     * @author fu
     * @description
     * @date 2020/4/8 13:42
     */
    @Limit(key = "customer_limit_test", period = 10, count = 3, limitType = LimitType.CUSTOMER)
    @GetMapping("/limitTest2")
    public int testLimiter2() {

        return ATOMIC_INTEGER_2.incrementAndGet();
    }

    /**
     * @author fu
     * @description 
     * @date 2020/4/8 13:42
     */
    @Limit(key = "ip_limit_test", period = 10, count = 3, limitType = LimitType.IP)
    @GetMapping("/limitTest3")
    public int testLimiter3() {

        return ATOMIC_INTEGER_3.incrementAndGet();
    }

}
复制代码
8、テスト

テストは予想:3つの連続した要求が成功することができ、第四の要求は拒否されます。私たちが望む効果、リクエストアドレスではありませんを見て次へ:http://127.0.0.1:8080/limitTest1で、postmanテスト、まったくありませんpostmanブラウザに直接取り付けたとしてもURL。

ここに画像を挿入説明
あなたは第四のリクエストを見ることができた場合、アプリケーションは直接、私たちはSpringboot + AOP + LUAビルド成功にプログラムを制限することを示しているが、要求を拒否しました。
ここに画像を挿入説明

概要

よりspringboot + aop + Lua制限の実装は、誰もが制限されたものを知っているように設計された、比較的簡単なのですか?これは何かであることを知っているインタビューを制限する簡単な電流を作る方法。現在のために上記のプログラムのいくつかは、しかし、どのような選挙を限定するものではなく、特定のビジネスシナリオではなく、使用のためにけれども。


小型のメリット:

一部のオタクは、コースの賃金、BOO〜に着く無料の小さな仲間のために。返事はありません[社会的関心のオタク ]自己コレクション

おすすめ

転載: juejin.im/post/5e8da655f265da47f9674cbb