Какие схемы нумерации страниц могут обрабатывать быстро меняющиеся списки контента? - PullRequest
38 голосов
/ 07 марта 2012

Разбивка на страницы затруднена, когда ваши рейтинги контента могут быстро меняться, и еще сложнее, когда эти оценки различаются для каждого пользователя.(Давайте рассмотрим бесконечную прокрутку как тип разбиения на страницы, где ссылки невидимы.) Есть две сложные проблемы: недавно добавленный контент вверху и переотмеченный контент.

Давайте забудем о недавно добавленном контенте и примем, что вам придется обновить страницу 1, чтобы увидеть его.Давайте также представим, что мы делаем чистые ORDER BY position;если вы заказываете что-то другое, вам, возможно, придется использовать оконные функции.Наши страницы имеют 4 ряда животных на страницу.Они начинаются:

+----+----------+-----------+
| id | position^|  animal   |
+----+----------+-----------+
|  1 |        1 | Alpacas   |
|  2 |        2 | Bats      |
|  3 |        3 | Cows      |
|  4 |        4 | Dogs      |
|  5 |        5 | Elephants |
|  6 |        6 | Foxes     |
|  7 |        7 | Giraffes  |
|  8 |        8 | Horses    |
+----+----------+-----------+

После того, как мы извлекаем страницу 1 и перед тем, как мы извлекаем страницу 2, перемещается множество элементов.БД теперь:

+----+----------+-----------+
| id | position^|  animal   |
+----+----------+-----------+
|  4 |        1 | Dogs      |
|  2 |        2 | Bats      |
|  1 |        3 | Alpacas   |
|  5 |        4 | Elephants |
|  6 |        5 | Foxes     |
|  7 |        6 | Giraffes  |
|  3 |        7 | Cows      |
|  8 |        8 | Horses    |
+----+----------+-----------+

Существует три общих подхода:

Смещение / ограничение

Это типичный наивный подход;в Rails работает will_paginate и Kaminari .Если я хочу получить страницу 2, я сделаю

SELECT * FROM animals
ORDER BY animals.position
OFFSET ((:page_num - 1) * :page_size) 
LIMIT :page_size;

, которая получает строки 5-8.Я никогда не увижу Слонов, и я увижу Коров дважды.

Последний подход к идентификации

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

SELECT * FROM animals
WHERE position > (
  SELECT position FROM animals 
  WHERE id = :last_seen_id
) 
ORDER BY position
LIMIT :page_size;

В некоторых случаях это работает лучше, чем страница / смещение.Но в нашем случае, Dogs, последний увиденный пост, увеличен до # 1.Таким образом, клиент отправляет ?last_seen_id=4, и моя страница 2 - Летучие мыши, Альпаки, Слоны и Лисы.Я не пропустил ни одного животного, но дважды видел Летучих мышей и Альпак.

Состояние на стороне сервера

HackerNews (и наш сайт, прямо сейчас) решает это с серверомбоковые продолжения;они хранят для вас весь набор результатов (или хотя бы на несколько страниц заранее?), а ссылка "Дополнительно" ссылается на это продолжение.Когда я получаю страницу 2, я запрашиваю «страницу 2 моего исходного запроса».Он использует тот же расчет смещения / лимита, но, поскольку он не соответствует исходному запросу, мне просто все равно, что сейчас все изменилось.Я вижу слонов, лис, жирафов и лошадей.Ни дупс, ни пропущенных элементов.

Недостатком является то, что мы должны хранить большое количество состояний на сервере.В HN это хранится в ОЗУ, и на самом деле эти продолжения часто заканчиваются, прежде чем вы можете нажать кнопку «Дополнительно», заставляя вас вернуться обратно на страницу 1, чтобы найти действительную ссылку.В большинстве приложений вы можете хранить это в memcached или даже в самой базе данных (используя свою собственную таблицу, или в Oracle или PostgreSQL, используя переносимые курсоры).В зависимости от вашего приложения, производительность может снизиться;в PostgreSQL, по крайней мере, вам нужно найти способ снова подключиться к нужному соединению с базой данных, что требует большого количества залипающих состояний или некоторой умной внутренней маршрутизации.

Это единственные три возможных подхода?Если нет, то есть ли какие-нибудь компьютерные концепции, которые дадут мне сок Google, чтобы прочитать об этом?Есть ли способы аппроксимировать подход продолжения без сохранения всего набора результатов?В долгосрочной перспективе существуют сложные системы потоковой передачи событий / на определенный момент времени, в которых «результат, полученный на момент, когда я получил страницу 1», всегда может быть получен.Если не считать этого ...?

Ответы [ 4 ]

6 голосов
/ 29 мая 2014

Решение 1: " хакерское решение "

Решение может состоять в том, что ваш клиент отслеживает уже просмотренный контент, например, список идентификаторов. Каждый раз, когда вам нужна другая страница, вы добавляете этот список идентификаторов в параметры вашего вызова на сервере. Затем ваш сервер может заказать контент, удалить уже просмотренный контент и применить смещение для получения нужной страницы.

Я бы не советовал, хотя и настаиваю на hacky . Я просто записываю это здесь, потому что это быстро и может соответствовать некоторым потребностям. вот плохие вещи, о которых я могу думать:

1) Требуется некоторая работа на стороне клиента, чтобы сделать это правильно (что означает «уже видел» в моем предложении выше, что, если я перейду на предыдущую страницу?)

2) Полученный заказ не отражает вашу истинную политику заказа. Контент может отображаться на странице 2, хотя политика должна была размещать его на странице 1. Это может привести к недоразумению пользователя. Давайте возьмем пример переполнения стека с его прежней политикой упорядочения, что означает, что большинство ответов сначала проголосовало. У нас может возникнуть вопрос с 6 ответами на странице 2, в то время как вопрос с 4 ответами будет на странице 1. Это может произойти, если 2 или более голосов произошло, когда пользователь все еще был на странице 1. -> это может удивить пользователя .

Решение 2 : " клиентское решение"

Это в основном решение на стороне клиента, которое вы называете «состоянием на стороне сервера». Это полезно только в том случае, если отслеживание полного заказа на стороне сервера недостаточно удобно. Работает, если список предметов не бесконечен.

  • Позвоните на сервер, чтобы получить полный (конечный) список заказов + количество товаров / страница
  • Сохранить на стороне клиента
  • Получить элементы напрямую через идентификаторы вашего контента.
6 голосов
/ 17 марта 2012

Oracle прекрасно с этим справляется. Пока курсор открыт, вы можете выбирать столько раз, сколько необходимо, и ваши результаты всегда будут отражать момент времени, в который был открыт курсор. Он использует данные из журналов отмены для виртуального отката изменений, которые были зафиксированы после открытия курсора.

Это будет работать до тех пор, пока необходимые данные отката все еще доступны. В конечном итоге журналы перерабатываются, и данные отката больше не доступны, поэтому существует некоторое ограничение в зависимости от пространства журнала, активности системы и т. Д.

К сожалению (IMO), я не знаю ни одной другой БД, которая бы работала подобным образом. Другие базы данных, с которыми я работал, используют блокировки для обеспечения согласованности чтения, что проблематично, если требуется согласованность чтения в течение более чем очень короткого промежутка времени.

4 голосов
/ 02 апреля 2012

Сейчас мы используем подход к состоянию сервера, кэшируя весь результат по первому запросу, чтобы мы всегда возвращали согласованный список. Это будет работать до тех пор, пока наш запрос уже возвращает все строки; в конце концов нам нужно будет использовать подход ближайшего соседа, и это не сработает.

Но я думаю, что есть четвертая возможность, которая очень хорошо масштабируется, если:

  1. Вам не нужна гарантия без дубликатов, только высокая вероятность
  2. У вас все в порядке с отсутствует некоторое содержимое во время прокрутки, если вы избегаете дублирования

Решение представляет собой вариант решения «последний увиденный идентификатор»: пусть клиент хранит не одну, а 5, 10 или 20 закладок - достаточно мало, чтобы вы могли эффективно хранить их. В итоге запрос выглядит так:

SELECT * FROM posts
WHERE id > :bookmark_1
AND id > :bookmark_2
...
ORDER BY id

По мере увеличения количества закладок шансы быстро уменьшаются: вы (а) начинаете в какой-то момент после всех n закладок, но (б) все равно видите дублированный контент, потому что все они были переназначены.

Если в будущем будут дыры, или лучше ответы, я с радостью откажусь от этого ответа.

1 голос
/ 11 апреля 2015

Очень поздно на вечеринку, но вот кое-что, с чем мы экспериментировали. Мы используем непрерывную загрузку, а не страницы, между которыми пользователь будет переходить назад и вперед.

Клиент создает список всех идентификаторов, которые он отображал, поэтому после первого набора это может быть: 4,7,19,2,1,72,3

Когда мы загружаем больше контента, мы делаем тот же запрос с тем же сортировкой, но добавляем к нему следующее: ГДЕ НЕ ВХОДИТ (4,7,19,2,1,72,3)

Список NOT IN может расти довольно быстро. Для нас это не проблема, поскольку наш внутренний инструмент обычно не дает тонны результатов.

Я хочу добавить еще одну идею. Возможно, к этому можно применить дополнение на стороне сервера. Когда пользователь выполняет поиск, добавьте все идентификаторы, которые он получил, в таблицу со ссылкой на свой поиск. Когда клиент хочет большего, ему нужно только указать идентификатор поиска (или использовать состояние на стороне сервера), и запрос может присоединиться к его поисковым данным.

...