Пишу сервис для обращения к vk api. У них стоит ограничение на запросы - не более 3 раз в секунду на методы c user token и 20 раз в секунду на методы с group token. Мое приложение постоянно делает запросы и поэтому я решил сделать механизм ограничения. Вот мое решение с использованием spring aop:
@Slf4j
@Aspect
@Component
public class BarrierMechanics {
private final Map<Integer, BarrierExecutor> executors = new HashMap<>();
private final ExecutorService pool;
private final Environment env;
public BarrierMechanics(
@Qualifier("barrierPool") Executor pool,
Environment env) {
this.pool = (ExecutorService) pool;
this.env = env;
}
@Around("@annotation(Frequency)")
public Object proceedAsync(
ProceedingJoinPoint pJP,
Frequency Frequency) throws Throwable {
int bar = Integer.parseInt(env
.resolvePlaceholders(Frequency.expression()));
if (executors.containsKey(bar)) {
return executors.get(bar).proceedAsync(pJP);
} else {
val barExec = new BarrierExecutor(bar);
executors.put(bar, barExec);
return barExec.proceedAsync(pJP);
}
}
private class BarrierExecutor {
private final Lock locker = new ReentrantLock();
private final int barrier;
private int counter = 0;
private long first = 0;
private long last = 0;
private BarrierExecutor(int barrier) {
if (barrier < 2) throw new IllegalArgumentException("Barrier cannot be less 2");
this.barrier = barrier - 1;
}
Object proceedAsync(
ProceedingJoinPoint pJP) throws Throwable {
val future = pool.submit(() -> {
try {
locker.lock();
if (counter == barrier) {
last = currentTimeMillis();
long res = last - first;
Thread.sleep(res > 1000 ? 0: 1000 - res);
counter = 0;
} else if (counter == 0) {
first = currentTimeMillis();
counter++;
} else counter++;
locker.unlock();
return pJP.proceed();
} catch (Throwable throwable) {
if (throwable instanceof VkServerException) {
if (((VkServerException) throwable).getCode() == 6) {
locker.lock();
Thread.sleep(300);
locker.unlock();
}
}
return throwable;
}
});
val result = FutureUtils.anyResult(future);
if (result instanceof Throwable) FutureUtils.anyThrow((Throwable) result);
return result;
}
}
}
Запросы я делаю с помощью resttemplate. Исключения преобразую в VkServerException. Ошибка с кодом 6 - слишком много запросов в секунду. Вот bean пула:
@Bean
public Executor barrierPool() {
return Executors.newFixedThreadPool(pS, new CustomizableThreadFactory(thNm));
}
В итоге, это решение работает почти всегда корректно и вроде бы быстро. Но все же иногда возникает ошибка too many requests. У меня есть пара вопросов по этому поводу:
1) Корректно ли мое решение с точки зрения потокобезопасности?
2) Как мне правильно обработать исключение too many request, чтобы следующие вызовы отрабатывали без него?
1) Mодификация Map<Integer, BarrierExecutor> executors
не потокобезопасная. Можно или ConcurrentHashMap
и использовать putIfAbsent
, или Google Guava Cache - у него можно сконфигурировать инициализатор для отсутствующего ключа.
2) В барьере, для случая counter == barrier
ты обнуляешь счётчик, но не меняешь first
. При этом запрос к АПИ ты делаешь. Получается этот запрос не будет учтен в интервале [first, last].
Я бы сделал так
if (counter == barrier) {
last = currentTimeMillis();
long res = last - first;
Thread.sleep(res > 1000 ? 0: 1000 - res);
counter = 0;
}
if (counter == 0) {
first = currentTimeMillis();
}
counter++;
3) Из их доков я не понял, как они делают подсчёт на своей стороне. Если скользящее окно, то твой код это не учитывает. Возможно, стоит сделать на своей стороне скользящее окно, оно будет работать и в случае, если на их стороне просто всё разбито на секундные интервалы.
Виртуальный выделенный сервер (VDS) становится отличным выбором
При использовании MySQL получаю такую ошибку:
Пытаюсь получить товары у которых нету фото в базе и есть фото у аналогичных товаровВ первом запросе получаем model по которому идет проверка...
Первое правило прячет список-подменюВторое - показывает подменю, если на верхний пункт меню (родитель), в котором находится подменю, наведут...