Лучшая практика распределенного ограничения тока springboot + aop + Lua

Java
Лучшая практика распределенного ограничения тока springboot + aop + Lua

Я разобрал кое-какую архитектуру Java и материалы интервью (микросервисы, кластеры, распределенное, промежуточное ПО и т.д.), а друзья, кому нужно, могут обратить внимание на официальный аккаунт [Внутренние дела программиста], и получить самостоятельно без всяких рутин


1. Что такое ограничение тока? Зачем ограничивать ток?

Я не знаю, ездили ли вы когда-нибудь в метро в имперской столице, то есть в такое, где приходится стоять в очереди, когда входишь на станцию ​​метро. Ответ на限流! Поскольку пропускная способность метро ограничена, скопление слишком большого количества людей одновременно приведет к переполнению платформы и перегрузке поездов, а также существуют определенные угрозы безопасности. Точно так же наша программа также ограничена в своих возможностях обработки запросов.Как только количество запросов превысит лимит обработки, она рухнет. Чтобы не было худшей аварийной ситуации, мы можем только отсрочить время входа всех на станцию.

在这里插入图片描述
Ограничение тока является важным средством обеспечения высокой готовности системы! ! !

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

Текущее ограничение приведет к тому, что пользователь будет недоступен в течение короткого периода времени (этот период времени в миллисекундах).Как правило, показатель, который мы измеряем, производительность системы измеряется в секунду.QPSилиTPS, предполагая, что порог трафика системы в секунду равен 1000, теоретически, когда 1001-й запрос поступает в течение одной секунды, этот запрос будет дросселироваться.

2. Схема ограничения тока

1. Счетчик

Java также может передавать счетчики атомарных классов внутриAtomicInteger,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. Алгоритм Token Bucket

Принцип действия алгоритма Token Bucket также относительно прост, мы можем понимать его как запись больницы на прием к врачу, и только после получения номера мы можем поставить диагноз.

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

在这里插入图片描述

4. Редис + Луа

Многие студенты не знают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метод, из кешаgetиkeyсвязанное значение, еслиnullзатем вернуть 0
  • Затем судите, больше ли значение, записанное в кэше, чем предельный размер, если он превышает, это означает, что текущий ограничен, и возвращает 0
  • Если оно не превышает, то значение кеша ключа равно +1, а время истечения устанавливается на 1 секунду позже, и возвращается значение кеша +1.

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

5. Ограничение тока на уровне шлюза

Ограничение тока часто выполняется на уровне шлюза, напримерNginx,Openresty,kong,zuul,Spring Cloud Gatewayи т. д., пока нравитсяspring cloud - gatewayОсновной принцип реализации ограничения тока шлюза основан наRedis + Lua, через встроенныйLuaСпособ ограничения скрипта.

在这里插入图片描述

3. Реализация ограничения тока Redis + Lua

Ниже мы проходим自定义注解,aop,Redis + LuaЧтобы добиться ограничения тока, шаги будут более подробными.Чтобы Xiaobai мог быстро начать работу, он будет немного более подробным, а опытные ветераны будут более осторожными.

1. Подготовка окружающей среды

springbootАдрес создания проекта: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выраженный в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 последовательных запроса могут быть успешными, а 4-й запрос отклонен. Далее, давайте посмотрим, ожидаемый ли это эффект, адрес запроса:http://127.0.0.1:8080/limitTest1,использоватьpostmanтест, есть лиpostmanТо же самое верно, если URL-адрес вставляется непосредственно в браузер.

在这里插入图片描述
Видно, что при четвертом запросе приложение напрямую отклонило запрос, указав, что наша схема ограничения тока Springboot + aop + lua успешно построена.
在这里插入图片描述

Суммировать

вышеspringboot + aop + LuaРеализация ограничения тока относительно проста, она направлена ​​на то, чтобы все знали, что такое ограничение тока? Как сделать простую функцию ограничения тока, интервью должен знать, что это такое. Хотя выше было упомянуто несколько схем реализации ограничения тока, какую из них выбрать, она должна сочетаться с конкретными бизнес-сценариями и не может использоваться ради пользы.

обращаться обеими рукамиисходный кодадрес :GitHub.com/Программист-NDS…


Небольшие преимущества:

Сотни различных технических электронных книг пересылаются друг другу, тсс~,бесплатноОтправить его друзьям. Обратите внимание на ответ на общедоступный номер【666] Самовывоз