Недавно я начал работать с Spring Boot в новом проекте, который имеет следующий код (упрощенный):
@RestController
@RequestMapping("/users")
public class MyController {
@Autowired
UserService service;
@GetMapping
public Mono<ResponseEntity<User>> getUser() {
// note, the non-reactive call is wrapped into Mono.just
return Mono.just(new ResponseEntity<User>(service.getUser(), ...);
}
}
// both service and dao (below) are non reactive
@Service
public class UserService {
@Autowired
UserDao dao;
User getUser() {
// some logic (everything is in memory, nothing is io-bound)
return dao.getUser();
}
}
@Repository
public class UserDao {
public User getUser() {
// call the non-reactive relational db and get the user
// it takes 99% of the time I believe
// return this user;
}
}
Я никогда не использовал реактивный подход, но я понимаю, что под капотом это приложение весенней загрузки должно запускать netty и использовать webflux. Я использовал базу данных в качестве примера операции, связанной с вводом-выводом, но иногда в коде также присутствуют вызовы удаленных служб (REST), и они не являются реактивными... Я считаю, что эти операции с БД/вызовы удаленного отдыха занимают около 99% времени выполнения, поэтому это делает приложение хорошим кандидатом для реактивной реализации.
Однако меня смущает текущая ситуация с кодом:
Мой вопрос: допустимо ли такое использование смеси нереактивных и реактивных подходов или это неправильно?
Насколько я понимаю, если я хочу использовать реактивный подход веб-потока, весь мой код должен быть реактивным вплоть до вызова базы данных/удаленного отдыха (включая их, конечно), в противном случае я эффективно сохраню потоки цикла событий. netty занята, и я не могу этого сделать... Так что в двух словах мне это кажется неправильным, но я не уверен, что на самом деле происходит в этом коде. Я правильно понимаю или я что-то упускаю?
Еще один вопрос, ты не можешь полностью реагировать? Есть ли у вас какие-либо ограничения? Если да, то вы можете использовать реактивный драйвер и просто обернуть нереактивный код в парадигму реактивного стиля, запланировать его для Schedulers.boundedElastic()
и вернуть результат метода Mono.subscribe()
. Обратите внимание, что я бы рекомендовал полностью использовать реактивность, это должно быть транзитивное решение или для очень конкретных случаев использования, о которых я не знаю.
Спасибо, Винсент, говоря об остальных вызовах, я имел в виду, что иногда поток вызывает другие службы приложения, а иногда запрашивает базу данных. Под нереактивностью я имел в виду, что оба блокируют вызовы, поток, выполняющий вызов, ждет, пока не получит ответ. В общем, я только что присоединился к проекту, и код в том виде, в каком он написан сегодня, мне кажется неправильным. Я не знаю, смогу ли я стать реактивным, потому что фактическая база данных, которую мы используем, вероятно, не поддерживает реактивную парадигму, поэтому, насколько я понимаю, для нее не существует реактивного драйвера.
По сути, вы можете обернуть свои нереактивные вызовы Mono.fromCallable(() -> nonReactive())
следующей документацией реактора. Таким образом, вы по-прежнему можете использовать реактивную парадигму в каждой из ваших точек входа, сохраняя при этом нереактивный драйвер для вашей БД. Я также рекомендовал бы вам принять шаблон стратегии при выпуске реактивных драйверов (если вообще когда-либо) для вашей конкретной базы данных, чтобы вы могли легко переключаться с реактивного на нереактивный режим.
Отстой, эта ссылка не приведет вас к правильной документации.
These DB operations/remote rest calls I believe take like 99% of the execution time, so this makes the application to be a good candidate for reactive implementation.
У вас все равно будет то же время выполнения. Реактивные фреймворки не предназначены для сокращения времени выполнения. https://projectreactor.io/docs/core/release/reference/#getting-started-introducing-reactor.
Reactor — это полностью неблокирующая основа реактивного программирования для JVM с эффективным управлением спросом (в форме управления «противодавлением»).
Это ваш вариант использования?
The controller is reactive
Действительно? Вы сделали его реактивным с помощью Mono::just
, но используете нереактивные аннотации.
The service / DAO / Remote Calls are not
Почему бы их тоже не поменять? Если вам нужно их обернуть, проверьте документацию. С.1. Как завершить синхронный блокирующий вызов? .
In my understanding if I want to go with reactive web-flux approach, all my code must be reactive all the way down to the database/remote rest call (including them of course), otherwise I'll effectively keep the event-loop threads of netty busy and this is something I can't do... So it seems wrong to me in a nutshell, But I'm not sure what happens actually in this code. Is my understanding correct or I'm missing something?
Да. Всю дорогу вниз. Цель — создать flow
и передать его клиенту. Когда клиент запрашивает результат, flow
выполняется в рамках реактивной структуры.
Когда платформа встречает блокирующий вызов, например DB или REST, она выполняет вызов и переназначает поток для выполнения другого параллельного запроса. Когда блокирующий вызов возвращает результат, поток перенаправляется обратно на исходный запрос. С помощью Apache Tomcat, использующего системные потоки, вы можете обрабатывать от 1 до 2 тысяч одновременных запросов с очень точным расчетом времени. Из коробки 384. С WebFlux от 30 до 50 тысяч, если правильно делать. У вас так много пользователей, что они будут стабильно выполнять 30 тысяч одновременных запросов в секунду?
Он также выполнен в стиле functional
парадигмы программирования. Редко кто-то пишет хороший код, используя эту парадигму. Я только что исправил блок кода, который занимал 20 строк, просто чтобы добавить на карту кучу листовых узлов. Заменил на пару forEach
петель.
Развлекайтесь в этой кроличьей норе. Людям нравятся новые вещи, потому что они экзотичные и увлекательные. Подождите, пока вам не понадобится поддерживать чей-то код, который на самом деле не понимает структуру и ее цель. Functional
просто уродлив и WebFlux
очень запутан.
PS> Для иллюстрации: Mono::just
сначала выполнит код, а затем построит поток с результатом. Он просто помещает результат в Mono
, так что на самом деле вы не сделали ничего реактивного. Поэтому, если вы поместите вызов REST в Mono.just, он вызовет конечную точку перед возвратом потока клиенту. Основная реактивная ловушка для новичков.
Спасибо К.Николас. Назвав контроллер реактивным, я имел в виду, что использование абстракции Mono указало мне на то, что под капотом он работает netty и использует webflux.... Я не думаю, что могу просто использовать реактивный подход, потому что фактическая база данных, которую мы используем, не поддерживает реактивный способ работы (нет реактивного драйвера). Поэтому я хотел убедиться, что мое понимание правильное, а такой способ проектирования кода неправильный. Лично я думаю то же самое, что и вы, но у меня не было большого опыта работы с реактивным кодом...
Да, не беспокойся. Мне потребовалось некоторое время, чтобы разобраться, что происходит с реактивом. Тем временем Oracle завершила и опубликовала виртуальные потоки, поэтому, если вы действительно хотите обрабатывать тысячи одновременных запросов, для этого специально созданы API Async Servlet и виртуальные потоки. Если вы все равно его используете, лучше понимать его как двухэтапный процесс. Первый — построить поток для клиента, второй — клиент его выполняет. Ловушки заключаются в следующем: 1) выполнение кода во время сборки, как в Mono.just 2) слишком усердная попытка сделать все функциональным, когда следует использовать простую старую Java внутри map
.
@MarkBramnik Если у вас нет драйвера реактивной базы данных, не тратьте время на Webflux. Просто обновитесь до Java 21 и вместо этого используйте виртуальные потоки. Примечание: вам нужен не только ХОРОШИЙ драйвер базы данных, совместимый с Spring, вам также могут понадобиться другие компоненты для его поддержки, такие как сам Spring, Hibernate и т. д.
@MarkBramnik Т.е. если бы вы решили использовать SQL, а затем захотели использовать Webflux, вы бы возненавидели жизнь. Существует драйвер Reactive SQL, но он чрезвычайно примитивен, Hibernate его тоже не поддерживает, поэтому, если вы хотите сделать что-то большее, чем плоские таблицы 1:1, вам придется ВСЕ это перевернуть вручную. Дочерние таблицы, связи и т. д. По сути, единственные БД, с которыми я бы рекомендовал использовать Webflux, — это Mongo или Elastic.
@SledgeHammer - Честно говоря, я думаю точно так же :))) Я постараюсь изо всех сил выяснить, как получилось, что проект начал использовать реактивный подход. Я знаю о проекте и, конечно, предпочел бы его использовать, но, поскольку я являюсь частью большой организации, мне потребуется время, чтобы принять Java 21... Спасибо за ваше мнение
@MarkBramnik, «это займет время», к сожалению, не вариант для Webflux. Mono.Callable
— это обходной путь для некоторых блокирующих вызовов ОС здесь и там. Он не предназначен для создания целого приложения. Если у вас нет встроенной реактивной поддержки сверху вниз для ВСЕХ ваших зависимостей, выполняющих ввод-вывод, вам не следует использовать Webflux. Одна из первых вещей, которую вам следует сделать при выборе Webflux, — это убедиться, что у вас есть реактивная поддержка всех операций ввода-вывода.
@MarkBramnik В качестве примечания: различные команды Spring намекнули, что они не собираются вкладывать значительные усилия в Webflux. Реляционный драйвер, как я уже упоминал, не может делать ничего, кроме 1:1, и руководитель фактически сказал, что он не собирается поддерживать 1:M и M:M в ближайшее время. Hibernate Reactive также не поддерживает Spring, только Vert.x. При этом, если бы я запускал новый сервис Mongo/Elastic/HTTP, я бы все равно выбрал Webflux. В противном случае ВЦ.
Да, спасибо — я вижу, что традиционные реляционные базы данных все еще не готовы к реагированию :) Придется разбираться, почему были приняты такие решения. Спасибо за разъяснения о будущем webflux,
Вы правы, обычно вам нужно использовать полную реактивность до тех пор, пока вы не выполните ввод-вывод. Но базовые службы не обязательно должны быть реактивными. Что меня щекочет в вашем вопросе, так это следующее:
sometimes in the code there are also calls to remote services (REST), and also they're not reactive
. При этом совершенно не имеет значения, какая парадигма использует базовые сервисы. Что наиболее важно, так это то, что все нечисто действия ЦП являются «действительными» с точки зрения реактивности: либо с использованием неблокирующих драйверов, либо, как сказал @K.Nicholas, путем переноса в «реактивный» совместимый блок кода.