ホームページ  >  記事  >  データベース  >  Redis の電流制限戦略について説明する記事

Redis の電流制限戦略について説明する記事

青灯夜游
青灯夜游転載
2021-12-30 10:16:431887ブラウズ

この記事では、Redis の電流制限を理解し、簡単な電流制限戦略とファネル電流制限を紹介します。

Redis の電流制限戦略について説明する記事

1. 単純な電流制限

基本原則

システムの処理能力が限界に達した場合の計画の立て方制限された外部リクエストはシステムに圧力をかけます。まず、ブルート フォース攻撃を防ぐための簡単な電流制限戦略をいくつか見てみましょう。たとえば、ある IP にアクセスしたい場合、5 秒ごとに 10 回までしかアクセスできず、制限を超えるとブロックされます。 [関連する推奨事項: Redis ビデオ チュートリアル ]

Redis の電流制限戦略について説明する記事

上記のように、スライディング ウィンドウは通常、間隔内の訪問数をカウントするために使用されます。 zset を使用して、IP の訪問数を記録します。各 IPkey を介して保存され、score は保存します現在のタイムスタンプ。 , value 実装にはタイムスタンプまたは UUID のみを使用します。

コードの実装

public class RedisLimiterTest {
    private Jedis jedis;

    public RedisLimiterTest(Jedis jedis) {
        this.jedis = jedis;
    }

    /**
     * @param ipAddress Ip地址
     * @param period    特定的时间内,单位秒
     * @param maxCount  最大允许的次数
     * @return
     */
    public boolean isIpLimit(String ipAddress, int period, int maxCount) {
        String key = String.format("ip:%s", ipAddress);
        // 毫秒时间戳
        long currentTimeMillis = System.currentTimeMillis();
        Pipeline pipe = jedis.pipelined();
        // redis事务,保证原子性
        pipe.multi();
        // 存放数据,value 和 score 都使用毫秒时间戳
        pipe.zadd(key, currentTimeMillis, "" + UUID.randomUUID());
        // 移除窗口区间所有的元素
        pipe.zremrangeByScore(key, 0, currentTimeMillis - period * 1000);
        // 获取时间窗口内的行为数量
        Response<Long> count = pipe.zcard(key);
        // 设置 zset 过期时间,避免冷用户持续占用内存,这里宽限1s
        pipe.expire(key, period + 1);
        // 提交事务
        pipe.exec();
        pipe.close();
        // 比较数量是否超标
        return count.get() > maxCount;
    }

    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        RedisLimiterTest limiter = new RedisLimiterTest(jedis);
        for (int i = 1; i <= 20; i++) {
            // 验证IP  10秒钟之内只能访问5次
            boolean isLimit = limiter.isIpLimit("222.73.55.22", 10, 5);
            System.out.println("访问第" + i + "次, 结果:" + (isLimit ? "限制访问" : "允许访问"));
        }
    }
}

実行結果

访问第1次, 结果:允许访问
访问第2次, 结果:允许访问
访问第3次, 结果:允许访问
访问第4次, 结果:允许访问
访问第5次, 结果:允许访问
访问第6次, 结果:限制访问
访问第7次, 结果:限制访问
... ...

デメリット: 非常に長い時間枠内ですべての行動記録を記録する必要がある 例えば、60 秒間で 100 万回までに回数を制限するシナリオ多くの記憶領域を消費するため、このような電流制限には適していません。

2. ファンネルの電流制限

基本原理

    ##ファンネルの容量には限界があります。もう入れません。
  • 注ぎ口から手を離すと水が下に流れ、一部が流れ出た後、続けて水を注ぐことができます。
  • 注ぎ口からの水の流量が水の充填速度よりも大きい場合、漏斗は決して満杯になりません。
  • 漏斗の流量が充填速度より小さい場合、漏斗が満杯になったら充填を一時停止し、漏斗が空になるまで待つ必要があります。

サンプルコード
public class FunnelLimiterTest {

    static class Funnel {
        int capacity; // 漏斗容量
        float leakingRate; // 漏嘴流水速率
        int leftQuota; // 漏斗剩余空间
        long leakingTs; // 上一次漏水时间

        public Funnel(int capacity, float leakingRate) {
            this.capacity = capacity;
            this.leakingRate = leakingRate;
            this.leftQuota = capacity;
            this.leakingTs = System.currentTimeMillis();
        }

        void makeSpace() {
            long nowTs = System.currentTimeMillis();
            long deltaTs = nowTs - leakingTs; // 距离上一次漏水过去了多久
            int deltaQuota = (int) (deltaTs * leakingRate); // 腾出的空间 = 时间*漏水速率
            if (deltaQuota < 0) { // 间隔时间太长,整数数字过大溢出
                this.leftQuota = capacity;
                this.leakingTs = nowTs;
                return;
            }
            if (deltaQuota < 1) { // 腾出空间太小 就等下次,最小单位是1
                return;
            }
            this.leftQuota += deltaQuota; // 漏斗剩余空间 = 漏斗剩余空间 + 腾出的空间
            this.leakingTs = nowTs;
            if (this.leftQuota > this.capacity) { // 剩余空间不得高于容量
                this.leftQuota = this.capacity;
            }
        }

        boolean watering(int quota) {
            makeSpace();
            if (this.leftQuota >= quota) { // 判断剩余空间是否足够
                this.leftQuota -= quota;
                return true;
            }
            return false;
        }
    }

    // 所有的漏斗
    private Map<String, Funnel> funnels = new HashMap<>();

    /**
     * @param capacity    漏斗容量
     * @param leakingRate 漏嘴流水速率 quota/s
     */
    public boolean isIpLimit(String ipAddress, int capacity, float leakingRate) {
        String key = String.format("ip:%s", ipAddress);
        Funnel funnel = funnels.get(key);
        if (funnel == null) {
            funnel = new Funnel(capacity, leakingRate);
            funnels.put(key, funnel);
        }
        return !funnel.watering(1); // 需要1个quota
    }

    public static void main(String[] args) throws Exception{
        FunnelLimiterTest limiter = new FunnelLimiterTest();
        for (int i = 1; i <= 50; i++) {
            // 每1s执行一次
            Thread.sleep(1000);
            // 漏斗容量是2 ,漏嘴流水速率是0.5每秒,
            boolean isLimit = limiter.isIpLimit("222.73.55.22", 2, (float)0.5/1000);
            System.out.println("访问第" + i + "次, 结果:" + (isLimit ? "限制访问" : "允许访问"));
        }
    }
}

実行結果

访问第1次, 结果:允许访问    # 第1次,容量剩余2,执行后1
访问第2次, 结果:允许访问    # 第2次,容量剩余1,执行后0
访问第3次, 结果:允许访问    # 第3次,由于过了2s, 漏斗流水剩余1个空间,所以容量剩余1,执行后0
访问第4次, 结果:限制访问    # 第4次,过了1s, 剩余空间小于1, 容量剩余0
访问第5次, 结果:允许访问    # 第5次,由于过了2s, 漏斗流水剩余1个空间,所以容量剩余1,执行后0
访问第6次, 结果:限制访问    # 以此类推...
访问第7次, 结果:允许访问
访问第8次, 结果:限制访问
访问第9次, 结果:允许访问
访问第10次, 结果:限制访问

  • Funnel##を観察しました# オブジェクトのいくつかのフィールドで、Funnel オブジェクトの内容をフィールドごとに hash 構造に保存できることがわかりました。また、hash のフィールドも埋め込む際に構造体が追加されるので、それを取り出して論理演算を行った後、新しい値を hash 構造体にバックフィルすることで動作頻度の検出が完了します。 しかし問題があります。プロセス全体の原子性を保証することはできません。
  • hash
  • 構造体から値を取得し、それをメモリ内で操作して、hash 構造体にバックフィルします。これら 3 つのプロセスはアトミックにすることはできません。つまり、適切なロック制御が必要になります。必須。ロックがかかってしまったらロック失敗ということになりますので、失敗した場合はリトライするか諦めるかを選択する必要があります。 再試行すると、パフォーマンスが低下します。諦めてしまうとユーザーエクスペリエンスに影響が出てしまいます。同時に、コードの複雑さも大幅に増加しました。これは本当に難しい選択ですが、どうすればこの問題を解決できるでしょうか?
  • Redis-Cell
  • 救世主が登場!
Redis-Cell

Redis 4.0 は、

redis-cell

という電流制限 Redis モジュールを提供します。このモジュールもファンネル アルゴリズムを使用し、原子電流制限命令を提供します。 このモジュールには cl.throttle という命令が 1 つだけあり、パラメータや戻り値が少し複雑になっていますが、次にこの命令の使い方を見てみましょう。 <pre class="brush:js;toolbar:false;">&gt; cl.throttle key:xxx 15 30 60 1</pre>

    15
  • : 15 容量 これがファンネルの容量です
  • 30 60
  • : 30 回の操作 / 60 秒 これが水漏れですrate
  • 1
  • : 1 クォータが必要 (オプションのパラメータ、デフォルト値も 1)
    > cl.throttle laoqian:reply 15 30 60
    1) (integer) 0   # 0 表示允许,1表示拒绝
    2) (integer) 15  # 漏斗容量capacity
    3) (integer) 14  # 漏斗剩余空间left_quota
    4) (integer) -1  # 如果拒绝了,需要多长时间后再试(漏斗有空间了,单位秒)
    5) (integer) 2   # 多长时间后,漏斗完全空出来(left_quota==capacity,单位秒)
  • 電流制限命令の実行時に、それが拒否された場合、破棄するか、再試行する必要があります。
cl.throttle

この命令は非常によく考えられており、再試行時間も自動的に計算されます。返したくない場合は、返された結果配列の 4 番目の値を取得し、sleep を実行するだけです。ブロックするスレッドは、非同期スケジュールされたタスクで再試行することもできます。 プログラミング関連の知識について詳しくは、

プログラミング ビデオ

をご覧ください。 !

以上がRedis の電流制限戦略について説明する記事の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はjuejin.cnで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。