Как реализовать постепенную отдачу данных клиенту при формировании большого отчета
Что проверяют
Понимаете ли вы, что проблема большого отчёта двойная: не держать весь результат в памяти и не заставлять клиента ждать минуту до первого байта. Следом спросят, что будет, если ошибка случится на середине отдачи или клиент закроет вкладку.
Как отвечать
Сначала разведите два сценария. Если отчёт можно писать по мере чтения, подойдёт потоковая отдача. Если он считается минутами, лучше асинхронная задача: 202 Accepted с идентификатором, генерация в фоне, файл в объектном хранилище и ссылка на скачивание.
Для потоковой отдачи в Spring:
StreamingResponseBody: контроллер пишет прямо вOutputStreamответа, запись идёт в потоке асинхронной обработки MVC. Естественный выбор для CSV и JSON построчно;SseEmitterиResponseBodyEmitter: когда клиенту нужны события прогресса, а не файл;- WebFlux:
Flux<Row>с типомapplication/x-ndjson, если приложение уже реактивное.
Читать из базы тоже нужно потоком: JdbcTemplate.queryForStream, Stream<T> из Spring Data JPA внутри @Transactional(readOnly = true) и в try-with-resources, либо fetchSize у JDBC. Драйвер PostgreSQL учитывает fetchSize только при выключенном autocommit, иначе вытащит всё сразу. Для Excel есть SXSSFWorkbook из Apache POI: он держит в памяти окно строк, а остальное сбрасывает во временный файл.
Где спотыкаются
Создают new Thread() на каждый запрос и оставляют таймаут эмиттера по умолчанию. Потоки нужно брать из управляемого пула, а таймаут задавать явно: иначе соединение оборвёт асинхронный таймаут MVC, прокси или балансировщик.
Забывают про буферизацию по дороге. Nginx по умолчанию копит ответ проксируемого сервера, и клиент получает всё разом; помогает заголовок X-Accel-Buffering: no или proxy_buffering off в конфиге. То же относится к сжатию, если оно настроено буферизовать крупными блоками.
Не продумывают ошибку посреди отдачи. Статус 200 уже ушёл, поменять его нельзя, поэтому нужен маркер ошибки в самом потоке. А на обрыв соединения клиентом генерацию надо останавливать, а не гнать данные в пустоту и держать соединение с базой.