Кейс: экспорт CSV на миллион строк укладывал API-под по памяти
Эндпоинт экспорта заказов за год собирал все строки в List, потом сериализовал в CSV одним куском и отдавал целиком через ResponseEntity. На тестовых данных — сотни строк — всё работало быстро и предсказуемо.
На проде за годовой период набиралось около миллиона заказов. Каждый DTO тянул связанные сущности через отдельные запросы — классический N+1, только это была не главная проблема. Главная — весь список целиком держался в heap до момента, когда сериализация в CSV завершится и байты уйдут клиенту.
Под падал с OutOfMemoryError не сразу, а спустя пару минут — GC успевал несколько раз пройтись, прежде чем куча забивалась окончательно. В логах это выглядело как рост latency перед падением, что маскировало настоящую причину.
Что чаще всего упускают при разборе такого кейса: дело не только в размере списка. Response буферизуется целиком, если не используется потоковая отдача — StreamingResponseBody или ResponseBodyEmitter, которые пишут данные по мере генерации, не дожидаясь полного списка в памяти.
Переделали на потоковую генерацию: курсор по БД, построчная запись в OutputStream, без промежуточного списка. Память под запрос перестала зависеть от объёма экспорта.
Любой экспорт «за период» без явного лимита на объём — кандидат в OutOfMemoryError, вопрос только в том, когда наберётся достаточно данных.
Тренажёр: 600 вопросов, мок с таймером, план повторов
senior·base — что спрашивают на самом деле
В этом посте были ссылки, но мы их удалили по правилам Сетки