LINUX.ORG.RU

Рейтинг в реальном времени или другая приемлемая реализация


0

1

Сап!

Есть проект, в базе сейчас около 40 000 пользователей. Активных не так много, но порядок цифр примерно таков. Пользователи лежат в базе mongodb. Все крутится на nodejs. Для каждого пользователя, раз в сутки, любой другой пользователь может один раз нажать +1 или -1. По факту изменения оценки пользователь, для которого оценка была изменена, заносится в коллекцию-очередь jobs. Воркер, постоянно перезапускающийся через setTimeout раз в секунду (ограничение внешнего АПИ, т.к. +1 и -1 это на самом деле лайки в социальной сети) выбирает задачу из этой очереди, делает запрос к АПИ и получает сегодняшнее количество плюсов и минусов.

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

Далее, мне необходимо получить массив всех пользователей, упорядочить их по общему ко-ву оценок за все дни и просто перераспределить индекс, чтобы получить значение места в рейтинге. Тоесть фаектически отображать мне нужно место, а не ко-во очков. Проблема в том что получение и обновление такого количества записей занимает нешуточное время, тоесть обновлять весь рейтинг по каждому запросу изменения оценки я не могу. Пока решил обновлять реже, значение подбираю. Но возможно есть более быстрые способы? Хотелось бы всетаки уметь это делать рилтаймово.

Вобщем жду советов мудрых. Надеюсь доступно изложил.

Может есть какие-то такие расщетные базы данных, чтоб держать там только id и общий скор и в любой момент быстро получать позициию пользователя. Вообще может есть алгоритм по которому я смогу пересчитывать только изменившиеся позиции?

trashymichael
() автор топика

короче собрал мысли в кучу, теперь

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

более медленный воркер, раз в час, например, просто получает выборку таблицы, упорядоченной но ко-ву голосов, и обновляет позиции в базе

trashymichael
() автор топика
Ответ на: комментарий от Dobriy_i_Prostoy

в смысле? я и так их юзаю, у меня на сервере есть и postgresql и mysql (нужен для gitlab_ci и некоторых движков), но мне кажется обновлять для всех юзеров будет быстрее чем пытаться вытянуть рейтинг тяжелым запросом

trashymichael
() автор топика
Ответ на: комментарий от Dobriy_i_Prostoy

еще будут люди вообще без голосов, такие в рейтинге не участвуют

trashymichael
() автор топика
Последнее исправление: trashymichael (всего исправлений: 1)
Ответ на: комментарий от trashymichael

Если я правильно тебя понял, то можно использовать таблицы типа memory для хранения и пересчета недавних результатов, постепенно сохраняя в обычные.

metrokto
()
Ответ на: комментарий от metrokto

ну так вот прикинь сколько займет 40 000 последовательных апдейтов

trashymichael
() автор топика

Это же монго, там же довольно вольная структура данных. Почему бы не завести поле - массив (например) votes в таблице юзеров, на каждый голос ты добавляешь в votes что-то типа [1, (new Date)], таким образом у каждого пользователя много votes. На каждое добавление ты высчитываешь его рейтинг с помощью какой-нибудь функции, массив из ~1000 элементов js обсчитает за доли секунды (а сильно старые голоса можно сразу и удалять), а результат сохраняй в соседней поле (например) rait в виде целого числа, поле рейт индексируешь. В принципе, в votes можешь хранить только даты, а можешь еще и user_id голосовавшего (на всякий).

special-k 🤡
()
Последнее исправление: special-k (всего исправлений: 3)
Ответ на: комментарий от special-k

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

trashymichael
() автор топика
Ответ на: комментарий от trashymichael

без перебора всех невозможно
update поля rate

Всех юзеров? поясни-ка этот момент, т.к. я предложил перебрать (суммировать, вычислить) только вложенные записи.

Мм понял, а простая сортировка тебе не помогает, всмысле, если нужно отобразить на странице первых 10, то я и выберу первых 10 по рейтингу.

special-k 🤡
()
Последнее исправление: special-k (всего исправлений: 2)
Ответ на: комментарий от special-k

есть 40к юзеров и 40к мест в рейтинге со-во, с первого места (больше всего очков) до последнего, 40к-вого

trashymichael
() автор топика

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

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

Deleted
()
Ответ на: комментарий от special-k

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

users.find(user.score < score).count() таким образом получая позицию в рейтинге

trashymichael
() автор топика
Последнее исправление: trashymichael (всего исправлений: 1)
Ответ на: комментарий от special-k

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

trashymichael
() автор топика
Ответ на: комментарий от Deleted

но операция count как и выборка в mongo действительно очень очень быстрая, даже без индексов, оно же не выбирает все записи

trashymichael
() автор топика
Ответ на: комментарий от Deleted

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

trashymichael
() автор топика

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

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

x_hash
()

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

1) Пересчитать рейтинг пользователя - просто прибавить или отнять значение счетчика из мепы. 2) Если значение прибавляем, то сравниваем с рейтингом следующего пользователя, если рейтинг больше, чем у него, то меняем местами с ним. Повторить. 3) Если значение вычитаем, то двигаемся вниз.

В место массива вполне можно использовать список, сложность не ухудшится, однако массив дает меньшую константу.

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

dizza
()
Ответ на: комментарий от x_hash

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

trashymichael
() автор топика
Ответ на: комментарий от dizza

Да, при наступлении 00:00 нужно остановить обработку лайков и пересчитать опять всю структуру.

dizza
()
Ответ на: комментарий от trashymichael

Без массива тоже не сложно: нужно что бы воркер проапдейтил текущее значение рейтинга - добавил или удалил. А ночью в 00:00 пересчитывать весь рейтинг с учетом того, что чем лайк старее, тем меньше его вес.

Ну да и забирать ранк примерно как select rank from (select user_id, rank() over (order by raiting) from user_rating) r where user_id = ? если для рсубд и кешить надолго

dizza
()
Ответ на: комментарий от trashymichael

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

trashymichael
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.