← назад к разделу

Когда читаешь про Spring, быстро натыкаешься на развилку: MVC или WebFlux? И статей много, и советы противоречат друг другу. Разберём с нуля: что такое каждый вариант, в чём реальная разница и как понять, что подходит вашему случаю.

Почему вообще возникла эта развилка

Представьте кофейню. На каждый заказ выходит один официант и стоит рядом, пока кофе не готов. Пришло 200 человек одновременно — нужно 200 официантов. Официанты стоят и ждут, хотя баристе нужна секунда.

Классический Spring MVC работает так же: на каждый HTTP-запрос уходит один поток. Поток обрабатывает запрос от начала до конца — включая ожидание базы данных или внешнего сервиса. Пока ждёт — занимает память. Стандартный пул — несколько сотен потоков. При большой нагрузке потоки заканчиваются раньше, чем успевают завершить работу.

Именно эту проблему и решали разными способами.

Три варианта сегодня

MVC на обычных потоках (классика)

Пул из нескольких сотен потоков. Каждый запрос занимает поток целиком — от приёма до ответа. Простой, предсказуемый, понятный любому разработчику. Если нагрузка невысокая — работает отлично.

MVC с виртуальными потоками (Java 21+)

Java 21 добавила виртуальные потоки (Project Loom). Это как если бы каждый официант был бумажным — занимает килобайты памяти вместо мегабайт, и их можно завести миллион. Пока виртуальный поток ждёт ответа от базы, он «отпускает» реальное ядро процессора — оно идёт обрабатывать другой запрос.

Самое важное: код выглядит точно так же, как обычный MVC. Никаких Mono, никаких Flux, никаких реактивных операторов. Просто добавляете одну строчку в конфигурацию:

spring:
  threads:
    virtual:
      enabled: true

И всё — Spring Boot 3.2+ сам переключается на виртуальные потоки. Стектрейсы читаются как обычно, отладка как обычно, ThreadLocal работает как обычно.

WebFlux (реактивный стек)

Совсем другой подход. Вместо «один запрос — один поток» работает событийный цикл: один поток обслуживает множество соединений, переключаясь между ними в момент, когда нечего делать.

Код выглядит иначе:

// Обычный MVC
@GetMapping("/order/{id}")
Order getOrder(@PathVariable Long id) {
    return orderRepository.findById(id); // блокирует поток
}

// WebFlux
@GetMapping("/order/{id}")
Mono<Order> getOrder(@PathVariable Long id) {
    return orderRepository.findById(id); // возвращает «обещание» результата
}

Mono — это «обещание одного результата», Flux — «обещание потока результатов». Вся обработка описывается цепочками операторов. Это мощно, но требует отдельного обучения.

Что изменили виртуальные потоки

До Java 21 главный аргумент за WebFlux звучал так: «MVC не масштабируется, потоки дорогие, под нагрузкой кончаются».

Виртуальные потоки закрыли именно этот аргумент. Теперь MVC с виртуальными потоками спокойно держит десятки тысяч одновременных запросов с блокирующим I/O — то, для чего раньше нужен был WebFlux.

Это не значит, что WebFlux стал бесполезным. Но область, где он оправдан, сузилась.

Когда MVC с виртуальными потоками достаточно

Это хороший выбор для подавляющего большинства сервисов:

  • Обычные REST API, которые ходят в базу данных и возвращают ответ.
  • Сервисы с JDBC, jOOQ или другими блокирующими драйверами — они работают как есть.
  • Команды, которые не имеют опыта с реактивным программированием.
  • Существующие MVC-приложения, которые нужно сделать более масштабируемыми — достаточно одного флага.

Важно понимать: виртуальные потоки не делают ваш код быстрее. Они делают ожидание дешевле. Запрос в базу данных занимает столько же времени — просто теперь пока он ждёт, поток не занимает память системы.

Когда стоит смотреть в сторону WebFlux

WebFlux сохраняет преимущество в нескольких конкретных сценариях.

Сотни тысяч одновременных соединений. API-шлюзы, прокси, edge-сервисы, где счёт соединений идёт на сотни тысяч — WebFlux эффективнее расходует ресурсы на таких масштабах.

Потоки данных с управлением скоростью. WebFlux поддерживает backpressure: если клиент читает медленно, продюсер притормаживает. Это важно для стриминга больших объёмов данных, где нельзя допустить переполнения буферов. MVC + виртуальные потоки такого не умеют.

Команда уже умеет реактивное программирование. Если команда работала с Reactor или RxJava в продакшене и хочет продолжать — это нормальный выбор.

Весь стек неблокирующий. WebFlux раскрывается только если неблокирующий драйвер есть везде: R2DBC вместо JDBC, реактивные клиенты к внешним сервисам. Один блокирующий вызов в цепочке — и событийный цикл встаёт, преимущество теряется.

Главные ошибки при выборе

WebFlux «ради производительности». Сервис на 200 запросов в секунду с обычной базой данных переписывают на реактивный стек «чтобы стало быстрее». Скорость не меняется — она упирается в базу, а не в потоки. Зато сложность утраивается.

block() внутри реактивной цепочки. Это самая частая ошибка. Блокирующий вызов на событийном цикле останавливает обработку для всех соединений, а не только для одного. Если без блокирующих вызовов не обойтись — это сигнал, что WebFlux не подходит.

Смешение двух моделей в одном сервисе. Часть контроллеров реактивные, часть — обычные. Команда поддерживает обе модели и обе плохо. Один сервис — одна модель.

Виртуальные потоки как магия. Включили флаг и ждут, что CPU-нагрузка упадёт. Виртуальные потоки помогают только там, где потоки ждут I/O. Если код считает — потоки как занимали ядра, так и занимают.

Pinning в старых библиотеках. Некоторые старые библиотеки используют synchronized вокруг I/O-операций. С виртуальными потоками это называется pinning: виртуальный поток не может «отпустить» реальный поток-носитель и держит его заблокированным. Перед включением виртуальных потоков в продакшене полезно прогнать нагрузочный тест с флагом -Djdk.tracePinnedThreads.

Коротко

  • Классический Spring MVC: один запрос — один поток, простой и понятный, но потоки дорогие.
  • MVC с виртуальными потоками (Java 21): тот же код, но потоки весят килобайты и их можно миллион — один флаг в конфигурации.
  • WebFlux: событийный цикл, Mono/Flux, другая модель программирования — мощно, но требует обучения.
  • Виртуальные потоки закрыли главный аргумент за WebFlux: «MVC не держит нагрузку».
  • Для большинства сервисов — MVC с виртуальными потоками, это дефолт.
  • WebFlux оправдан при сотнях тысяч соединений, потоках данных с backpressure и неблокирующем стеке насквозь.
  • Блокирующий вызов внутри WebFlux-цепочки — деградация всего сервиса, не одного запроса.

Что почитать дальше

  • Spring WebFlux: когда брать, Mono/Flux, R2DBC — устройство реактивного стека и его ловушки.
  • Scheduled, Async, виртуальные потоки — что виртуальные потоки меняют за пределами веб-слоя.
  • Spring MVC — стек, который выбирается по умолчанию.
  • Монолит или микросервисы — развилка уровнем выше.