Рейтинг лидеров по Firebase

У меня есть проект, который должен отображать таблицу лидеров из 20 лучших, и если пользователь не находится в таблице лидеров, они появятся на 21-м месте с их текущим рейтингом.

Есть ли эффективный способ для этого?

Я использую Cloud Firestore в качестве базы данных. Я считаю, что было ошибкой выбирать его вместо MongoDB, но я нахожусь в центре проекта, поэтому я должен сделать это с Cloud Firestore.

Приложение будет использоваться пользователями 30K. Есть ли способ сделать это, не получая всех пользователей 30k?

 this.authProvider.afs.collection('profiles', ref => ref.where('status', '==', 1)
        .where('point', '>', 0)
        .orderBy('point', 'desc').limit(20))

Это код, который я сделал, чтобы получить 20 лучших, но какая будет лучшая практика для получения текущего зарегистрированного рейтинга пользователя, если они не входят в 20 лучших?

Ответ 1

Поиск произвольного ранга игрока в таблице лидеров, таким образом, что масштабы - общая сложная проблема с базами данных.

Есть несколько факторов, которые помогут вам решить проблему, например:

  • Общее число игроков
  • Оцените, что отдельные игроки добавляют баллы
  • Оцените, что новые баллы добавлены (одновременные игроки * выше)
  • Диапазон оценки: ограниченный или неограниченный
  • Распределение баллов (равномерное или их "горячие баллы" )

Упрощенный подход

Типичный упрощенный подход - подсчет всех игроков с более высоким счетом, например SELECT count(id) FROM players WHERE score > {playerScore}.

Этот метод работает в низком масштабе, но по мере роста базы ваших игроков он быстро становится медленным и ресурсоемким (как в MongoDB, так и в Cloud Firestore).

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

Периодическое обновление

Вместо того, чтобы давать им живое ранжирование, измените его на обновление только так часто, как каждый час. Например, если вы посмотрите на рейтинг стека переполнения, они обновляются только ежедневно.

Для этого подхода вы можете запланировать функцию или schedule app Engine, если требуется более 540 секунд для запуска. Функция будет выписывать список игроков, как в коллекции ladder, с новым полем rank, заполненным рангом игроков. Когда игрок просматривает лестницу сейчас, вы можете легко получить верхний X + собственный рейтинг игроков в O (X).

Еще лучше, вы могли бы дополнительно оптимизировать и явно выписывать верхний X как отдельный документ, поэтому для извлечения лестницы вам нужно всего лишь прочитать 2 документа, топ-X и игрока, экономя деньги и ускоряя их.

Этот подход действительно будет работать для любого количества игроков и любой скорости записи, так как это делается вне диапазона. Возможно, вам придется корректировать частоту, но по мере роста в зависимости от вашей готовности платить. 30 тыс. Игроков каждый час будут составлять 0,072 долл. США в час (1,73 долл. США в день), если вы не сделали оптимизацию (например, проигнорируйте все 0 очков, поскольку вы знаете, что они связаны последним).

Инвертированный индекс

В этом методе мы создадим несколько инвертированный индекс. Этот метод работает, если существует ограниченный диапазон баллов, который значительно меньше, чем количество игроков (например, 0-999 баллов против 30 тыс. Игроков). Он также может работать в неограниченном диапазоне баллов, где количество уникальных баллов по-прежнему значительно меньше, чем количество игроков.

Используя отдельный сбор, называемый "баллы", у вас есть документ для каждого индивидуального счета (несуществующий, если его нет), с полем player_count.

Когда игрок получит новый общий балл, вы сделаете 1-2 записи в коллекции scores. Одна запись - от +1 до player_count для их нового балла, и если это не первый раз -1 к их старому счету. Этот подход работает как для "вашей последней оценки - ваш текущий счет", так и для ваших "лучших баллов".

Поиск точного ранга игрока так же просто, как что-то вроде SELECT sum(player_count)+1 FROM scores WHERE score > {playerScore}.

Так как Cloud Firestore не поддерживает sum(), вы делаете выше, но суммируете на стороне клиента. +1 - это потому, что сумма - это количество игроков выше вас, поэтому добавление 1 дает вам рейтинг игрока.

Используя этот подход, вам нужно будет прочитать максимум 999 документов, в среднем 500ish, чтобы получить рейтинг игроков, хотя на практике это будет меньше, если вы удалите оценки, у которых есть нулевые игроки.

Скорость записи новых баллов важна для понимания, так как вы сможете обновлять индивидуальный счет только раз в 2 секунды * в среднем, что для отлично распределенного диапазона баллов от 0 до 999 означает 500 новых баллов/секунду **. Вы можете увеличить это, используя распределенные счетчики для каждого балла.

* Только 1 новая оценка за 2 секунды, так как каждый балл генерирует 2 записи
** Предполагая, что среднее время игры составляет 2 минуты, 500 новых очков в секунду могут поддерживать 60000 одновременных игроков без распределенных счетчиков. Если вы используете "Самый высокий балл - ваш текущий счет", это будет намного выше на практике.

Осколок N-арного дерева

Это, безусловно, самый сложный подход, но может позволить вам иметь как более быстрые, так и фиксированные позиции в реальном времени для всех игроков. Его можно рассматривать как оптимизированную для чтения версию подхода Inverted Index выше, тогда как вышеописанный подход с инвертированным индексом представляет собой оптимизированную для записи версию.

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

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

Пример троичного дерева

Заключительные мысли

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

Объединение "Инвертированного индекса" с "Периодическим обновлением" на более коротком временном интервале может дать вам доступ к O (1) для всех игроков.

Пока над всеми игроками таблица лидеров просматривается > 4 раза в течение периода "Периодического обновления", вы сэкономите деньги и получите более быструю таблицу лидеров.

По существу каждый период, скажем, 5-15 минут, вы читаете все документы из scores в порядке убывания. Используя это, держите общее количество players_count. Перепишите каждый балл в новую коллекцию под названием scores_ranking с новым полем players_above. Это новое поле содержит текущее общее количество, исключая текущие оценки player_count.

Чтобы получить ранг игрока, все, что вам нужно сделать сейчас, - это прочитать счет игрока с score_ranking → Их ранг players_above + 1.

Ответ 2

Одно решение, не упомянутое здесь, которое я собираюсь реализовать в моей онлайн-игре и которое может быть использовано в вашем случае, - это оценка ранга пользователя, если его нет в видимой таблице лидеров, потому что, честно говоря, пользователь не узнает (или все равно?), занимают ли они 22 882-е место или 22 838-е место.

Если 20-е место набрало 250 очков, а общее количество игроков - 32 000, то каждое очко ниже 250 в среднем стоит 127 мест, хотя вы можете использовать некую кривую, чтобы они двигались вверх по точке к нижней части видимого в таблице лидеров они не прыгают ровно 127 мест каждый раз - большинство прыжков в рейтинге должны быть ближе к нулю.

Вам решать, хотите ли вы определить этот оценочный рейтинг как оценку или нет, и вы можете добавить к числу случайную соль, чтобы она выглядела достоверно:

// Real rank: 22,838

// Display to user:
player rank: ~22.8k    // rounded
player rank: 22,882nd  // rounded with random salt of 44

Я буду делать последнее.

Ответ 3

Решение, не упомянутое Дэном, - это использование правил безопасности в сочетании с облачными функциями Google.

Создайте карту рекордов. Пример:

  • highScores (top20)

Тогда:

  • Предоставьте пользователям доступ для записи/чтения к highScores.
  • Дайте документ/карту highScores самый маленький результат в собственности.
  • Позвольте пользователям писать только в highScores, если его оценкa > наименьшая оценка.
  • Создайте триггер записи в облачных функциях Google, которые активируются при записи нового highScore. В этой функции удалите наименьший балл.

Это выглядит как самый простой вариант. Это также в реальном времени.