LINUX.ORG.RU

Как реализовать лайки в кастомной СУБД?

 


3

3

Есть кастомная СУБД, типа на redis, которая умеет хранить key=SET. SET - это хранилка уникального набора строк. По ключу key я могу в этот SET добавлять/удалять строки с результатом «успех/фейл», получать текущее число строк в этом SET и т.п. То есть, эта СУБД условно представляет собой C++-структуру map<string, set<string>>.

Юзер uid нажал лайк на посте/комменте/фотке obj_id - тогда я втыкаю в SET с именем likes_{obj_id} новую строку obj_id:

DB.insert("likes_" + obj_id, uid);

Мы можем достать из ключа likes_{obj_id} список лайкеров данного поста, просто увидеть число лайков, взяв size() этого SET и просто не дать юзеру лайкнуть что-то дважды, SET ведь не жрёт одну строку дважды.

Например, у меня есть чатик (группа в телеграме), а при заходе в чатик надо показать сразу 50 последних сообщений. Если сам чатик хранитсяв структуре данных типа key=LIST, то достать любой подинтервал сообщений чатика очень быстро по диску/памяти, но вот нарисовать у каждого сообщения сердечко с числом лайков этого сообщения - это уже нужно перебирать ключи likes_{obj_id}, где obj_id - конкретная сообщенечка. Когда 10К юзеров поставят утюг на F5 в этом чатике, серверу станет больно.

Чтобы сервер так не болел, в каждой сообщенечке есть бинарный header, где в каком-то месте есть 4-байтный счётчик лайков, а в СУБД реализован такой транзакционный прикол:

// Найти в key2 SET, вставить в него str и если это удалось
// то найти в key1 строку, и трактуя 4 байта по offset как uint32 заинкрементить этот uint32 на 1.
increment_uint32(key1, offset, key2, str);

Аналогично есть декремент и прочие подобные штуки. То есть, лайк сообщения сразу инкрементит что-то в header этого сообщения, никак не меняя длину объекта-сообщения. Таким образом, доставая сообщение, я сразу получаю и его число лайков.

Ну, в логике «классических» СУБД, типа mysql, можно сказать, что сообщение - это строка в таблице, у строки есть колонка «likes_cnt» и эту колонку в этой строке инкрементят какой-то хранимой процедурой, которая не инкрементит это дважды для данного юзера и данного сообщения. В общем, tuple из типизированных полей он и в африке такой: ему можно менять поля, не меняя его размер и сохраняя offset до каждой колонки константным.

Теперь возникает боль: а лайков-то разных бывает много. Лайк - это только один из видов реакции на сообщение. Во всяких там Slack разных реакций существует 100500 и юзеры ещё и свои умеют добавлять. Тут уже прикол с header внутри сообщения проканает чуть менее, чем никак. Я конечно могу в колонке «likes_cnt» хранить адский бинарный JSON вида {«likes»:10, «dislikes»:80, «cry»:4, «fuck»:1}, перезаписывая полностью на какое-то изменение, но это какая-то боль и профнепригодность. Посоветуйте каких-нибудь дата-структурных идей, имея то допущение, что в нашей кастомной СУБД мы можем реализовать что взбредёт в голову, подобно вон тому «условному инкременту любых байт по любому оффсету» и даже страшнее.



Последнее исправление: hellonik (всего исправлений: 1)

конечно могу … но это какая-то боль

Это не техническая проблема, и вряд ли решать ее нужно технически.

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

Лол, ещё какая техническая.

anonymous
()

не совсем понял о чем это, но я информацию о лайках (и дизлайках) хранил бы так идентификаторы пользователей в строке с разделителями потом split в python или explode в php и получаем список (массив). И даже сравнительно быстро хоть и не оптимально.

XoFfiCEr ★★☆☆
()
Последнее исправление: XoFfiCEr (всего исправлений: 3)

Если взять за факт, что:

  1. задача именно та, что описывается
  2. всегда может быть N+1 видов эмоций
  3. примерно из за таких задач возникли реляционные СУБД
  4. в реляционные СУБД скатываться нет смысла
  5. в графовые тем более

Тогда нам сначала нужны ограничения.

  1. у нас предвидится распределенное использование этой СУБД в будущем?
  2. у нас реально может быть сколько угодно эмоций?
  3. бинарные эмоции, вроде like/dislike могут быть одновременно от одного пользователя на один пост?
  4. как долго можно задерживать приближённые к реальным показатели для показа?
  5. сколько эмоций (любых) за раз может быть для одного поста? У нас очевидно возникнут ограничения по UI
system-root ★★★★★
()

Тебе надо два индекса. На запросы кэш, если время < configured. Ну и так далее. Пост загружается. Дальше грузишь лайуи к этому посту текущего пользователя (отдельный индекс) и всех кто лайкал (отдельный индекс). Это даст 2хO(logN), если древесину применять для инднксации. При этом для лайков постов ещё и TTL. Если застарело – пихаешь в БД, и выкидываешь из памяти. Т.е. last_update нужен.

anonymous
()

Ну в качестве ключа 🔑 надо и пытаться использовать Emoji.

Меньше сущностей будет.

fornlr ★★★★★
()

Моя ставка: делать счетчик лайков append-only, не в виде JSON, а в виде структурированной подтаблицы key-value. Если находим ключ лайка — делаем банальный инкремент. Если не находим ключ — лочим-пересоздаем запись/подтаблицу, получая новую с уже имеющимся ключем в счетчике лайков. И дальше уже делаем банальный инкремент. Причем, первый этап создания нового ключа в подтаблице лайков можно вообще выделить отдельно, чтобы он выполнялся до вставки лайка в таблицу пользователь-лайки.

Второе, более радикальное решение — переделать систему на нормализованные колоночные СУБД с первичным хранением в оперативной памяти, без хранения предварительно агрегированных значаений, с агрегацией лайков заново на каждый запрос. Ну или что-то смешанное — MS SQL и Oracle умеют так делать, не знаю про опенсорсные СУБД.

byko3y ★★★★
()

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

Т.е хранить в чате не 50 сообщений а 500 разного типа.

Оно и конкарентно будет и просто. Индекс нужен вообще один по дате и потномеру чата

OxiD ★★★★
()
Последнее исправление: OxiD (всего исправлений: 2)

Можно хранить в header в 4 байтах не счетчик, а закодированный порядок количества эмоций. Например 16 эмоций: по 00 - нет такой эмоции, 01 - десятки, 10 - сотни, 11 - тысячи. Или 8 эмоций с кодировкой в три бита и более подробным шагом количества.

А подробности по количеству - хранить отдельно.

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

конечно нет, ты ж таких нагрузок и скоростей в жисть не видал.

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

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

deep-purple ★★★★★
()

Можно прикинуть распределение по количеству реакций на сообщение. Пусть 99% сообщений, например, содержат не более 10 реакций.

Тогда можно в заголовке каждого сообщения хранить структуру reaction_id1,reaction_count1,reaction_id2,reaction_count2,…,reaction_id10,reaction_count10,rest_ptr. Если реакций 10 или меньше, то в rest_ptr пишем какую-нибудь заглушку (0), а если больше - смещение/указатель на ещё одну такую же структуру.

kmeaw ★★★
()
Ответ на: комментарий от deep-purple
import random
sep = '|'
i = 1000000
s = []
while i > 0:
    r = random.uniform(0, 255)
    r = str(int(r))
    s.append(r)
    i = i - 1
str = sep.join(s)
print (str)
arr = str.split(sep)
print (arr)

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

p. s. список случайных значений можно сделать и побыстрее.

p. s. сам вывод тоже занимает время

XoFfiCEr ★★☆☆
()
Последнее исправление: XoFfiCEr (всего исправлений: 2)
Ответ на: комментарий от XoFfiCEr

В решениях с обновлением сообщения мне не понятна одна вещь - как этот апдейт попадет на клиент.

Есть вот значит чат, к нему по (веб)сокету/лонг-полл подключены клиенты. Кто-то написал сообщение, потом написали еще сотню. Верхнее сообщение лайкнули.

Что должно в (веб)сокет уйти? Всем клиентам должна уйти полная копия верхнего сообщения с лайками? Или сервер должен помнить кто его читал, а кто нет?

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

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

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

Имхо когда сервер стимит все подряд, а клиент уже фильтрует что показать, а что нет самое простое и надежное

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

OxiD ★★★★
()

Дизлаики лучше организуй будет ор выше гор одни минусы у всех вот это коммунизм по столману не то что эти ваши либерало

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

В решениях с обновлением сообщения мне не понятна одна вещь - как этот апдейт попадет на клиент.

Всем подписанным на тред/диалог/чатик рассылаются все события про этот чатик (все лайки всех постов после момента подписки, все новые месаги). Вроде это элементарно. Зашел в чатик - ты подписан, вышел - отписан.

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

Я об этом и пишу. Если всем вошедшим рассылаются все лайки то как это происходит?

Вот я лайкнул сообщение. Какой-то поток где-то на сервере в нью-йорке записал это в базу.

После этого клиенты которые поодключены к серверу в москве должны этот апдейт увидеть.

И тут они либо дергают базу в цикле либо получают обновление.

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

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

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

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

Или я что-то не понимаю, тогда расскажите как оно работаеть будет.

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

Просто есть «микросервис подписок», который только и страдает этим. В него лайкер пишет событие параллельно с записью лайка в базу.

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

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

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

У тебя в чатике есть функция ответа на сообщение? Вот лайк это то же самое, только вместо текста ответа емодзя лайка.

PolarFox ★★★★★
()

Когда-то давно читал статью о том как это сделал контач на хабре. Не помню многих деталей, но я бы сделал так:

1. Если у тебя реляционная БД, создаешь поле reactions
2. Внутри у тебя бинарная структура данных. Например:

[
  {id_реакции1,
   количество_отреагировших,
   [id_отреагировавшего1, id_отреагировавшего2, ...]
  },
  {id_реакции2,
   количество_отреагировших,
   [id_отреагировавшего1, id_отреагировавшего2, ...]
  },
  ...


Естественно, это всё в бинарном виде. Можешь применить разные техники по сокращения кол-ва байт для каждого из полей чтобы меньше занимало данных в БД.

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

Смысл сего действа состоит в том, что:

1. Очень много записей имеют либо 0 реакций либо до десяти/сотни
2. Если их так мало то нехрен залазить еще одним запросом в БД
3. Если их много, то вот тут уже можно залазить в БД. Но с учётом того что таких записей в целом лишь 1-2%, это обычно какой-то серьезный медийный контент, то оно того стоит. И вообще будет кешироваться.

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

Не совсем. Чтобы показать ответ достаточно показать ответ, лезть в историю за исходным сообщением не нужно, тот кто пишет ответ просто вместе с ним сохранит id (или даже весь текст!) исходного, и клиент при желании его по этому номеру и загрузит. А лайк (ну как я понял), это штука котоаря рисуется прямо поверх сообщения, и одно без другого нарисовать нельзя. Поэтому недостаточно отправить клиенту лайк. Надо еще ему обязательно отправить само сообщение. Ну как бы да самоый просто способ - кто-то лайкнул какое-то говно мамонта, мы идем в базу, читаем его оттуда, и рассылаем всем клиентам, хотя из 5000, только у двоих лайкнутое было загружено. Работать оно будет. Но не эффективно.

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

Помимо оптимальности (кстати что это?) тут проблема с concurrency. Как обновление происходит строки? Вычитывается вся строка целиком и перезаписывается с новым лайком? А если во время этого процесса придет еще пара лайков, что в базу запишется (когда ты уже прочитал то что было, но новую версию еще не сохранил)? Ну можно делать конечно транзакцию. Если БД это поддерживает.

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

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

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

Отправляешь клиенту сообщение о том что на такой-то айди пришёл лайк. Если сообщение у клиента в памяти, прибавляешь к счётчику. Если на экране, то проигрываешь анимацию. Если клиент про это сообщение не в курсе, то игнорит.

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

Я этот вариант описал уже раза три, и

  1. Помимо клиентов есть еще другие инстансы сервера, в других ДЦ, и даже странах, которые тоже как-то должны увидеть это обновление чтобы переслать своим клиентам

  2. Передавать апдейты просто через сокет между клиентами и серверами - мне кажется не надежным, коннекты могут мигать, нужно быть готовым что клиент (или целый инстанс с пачкой своих клиентов) пропал на пару сек и потом вернулся, но записанное сервером не прочитал (да, tcp может подтвердить прием пакета, хотя приложение его не видело). Т.е тут какой-то протокол с подтверждением приема получается и буферизацией на стороне сервера?

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

OxiD ★★★★
()
Последнее исправление: OxiD (всего исправлений: 3)

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

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

Но вообще да, ты прав :))

Я просто глубже копаю в «Отправляешь клиенту сообщение о том что на такой-то айди пришёл лайк»

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

Это ещё вопрос (т.е. я не уверен заранее в ответе), что быстрее: разложить небольшое число или разобрать джейсонину.

ugoday ★★★★★
()
Ответ на: комментарий от deep-purple

уже б и пощупал хайлоад во все поля в какой-нибудь серьезной компании

Кстати, что такое «хайлоад»? Хочу пощупать, но не знаю куда тянуться.

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

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

PolarFox ★★★★★
()

В ScyllaDB есть counters для этого, очень удобно.

nikolnik ★★★
()

Clickhouse как раз для таких целей вроде предназначен.

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

Это такая штука, когда по пользакам «хай», но у тебя всё правильно сделано и поэтому под «лоад» есть хороший запас.

deep-purple ★★★★★
()
Ответ на: комментарий от deep-purple

Ясно.

под «лоад» есть хороший запас

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

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

Я тут писал про детали реализации распределенного «хайлоада», и скорее про обновления в онлайн

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

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

удалить информацию о лайке из строки данных - да просто replace на пустую строку

добавить - просто конкатенация к тому что есть уже

проверить существует ли такие данные уже - тупо find дабы не разделять опять строку по разделителям.

о том как апдейт попадет на клиент не думал еще.

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

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

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

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

я думал об этом специальные программные флаги сделать... да все реализуемо

XoFfiCEr ★★☆☆
()
Ответ на: комментарий от cloun1902

сильное заявление! проверять я его, конечно, не стану.

deep-purple ★★★★★
()

Рисуешь кнопочку пальчик вверх либо сердечко

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

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

XoFfiCEr ★★☆☆
()
Ответ на: комментарий от XoFfiCEr
  1. Процессы запускаются на разных машинах и-или в разных ДЦ и-или разных городах

  2. Если делать лок на всю вставку в базу, то это как-то для производительности совсем не очень, и обычно это делается средствами бд, если таковая используется.

  3. При чем тут вообще запуск программы? Одна «программа» может одноврменно обслуживать тысячи сетевых соединений, даже работая в один поток. Ничто не мешает двум запросам перемешаться друг с другом (кроме соответственно какой-то системы блокировок). Ну если конечно «программа» умеет работать с клиентами только последовательно, то это какая-то очень странная «программа». В рамках одного инстанса программы ситуацию можно решить относительно дешево - ставя как-то лок на строку которую меняем. Но это сразу же не работает в распределенной среде.

OxiD ★★★★
()
Последнее исправление: OxiD (всего исправлений: 1)
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.