задний план
В наших проектах мы часто сталкиваемся со сценариями запросов к базе данных, такими как просмотр истории заказов пользователя, просмотр списка контактов пользователя и так далее. Как правило, когда полный объем пользовательских данных не поддается контролю, мы рассматриваем пейджинг для получения данных. С одной стороны, может быть гарантирована производительность запросов к базе данных, а с другой стороны, может быть уменьшена передача клиентских данных.
Эти маленькие ямы
В запросе на подкачку базы данных также есть некоторые сценарии, требующие особого внимания, иначе можно будет легко столкнуться с ямами.
Отсутствующие данные?
Предполагая следующий сценарий, нам необходимо выполнять статистическую сверку по заказам каждого дня. Простая информация таблицы заказов показана в следующей таблице.
Атрибуты | Типы | Примечание |
---|---|---|
id | long | Автосорательный первичный ключ |
... | ... | ... |
status | int | Заказ |
update_time | long | Время обновления |
Поскольку заказ считается каждый день, мы можем легко представить себе условную фильтрацию на основе времени обновления заказа update_time. Таким образом, основной оператор запроса в основном выглядит следующим образом.
select * from order_:tableId where update_time>:starTime and update_time<=:endTime and status=:status order by id asc limit :limit
В нашем сервисе список заказов будет получен путем листания до тех пор, пока не будут получены все записи заказов. Пример кода выглядит следующим образом:
@Resource
private OrderDao orderDao;
public void countAllOrders(int tableId, long startTime, long endTime, int limit) {
long tempStartTime = startTime;
while(true) {
List<OrderRecord> orders = orderDao.getOrderByTime(tableId, tempStartTime, endTime, limit);
// 统计订单...
if(order.size()<limit){
// 当数据条数没达到指定数量时,说明没有更多数据了
break;
}
tempStartTime = orders.get(order.size()-1).getUpdateTime();
}
}
На первый взгляд, с оператором запроса mysql проблем нет, но если хорошенько подумать, то можно обнаружить, что этот запрос таит в себе скрытые опасности, а в крайних случаях данные могут быть упущены.
Предполагая, что updateTimes нескольких заказов точно равны, и они находятся только на краю страницы, в это время будет пропуск заказа. Чтобы упростить этот сценарий, предположим, что в таблице заказов есть следующие 3 записи, и мы ограничиваем количество возвращаемых записей до 1.
id | ... | update_time |
---|---|---|
1 | ... | 1552105405000 |
2 | ... | 1552105405000 |
3 | ... | 1552105405001 |
В это время при просмотре данных заказа мы обнаружим, что запись с id=2 будет пропущена при просмотре данных. Это связано с тем, что записи заказов с id=1 и id=2 имеют одинаковое время update_time. После прохождения записи заказа с id=1 для startTime было установлено значение 1552105405000, а затем после прохождения записи с update_time больше 1552105405000, в это время запись с id=2 пропускается.
Итерация с автоматически увеличивающимся идентификатором
Основной причиной отсутствия данных здесь является то, что updateTime не уникален. Мы можем рассмотреть возможность использования самоувеличивающегося первичного ключа для итерации, и соответствующий оператор запроса будет изменен следующим образом.
-- startTime和endTime仅仅作为条件范围限制,通过id来进行分页迭代
select * from order_:table_id where id>:id and update_time>:starTime and update_time<=:endTime and status=:status order by id asc limit :limit
Код также корректируется соответствующим образом, используя идентификатор в качестве основы для итерации.
@Resource
private OrderDao orderDao;
public void countAllOrders(int tableId, long startTime, long endTime, int limit) {
long id = 0;
while(true) {
List<OrderRecord> orders = orderDao.getOrderByTime(tableId, id, startTime, endTime, limit);
// 统计订单...
if(order.size()<limit){
// 当数据条数没达到指定数量时,说明没有更多数据了
break;
}
id = orders.get(order.size()-1).getId();
}
}
Итерация разбиения на страницы с помощью самоувеличивающегося идентификатора позволяет избежать пропуска данных, а другие факторы, такие как время, можно использовать в качестве условий фильтрации.
следущая страница?
Во многих сценариях нам нужно знать, есть ли следующая страница на текущей странице, и нам также может потребоваться дать четкое «завершенное» приглашение на стороне клиента. О наличии следующей страницы можно судить по тому, достигают ли выгружаемые данные заданного числа, но при таком способе обработки может потребоваться еще один запрос к базе данных (последние данные возвращаются как раз на предыдущей странице, а поскольку число в точности равно кол-во страниц, нет стр. способ судить).
Запросить еще 1 данные
На самом деле, более простой метод обработки состоит в том, чтобы запросить еще один фрагмент данных, а один дополнительный фрагмент данных можно использовать для определения наличия следующей страницы и определения начальной позиции nextOffset следующего запроса. Если количество возвращенных данных равно limit+1, это означает, что есть следующая страница, в противном случае чтение данных завершено.
обращать внимание
Чтобы запросить еще один фрагмент данных, вам нужно обратить внимание на начальное суждение итерации, которое должно включать nextOffset. Поскольку последний фрагмент данных не возвращается клиенту, он используется только для определения наличия следующей страницы и для определения начальной позиции nextOffset следующего извлечения.
резюме
1. В сценарии запроса на подкачку попробуйте использовать неповторяющиеся первичные ключи для итерации;
2. Вы можете запросить еще один фрагмент данных, чтобы определить, существует ли в данный момент следующая страница данных и начальная позиция следующей страницы;