Я разобрал кое-какую архитектуру 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
Реализация ограничения тока относительно проста, она направлена на то, чтобы все знали, что такое ограничение тока? Как сделать простую функцию ограничения тока, интервью должен знать, что это такое. Хотя выше было упомянуто несколько схем реализации ограничения тока, какую из них выбрать, она должна сочетаться с конкретными бизнес-сценариями и не может использоваться ради пользы.
обращаться обеими рукамиисходный кодадрес :GitHub.com/Программист-NDS…
Небольшие преимущества:
Сотни различных технических электронных книг пересылаются друг другу, тсс~,бесплатноОтправить его друзьям. Обратите внимание на ответ на общедоступный номер【666] Самовывоз