Я могу реализовать четыре основные стратегии ограничения тока с помощью Redis|Обучение по выходным

Java
Я могу реализовать четыре основные стратегии ограничения тока с помощью Redis|Обучение по выходным

Первое предложение основной части статьи: Эта статья участвовала в учебном плане выходного дня,Нажмите на ссылку, чтобы просмотреть подробности

введение

  • В веб-разработке функциональность является краеугольным камнем.В дополнение к функциям главными приоритетами являются эксплуатация, обслуживание и защита. Потому что во время работы веб-сайта бизнес может быть ненормальным из-за внезапного трафика, а также может быть злонамеренно атакован другими лицами.
  • Поэтому наш интерфейс должен ограничивать трафик. Обычно известный как QPS, также является описанием трафика.
  • Для текущего ограничения это должен быть в основном алгоритм корзины токенов, потому что он может гарантировать большую пропускную способность. В дополнение к алгоритму ведра токенов, есть также его предшественник алгоритм дырявого ведра и простой алгоритм подсчета.
  • Давайте посмотрим на эти четыре алгоритма.

Алгоритм фиксированного временного окна

  • Алгоритм фиксированного временного окна также можно назвать простым алгоритмом подсчета. Есть много онлайн, которые выделяют алгоритм подсчета. Но автор считает, что алгоритм подсчета — это идея, а алгоритм фиксированного временного окна — одна из его реализаций.
  • Включение следующего алгоритма скользящего временного окна также является реализацией алгоритма подсчета. Потому что если счет не привязан ко времени, то теряется суть ограничения тока. превратилось в отказ

image-20210508103957917

преимущество

  • Если переполнение потока происходит в течение фиксированного периода времени, ограничение тока может быть выполнено немедленно. Каждое временное окно не влияет друг на друга
  • Стабильность системы гарантируется в единицу времени. Верхний предел пропускной способности системы в гарантированную единицу времени

недостаток

  • Как показано, его самая большая проблема — критичность. В критическом состоянии в худшем случае будет получен вдвое больший запрос трафика.
  • В дополнение к критической ситуации существует также случай, когда порог запроса быстро расходуется на ранней стадии единичного временного окна. Тогда остальное время запросить не получится. Это приведет к тому, что система будет недоступна в течение определенного периода времени из-за мгновенного потока трафика. Это неприемлемо в системе с высокой доступностью Интернета.

выполнить

  • Ну, мы уже поняли принцип введения и преимущества и недостатки. Давайте сделаем это
  • Прежде всего, когда мы реализуем этот вид подсчета, использование Redis — очень хороший выбор. Здесь мы реализуем это через Redis

controller

	@RequestMapping(value = "/start",method = RequestMethod.GET)
    public Map<String,Object> start(@RequestParam Map<String, Object> paramMap) {
        return testService.startQps(paramMap);
    }

service

@Override
public Map<String, Object> startQps(Map<String, Object> paramMap) {
    //根据前端传递的qps上线
    Integer times = 100;
    if (paramMap.containsKey("times")) {
        times = Integer.valueOf(paramMap.get("times").toString());
    }
    String redisKey = "redisQps";
    RedisAtomicInteger redisAtomicInteger = new RedisAtomicInteger(redisKey, redisTemplate.getConnectionFactory());
    int no = redisAtomicInteger.getAndIncrement();
    //设置时间固定时间窗口长度 1S
    if (no == 0) {
        redisAtomicInteger.expire(1, TimeUnit.SECONDS);
    }
    //判断是否超限  time=2 表示qps=3
    if (no > times) {
        throw new RuntimeException("qps refuse request");
    }
    //返回成功告知
    Map<String, Object> map = new HashMap<>();
    map.put("success", "success");
    return map;
}

результат теста

image-20210508135718284

  • Ставим qps=3, видим, что первые три доступа нормально после пяти одновременных входящих, а последние два терпят неудачу. Через некоторое время мы получаем доступ одновременно, и к первым трем можно получить доступ в обычном режиме. Указывает следующее временное окно

    image-20210508140028835

image-20210508141549335

Алгоритм скользящего временного окна

  • Из-за недостатка фиксированного временного окна - проблема двойного потока критического значения. Наше скользящее временное окно сгенерировано.
  • На самом деле это легко понять, то есть для фиксированного временного окна статистика временного окна меняется с исходного фиксированного интервала на более точную единицу.
  • В нашей демонстрации с фиксированным временным окном выше мы установили единицу времени 1S. Для 1S мы разбили 1S на метки времени.
  • Фиксированное временное окно — это статистическая единица, которая продолжает двигаться назад во времени. Скользящее временное окно — это то, что мы представляем себе как единицу времени, которая фиксирует время в соответствии с идеей относительности, а наша абстрактная единица времени движется сама по себе. Абстрактная единица времени меньше фактической единицы времени.
  • Читатели могут взглянуть на анимацию ниже, чтобы понять.

преимущество

  • По сути, это усовершенствование алгоритма фиксированного временного окна. Таким образом, недостаток фиксированного временного окна является его преимуществом.
  • Скользящее временное окно абстрагируется внутри, чтобы минимизировать время. Еще меньше проблем с границами. Восприятие клиентов слабее.

недостаток

  • Будь то алгоритм фиксированного временного окна или алгоритм скользящего временного окна, они оптимизируются на основе алгоритма счетчика, но их стратегия ограничения тока слишком грубая.
  • Почему грубо, нормально выпускают без лимита тока. Как только текущий лимит будет достигнут, он будет отклонен напрямую. Таким образом мы потеряем часть запросов. Это не очень удобно для продукта

выполнить

  • Скользящее временное окно предназначено для уточнения времени, которого мы достигли выше с помощью redis#setnx. Здесь мы не можем записать через него равномерно. Мы должны добавить меньшие единицы времени в сводку коллекции. Текущий лимит затем рассчитывается на основе общей суммы сбора. Структура данных zsett в Redis соответствует нашим потребностям.

  • Зачем выбирать zset, потому что zset redis имеет вес в дополнение к значению. будут отсортированы по этому весу. Если мы используем нашу единицу времени и отметку времени в качестве нашего веса, нам нужно только следовать диапазону отметок времени, когда мы получаем статистику.

  • Поскольку элементы в наборе zset уникальны, наше значение использует генератор идентификаторов, такой как uuid или алгоритм снежинки.

controller

	@RequestMapping(value = "/startList",method = RequestMethod.GET)
    public Map<String,Object> startList(@RequestParam Map<String, Object> paramMap) {
        return testService.startList(paramMap);
    }

service

		String redisKey = "qpsZset";
        Integer times = 100;
        if (paramMap.containsKey("times")) {
            times = Integer.valueOf(paramMap.get("times").toString());
        }
        long currentTimeMillis = System.currentTimeMillis();
        long interMills = inter * 1000L;
        Long count = redisTemplate.opsForZSet().count(redisKey, currentTimeMillis - interMills, currentTimeMillis);
        if (count > times) {
            throw new RuntimeException("qps refuse request");
        }
        redisTemplate.opsForZSet().add(redisKey, UUID.randomUUID().toString(), currentTimeMillis);
        Map<String, Object> map = new HashMap<>();
        map.put("success", "success");
        return map;

результат теста

image-20210508150557980

  • Тот же параллелизм, что и в окнах с фиксированным временем. Почему критическая ситуация выше? Потому что интервал единицы времени в коде больше, чем фиксированный интервал времени. Приведенная выше демонстрационная единица времени с фиксированным временным окном является наихудшим случаем 1S. Скользящее временное окно должно быть разработано с более короткими интервалами. И я установил его на 10S, и нет плохой ситуации
  • Здесь объясняются преимущества фиксированного коэффициента скольжения. Если скорректировать поменьше, критических проблем быть не должно, но в итоге критических проблем ему все равно не избежать.

алгоритм дырявого ведра

  • Хотя скользящее временное окно может в значительной степени избежать проблемы критического значения, оно все же неизбежно.
  • Кроме того, у временного алгоритма есть фатальная проблема: он не может столкнуться с внезапным большим объемом трафика, потому что сразу отбрасывает другой дополнительный трафик после достижения лимита.
  • Для этой задачи мы продолжаем оптимизировать наш текущий алгоритм ограничения. Появился алгоритм дырявого ведра

image-20210508154607271

преимущество

  • Перед лицом большей гибкости в ограничении тока это уже не грубый отказ.
  • Повышенная восприимчивость интерфейса
  • Обеспечьте стабильность приема нисходящих услуг. распределять равномерно

недостаток

  • Я не вижу минусов. Если уж быть придирчивым, то могу только сказать, что вместительность дырявого ведра - недостаток

выполнить

controller

@RequestMapping(value = "/startLoutong",method = RequestMethod.GET)
public Map<String,Object> startLoutong(@RequestParam Map<String, Object> paramMap) {
    return testService.startLoutong(paramMap);
}

service

  • В сервисе мы моделируем эффект ведра через функцию списка redis. Этот код носит лабораторный характер. В реальном использовании нам также необходимо учитывать проблемы параллелизма.
@Override
public Map<String, Object> startLoutong(Map<String, Object> paramMap) {
    String redisKey = "qpsList";
    Integer times = 100;
    if (paramMap.containsKey("times")) {
        times = Integer.valueOf(paramMap.get("times").toString());
    }
    Long size = redisTemplate.opsForList().size(redisKey);
    if (size >= times) {
        throw new RuntimeException("qps refuse request");
    }
    Long aLong = redisTemplate.opsForList().rightPush(redisKey, paramMap);
    if (aLong > times) {
        //为了防止并发场景。这里添加完成之后也要验证。  即使这样本段代码在高并发也有问题。此处演示作用
        redisTemplate.opsForList().trim(redisKey, 0, times-1);
        throw new RuntimeException("qps refuse request");
    }
    Map<String, Object> map = new HashMap<>();
    map.put("success", "success");
    return map;
}

последующее потребление

@Component
public class SchedulerTask {

    @Autowired
    RedisTemplate redisTemplate;

    private String redisKey="qpsList";

    @Scheduled(cron="*/1 * * * * ?")
    private void process(){
        //一次性消费两个
        System.out.println("正在消费。。。。。。");
        redisTemplate.opsForList().trim(redisKey, 2, -1);
    }

}

контрольная работа

  • Мы по-прежнему прокручиваем 50 одновременно для 10 посещений. Мы можем обнаружить, что относительно высокая пропускная способность может быть достигнута только в начале. После того, как вместимость следующей бочки заполнена. В случае, когда скорость дроплетов в нисходящем направлении ниже, чем скорость запросов в восходящем направлении. Доступ может быть получен только с постоянной скоростью в нисходящем направлении.
  • Его проблемы также были раскрыты. Недостаточное количество «дырявых ведер» для временных окон — это недостатки, но все же недостатки. Полностью избежать проблемы переполнения запросов невозможно.
  • Переполнение запроса само по себе является катастрофической проблемой. Ни один из алгоритмов в настоящее время не решает эту проблему. просто замедляя проблемы, которые он вызывает

Алгоритм ведра токенов

  • Маркерное ведро и метод дырявого ведра одинаковы. Просто изменил направление ствола.

  • Скорость выхода воды из дырявого ведра постоянна, если поток резко увеличится, мы можем только отказаться от входа в бассейн.

  • Но ведро для токенов - это поместить токен в ведро. Мы знаем, что токен представляет собой строку символов при нормальных обстоятельствах. Когда ведро заполнено, токен будет отклонен от входа в пул, но перед лицом высокого трафика , добавление тайм-аута является нормальным явлением, что оставляет достаточно времени для производства и потребления токенов. Таким образом, запрос не будет отклонен в максимально возможной степени.

  • Наконец, если ведро с токенами не может получить токен и отклоняется, или вода в дырявом ведре переполняется, это должно обеспечить нормальное использование большей части трафика, при этом жертвуя небольшой частью трафика.


public Map<String, Object> startLingpaitong(Map<String, Object> paramMap) {
        String redisKey = "lingpaitong";
        String token = redisTemplate.opsForList().leftPop(redisKey).toString();
        //正常情况需要验证是否合法,防止篡改
        if (StringUtils.isEmpty(token)) {
            throw new RuntimeException("令牌桶拒绝");
        }
        Map<String, Object> map = new HashMap<>();
        map.put("success", "success");
        return map;
    }


@Scheduled(cron="*/1 * * * * ?")
    private void process(){
        //一次性生产两个
        System.out.println("正在消费。。。。。。");
        for (int i = 0; i < 2; i++) {
            redisTemplate.opsForList().rightPush(redisKey, i);
        }
    }