Масштабируемый способ объединения одинаковых запросов в пределах определенного временного порога

У меня есть приложение, назовите его Сервис 1, которое потенциально может выполнять множество тех же запросов к другому приложению, назовите его Сервис 2. В качестве примера, x количество людей используют Сервис 1, что приводит к x запросам (которые являются точными тот же запрос) к Сервису 2. Каждый ответ кэшируется в Сервисе 1.

В настоящее время у нас есть синхронизированный метод, который проверяет, был ли сделан тот же запрос в течение определенного порогового времени. Проблема, с которой мы сталкиваемся, заключается в том, что когда сервер находится под большой нагрузкой, когда метод synchronized блокирует потоки, кубернеты не могут выполнять проверки работоспособности, поэтому кубернеты перезапускают службу. Причина, по которой мы хотим предотвратить дублирование запросов, двоякая: 1) мы не хотим забивать службу 2 и 2) если мы уже делаем запрос, мы не хотим делать его снова, просто дождитесь результата, который уже вернусь.

Какое решение является наиболее быстрым и масштабируемым, чтобы избежать дублирования запросов без блокировки и отключения сервера?

как определить, такой же запрос или другой?

Niraj Chauhan 08.08.2018 18:51

Есть ли причина, по которой вы поддерживаете этот кеш на своем собственном сервере вместо службы поставщика кеша, такой как MemcacheD или Redis?

Jay 08.08.2018 19:13

@NirajChauhan Мы знаем, что это тот же запрос, потому что запрос создаст тот же ключ и ресурс, которые попадают в кеш.

j will 08.08.2018 19:57

@Jay, мы используем ehcache в приложении. Почему? Это наследие

j will 08.08.2018 19:58

@jwill AFAIK, вы можете запустить EHCache на отдельном сервере и подключить ваше приложение к нему, используя адреса многоадресной рассылки. Таким образом, вам не нужно беспокоиться о том, что кеш в вашем приложении окажется слишком большим. Пусть этим занимается EHCache.

Jay 09.08.2018 07:28
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
5
89
1

Ответы 1

FWIW, мой опыт работы с rx-java очень ограничен, поэтому я не совсем уверен, насколько это применимо в вашем случае. Это решение, которое я использовал несколько раз со Scala, и я знаю, что в самой Java есть аналогичные конструкции, которые допускают такой же подход.

Решение, которое я использовал в прошлом, которое мне очень помогло, включает использование Futures. Это не уменьшает дублирование полностью, но удаляет дублирование для каждого запрашивающего сервера. Подход включает использование кэша TTL, в котором мы сохранили объект Future, который содержит или будет содержать результат запроса, который мы хотим выполнить дедупликацию. Он хранится под ключом, который может определять уникальность запроса, например, различные параметры, которые могут быть применимы.

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

Когда начинается запрос и мы собираемся вызвать getPage с номером страницы 2, мы проверяем кеш на наличие ключа типа «getPage: 2». Это не будет содержать ничего для первого запроса, поэтому мы вызываем getPage(2), который возвращает Future[SomeResponseObject]. Мы устанавливаем "getPage: 2" в кэше TTL для объекта Future. Когда поступает другой запрос, который может порождать повторяющийся запрос, происходит такая же проверка кеша, однако объект Future уже находится в кеше. Мы получаем это будущее и добавляем прослушиватель ответа, который будет вызываться, когда ответ доступен, или в Scala просто .map() на нем.

У этого есть несколько преимуществ. Если ваш запрос выполняется медленно или очень много дублирующихся запросов даже в короткие сроки, многие запросы к Сервису 1 обслуживаются одним ответом от Сервиса 2.

Во-вторых, как только запрос к Сервису 2 вернулся, если у вас есть окно, в котором ответ все еще действителен, ответ уже доступен немедленно, и запрос вообще не нужен.

Если ваш запрос Service 2 занимает 50 мс, а ваш ответ можно считать действительным в течение 5 секунд, все запросы, поступающие на один и тот же сервер в первые 50 мс, обслуживаются в течение 50 мс при возврате ответа, а с этого момента - для оставшихся 4950 мс. мс уже есть доступ к ответу.

Как я уже упоминал ранее, эффективность здесь зависит от количества запущенных экземпляров службы 1. Количество повторяющихся запросов в любой момент времени линейно зависит от количества запущенных серверов.

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


В качестве расширения этого вы можете потенциально использовать что-то вроде redis для кеширования ответов от Service 2, если у него долгое время отклика, и пусть ваш эквивалент getPage сначала проверит кеш redis для сериализованного ответа (и напишите истекающее значение, если оно там не было). Это позволяет еще больше сократить количество запросов к Сервису 2 за счет кэширования более глобального значения, но наличие второго уровня кэширования действительно добавляет сложности и повышает вероятность возникновения проблем.

Другие вопросы по теме