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 ★★★★★
()

А почему бы не заюзать для этого какой-нибудь redis? Вроде бы почти идеальное решение.

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

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

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