В вашем сервисе есть API для запроса массива объектов с фильтрацией и пагинацией. Но что делать, если эти объекты переезжают в другой источник данных? Мигрировать существующие данные в новое место и перенастроить конфигурацию вашего сервиса. А если такая миграция недоступна? Найдётся вариант и на этот случай!
Предположим, что у нас есть ничем не примечательный микросервис или монолит, который среди прочего предоставляет API для получения списка объектов. Для выдачи списка применяется пагинация, основанная на offset/limit. В этом случае запрос списка данных содержит указание порядкового номера первой запрашиваемой записи или страницы (offset) и размер одной страницы (limit). В ответе, в дополнении к запрошенной странице данных, содержится информация об общем количестве элементов списка и общем количестве страниц. При использовании реляционных БД подобного можно достичь, используя отдельный запрос количества записей, попадающих в выборку, и запрос самих записей, попадающих на требуемую страницу. Такое API обычно поддерживает фильтрацию и сортировку списка и может использоваться как для отображения в интерфейсе пользователя, так и для других целей.
Спринты идут и появляются новые требования. Планируются изменения в наших объектах. Появляется новое место хранения данных и способ их получения. Например, переход на использование другой СУБД или несовместимые изменения схемы данных в текущей БД. При этом предусматривается переходный период, когда в системе должны функционировать обе версии объектов. По факту, этот переходный период может растянуться на месяца. Встаёт вопрос, как не сломать наше API запроса списка объектов, с учётом, того, что их необходимо получать из двух источников?
Запрос всех данных из двух источников с последующей агрегацией и фильтрацией силами рассматриваемого сервиса можно отбросить сразу, в силу неэффективности. Предлагаю рассмотреть подход, разделяющий все страницы объектов на две группы по признаку их источника. При этом все объекты из нового источника данных будут расположены в первых i страницах выдачи. Все объекты из старого источника данных будут расположены на страницах, начиная с i+1. Такое разделение на две группы происходит с учётом указанных в запросе фильтров.
Алгоритм состоит из следующих шагов:
В исходном запросе присутствуют: запрашиваемая страница requested_page, желаемый размер страницы page_size и опциональные параметры фильтрации.
Получение общего количества старых объектов total_items_old, попадающих в выборку.
Вычисление количества страниц старых объектов
total_pages_old = ceil(total_items_old/page_size)
.Получение общего количества новых объектов total_items_new, попадающих в выборку.
Вычисление количества страниц новых объектов
total_pages_new = ceil(total_items_new/page_size)
.Если запрашиваемая страница requested_page попадает в диапазон
[1, total_pages_new]
, то делаем запрос данных из нового места хранения объектов.Иначе, делаем запрос в старое место хранения, не забыв преобразовать номер нужной страницы
actual_page = requested_page - total_pages_new
.В ответе в качестве общего количества страниц и объектов следует отдать
(total_pages_old + total_pages_new)
и(total_items_old + total_items_new)
соответственно.В результате запроса нужно отдать список объектов, полученный в п.6 или п.7.
Описанный подход содержит несколько компромиссов. Серьёзный минус - это невозможность использовать сортировку агрегированного списка объектов. Менее значительный - наличие страницы с номером total_pages_new, которая в большинстве случаев будет содержать меньше записей, чем запрошено. Если такое поведение неприемлемо, то можно усложнить расчёт отступов, чтобы заполнить эту страницу объектами из старого источника данных.
Спасибо за внимание, пусть ваши временные решения не становятся постоянными.
Комментарии (21)
breninsul
20.05.2023 11:59+2Мне кажется, что в таком случае логично все просто лить в эластик/сфинкс (pgsync или похожим софтом для ваших БД)?
Ну и count это очень больно для БД. Все от количества данных, конечно, зависит. Но база вынуждена отсканировать всю-всю таблицу/таблицы для получения в целом бесполезного параметра.
Оно надо? Обычно достаточно знать, что есть сл. страница, или еще 2,3,n страниц.
Didimus
20.05.2023 11:59Всегда было интересно, почему не делать пагинацию на клиенте? Неужели так сложно выгрузить 1000 товаров сразу, каждая запись меньше килобайта, а дальше браузер тянет картинки для отображаемых карточек. Это если интернет-магазин, например
BugM
20.05.2023 11:59+1А если товаров миллион?
Для типичного маркетплейса миллион позиций это реально вполне.
breninsul
20.05.2023 11:59+2эээ .. ну по тому-что фронту не нужно 1000 записей?
А по факту там будет не тысяча, а как указали, миллион.
И сразу у вас задохнётся БД, а потом сеть. А потом и браузер клиента, но это уже не бэкендерские пробоемы)
stackjava
20.05.2023 11:59Самому бэку создать 1000 обьектов на каждый запрос тоже накладно.
100 запросов в секунду считай 100 000 объектов в памяти одной ноды по одному апи.
А в сервисе еще и другие апи есть, которые тоже создают нагрузку.
Didimus
20.05.2023 11:59Только ваш кейс с пажинацией создаёт в сто раз больше запросов к БД и на сети. При этом накладные расходы составляют большую часть нагрузки
И пользователю приходится ждать, пока его запрос обрабатывается при каждом перещёлкивании страницы результатов
stackjava
20.05.2023 11:59Не факт что мне нужно увидеть 1000 товаров.
Я могу останоаиться на 10 и провалиться в него. Пойму что не подходит, вернусь обратно и запрошу снова 1000 товаров.
И так может повторяться много раз. И каждый раз буду запрашивать по 1000... Это оверхед. И по статистике человек редко проходит дальше определенного числа результатов выдачи...
Так в яндекс поиске большая часть людей не проходит дальше 1 стр. И нужно ли было им отдавать 1000 результатов? Только бд, сервис, сеть нагрузили бы.
breninsul
20.05.2023 11:59это не правда, откуда большая нагрузка?
Более того, "вам не даст без фильтра вывести все позиции".
А когда окажется, что есть фильтр, покрывающий 99% позиций?
Будем городить костыли?
Ваша схема не работоспособна. Просто попробуйте применить ее на хоть сколько-нибудь большой базе
Didimus
20.05.2023 11:59«откуда большая нагрузка?» - я регулярно покупаю в интернет магазинах. И вижу тормоза
«А когда окажется, что есть фильтр, покрывающий 99% позиций?» - не окажется, т.к. сперва вы проваливаетесь в категорию, представляющую очень ограниченную вьюшку. Тех же жестких дисков штук 100 разных
dopusteam
20.05.2023 11:59+1А в чем суть статьи? Мне кажется, решение не выглядит каким то неординарным или таким, до которого сложно дойти самому.
Плюс проблема с сортировкой - вот её решение было бы интересно
BugM
20.05.2023 11:59+3На практике все скучно делается.
Делаем вторую БД. Пишем и в новую и в старую. Читаем из старой. Потихоньку копируем. Как все скопировали переключаем чтение на новую. Отключаем запись в старую. Вычищаем хвосты.
Скучно, долго, надежно.
BugM
Это же не работает. Точнее работает, но выдает неверный результат.
По фильтру нашлось три страницы объектов из первого хранилища и четыре страницы из второй. Пользователю нужны сортированные данные, четвертая страница.
ARei0 Автор
Да, вы правы, сортировка не будет работать, в предпоследнем абзаце это было указано:
BugM
Это не минус. Это неработа программы. Пользователи очень расстраиваются когда вместо сортировки получают ерунду какую-то.
breninsul
может там ее и не было, сортировки этой
BugM
Несортированный список с пагинацией не имеет смысла. Даже в теории. БД может возвращать данные в любом порядке в этом случае.
breninsul
это правда. Но это не значит, что сортировка была)
По факту позиции PostgreSQL будет выдавать те-же до апдейта записей или вакуума. Подозреваю, в остальных так-же, иначе это странно
Ну и гарантии, что пока мы кликаем не добавилось записей, и у нас стало больше/меньше записей, соответсвенно страницы изменились все-равно нет.
Т.е. пагинация работать будет нормально, если это редко обновляемые данные.
BugM
Надеюсь что вы это только в академических целях пишите.
Любой вывод данных пользователю без сортировки должен зарубаться на ревью. Ну или на тестировании. Въедливые ручные тестировщики новых фич полезны. Автотесты такое не видят.
В задаче не написали по чему сортировать? Сортируй по имени. Нет имени? Сортируй по номеру. Номера тоже нет? Сортируй по ID объектов. Его тоже нет? Тут пора что-то править глобально. Без PK жить совсем нельзя.
ARei0 Автор
Да, сортировка по произвольному полю не требуется в моём кейсе. Сортировки объектов в рамках отдельной схемы по PK (используется UUID) достаточно, чтобы данные выводились стабильно.
Если требуется сортировка по произвольному полю, то, как уже упоминали в комментариях, без миграции данных в одну БД или других вариантов аггрегации (эластик) не обойтись.