Как реализовать пользователей сверху на Redis с помощью zsets

Например, у меня 10 000 пользователей, которые играют в игру. Каждый пользователь может выиграть или потерять очки во время игры несколько (может быть 100 или 1000) раз в час. Мне нужно показать 10 лучших пользователей по набранным очкам за последний 1 час. Топ-лист должен обновляться каждую минуту.

Поэтому мне нужно хранить и обновлять 60 (минут в час) zsets за каждый выигрыш или проигрыш. Старые zsets будут автоматически удалены по истечении срока действия.

Другой способ — хранить пользовательские очки поминутно в hset (только один hincrby за каждый выигрыш или проигрыш) и каждую минуту пересчитывать значения для zset, используя эти данные. В этом случае я должен брать 10 000 hkeys каждую минуту, удалять старые данные (старше одного часа) в каждом ключе, суммировать остальные данные и создавать новый zset для отображения.

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

Можно ли это реализовать по-другому в Redis?


person dkop    schedule 29.01.2015    source источник
comment
Просто чтобы уточнить, пытаетесь ли вы отобразить первую десятку пользователей, отсортированных по чистым баллам, набранным за последний час, или совокупный общий балл (если это применимо)?   -  person rchang    schedule 29.01.2015
comment
Да, я пытаюсь показать пользователей, отсортированных по нетто-баллам за последний час.   -  person dkop    schedule 30.01.2015


Ответы (2)


Загвоздка в том, как вы определяете «последний час». Гораздо проще сделать это с «часами», а не с «последними 60 минутами». Я объясню, как сделать часы, учитывая их простоту.

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

Когда пользователь закончил свою игру, вы делаете:

Идентификатор пользователя HINCR "таблица лидеров: номер часа"

Это даст вам очки, полученные или потерянные за этот час. Теперь, например, чтобы получить 10 лучших, вам нужно пройти маршрут HGETALL, чтобы вернуть все обратно и выполнить сортировку на стороне клиента.

Чтобы использовать аспект кэширования, вы можете затем сохранить полученные значения пользователей/точек «top X» в ключе (например, сохранить его как JSON), срок действия которого истекает каждые N минут. При этом процесс, отображающий рейтинг, будет извлекать этот ключ и отображать его, если он найден, в противном случае генерировать/сохранять/отображать результат. В качестве альтернативы или в дополнение к вышеперечисленному у вас может быть запланированное задание, которое вычисляет и сохраняет результаты для отображения каждую минуту.

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

Чтобы сделать это с скользящим окном, вы могли бы сделать что-то вроде поминутного хэша, как указано выше, вместо почасового (возможно, таблица лидеров: число минут), а на стороне расчета выяснить, на каком минутном номере вы находитесь прямо сейчас. и выполните HGETALL для предыдущих 60-минутных хэшей в конвейере. Конечно, установите для поминутных хэшей срок действия после 60, чтобы уменьшить использование.

Делая это таким образом, вы вычисляете ключи, а не запрашиваете их.

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

person The Real Bill    schedule 29.01.2015
comment
Как насчет ZINCRBY вместо HINCRBY, чтобы вам не приходилось выполнять сортировку на стороне клиента? Между прочим, я бы воспользовался вашим подходом «ключ в час», что является хорошей идеей по нескольким причинам, включая легкий доступ, истечение срока действия, ... - person antirez; 30.01.2015
comment
@antrez Сначала я начал с этого (я полагаю, я любитель отсортированных наборов), но отказался от этого, потому что я чувствовал, что сложность перевода этого маршрута в скользящее 60-метровое окно потребует от Redis много вычислений, которые я не уверен, что должен быть там, и обычно старайтесь избегать в горизонтально масштабируемой клиентской модели. Это и для обработки связей вы либо принимаете обратный порядок лексики, либо выбираете свой собственный. Делая все это на стороне клиента, вы можете выбрать, что именно. В противном случае отсортированные наборы будут работать нормально. - person The Real Bill; 31.01.2015
comment
О да, если необходимо скользящее окно, это определенно вызывает беспокойство, если дискретное решение приемлемо, подход zset, вероятно, будет лучшим. Одна вещь, которая может быть приемлемой с точки зрения многих продуктов, — это всегда показывать окно последнего часа при заполнении этого окна часа. Однако в случае с игрой, возможно, требуется более реалистичное ощущение времени. - person antirez; 31.01.2015

Хорошо, здесь я пытаюсь предоставить другое решение по сравнению с тем, которое предоставил Билл, с недостатками и преимуществами одновременно, чтобы предоставить альтернативу. С этим решением вы получаете:

  1. Top N без сортировки на стороне клиента.
  2. Роллинг окно.

Однако это немного дороже с точки зрения памяти и вычислений.

Вот как это работает:

  1. У вас есть ключ topn, который заполняется ZINCRBY данными в реальном времени. Если мы представим, что будем делать это всегда, произойдет то, что у вас будет не скользящее окно, а «верхнее N за все время», поэтому нам нужно исправить это.
  2. Вы также берете дополнительный отсортированный набор, по одному на каждую минуту последних ~ 2 часов (если у него более 1 часа, все должно быть в порядке). Таким образом, каждый раз, когда обновляется оценка пользователя, вы выполняете ZINCRBY для topn, а также для topn_<minute>.
  3. У вас есть дополнительный процесс, который делает следующее: для каждой topn_<minute> записи, которая больше не находится в пределах текущих часов, он вычитает свои баллы из topn. Это должно быть возможно с помощью одного вызова ZUNIONSTORE, используя в качестве AGGREGATE "SUM". Важно удалить его (в транзакции) одновременно, поэтому мы уверены, что удалим его один раз.

Поскольку у нас есть только SUM, здесь есть хитрость. На самом деле на шаге "2" вы должны заполнить topn_<minute> инвертированными значениями. Положительный для отрицательных результатов, отрицательный для положительных результатов.

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

person antirez    schedule 30.01.2015