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)
Ответ на: комментарий от OxiD

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

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

Я не ТС ))) Без понятия что он выбрал, кажется он тут вообще не появлялся.

Ну я таком случае рассказал чтобы выбрал. Но тут много чего неизвестно. как минимум непонятно с какими объемами данных будет работа. И нужно ли уметь находить все лайки сообщения за O(1).

OxiD ★★★★
()
21 июля 2022 г.
Ответ на: комментарий от OxiD

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

Как у меня реализовано в https://nanochat.ru : клиент имеет 1 websocket-коннект к бинарнику, в оперативной памяти которого для каждого чатика представлен набор клиентов, которые в нём сейчас сидят, т.е. «подписаны». Ну то есть, абстрактно, один чатик - это очередь уведомлений, на которую подписываются все, кто в этот чатик сейчас зашёл. Все события с этим чатиком валятся в эту очередь, а эта очередь рассылает всё попавшее в неё всем, кто на неё подписан. Если какой-то месаге поставили лайк, то событие вида "like:chatname:12345:1:12:0" рассылается всем, кто в этом чатике сидит. chatname - чатик, 12345 - id мессаги в этом чатике, 1 - какого типа объект у сообщеньки поменялся (0 - это простой лайк-сердечко), 12 - сколько теперь у этой месаги лайков, 0 - лайкал ли я эту месагу (чтобы нарисовать сердечко залитое или пустое). Посылается не всё прямо так, как тут описано, а иначе и в бинарном формате, но суть передана.

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

Я не ТС ))) Без понятия что он выбрал, кажется он тут вообще не появлялся.

ТС хранит как изначально предполагал. В отдельном инстансе базы с кодовым именем likes для каждого сообщения, у которого лайки вообще есть, лежит ключ chatname:12345:0 в котором лежит value типа set с содержимым типа [10,20,30,40]. Смысл: в чатике chatname на мессагу 12345 есть реакция типа 0 и её поставили юзеры 10, 20, 30, 40. Тип set встроен в базу так, что гарантирует отсутствие повторов значений и этих значений может быть дохера миллиардов с b+-tree-логарифмической ценой вставки/поиска в таком set.

На самой сообщеньке стоит 8-битовое поле, которое равно 1 если вообще хоть кто-то когда-то ставил лайк. Это поле фильтрует необходимость запросов в базу likes. Это поле говорит коду («бизнес-логике»), что надо сходить в сервис likes и позырить сколько там лайков и позырить лайкал ли данный юзер эту мессагу.

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

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

Между серверами (бирарниками, которые терминируют WebSocket) существуют «магистральные» коннекты, по которым они гоняют нотификации между собой. Каждый бирарник знает, что к чатику "chatname" подключен вася, петя и ещё кто-то на двух других серваках с такими-то коннектами до них.

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

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

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

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

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

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

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

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

Но замечу что, «магистральные коннекты» (это ты так назвал репликацию видимо), какой-то непонятный set который хранится в виде b-tree (таких готовых БД не существует кажись), заставлять сервер хранить стейт клиента, отдельный микросервис (наверное спроектирован по DDD) для лайков, и вообще архитектура которую ты тут описываешь это сложная и дорогая дичь, по сравнению с тем что предложил я - просто хранить список сообщений, линейный, без всяких сетов, и когда кто-то ставит реакцию - всем в чатике уходит новое сообщение с реакцией и id сообщения к которому она относится. Когда появялется новое сообщение любого типа - клиент просто получит новое сообщеие, куда бы он ни был подключен (ну да, прям как наверное в https://nanochat.ru).

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

О боже. Какой ужас, это же то, ради чего создаются чаты. Чтобы что - только я не смог придумать.

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

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

В обоих случаях еще нужна какая-то система нотификации что новое сообщение появилось, но это успешно решается уже редисом например, с pub/sub.

Но вообще твое решение будет наверное ок, пока живет на одном сервере и без «магистральных» коннектов.

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

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

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

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

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

Как понять - нужно клиенту это сообщение отправлять или нет?

Каждый раз отправлять юзеру всю историю?

Хранить где-то все изменения за всю историю (чат в гите)?

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

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

Можно конечно. Если вас такое решение устроит.

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

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

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

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

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

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

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

Ну ок ;)

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

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

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

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

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

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

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

Устраивает. Так и сделано тащемта.

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

Как понять - нужно клиенту это сообщение отправлять или нет?

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

Клиент при подключении качает только сообщеньки, которых у него нет. Знает он о том, что их нет по длине чатика (сервер сказал, что длина чатика 1060, а у меня закешированы только [1000,1050], а сервер сказал, что у чатика длина уже 1060). Ну плюс у каждой месаги есть версия, которая у меня может быть отставшей от сервера и надо подгрузить отредактированную - короче там долгая история.

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

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

Ну как знаете.

Я бы делал immutable данные.

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

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

Их нет, сервис хранения сообщенек однопоточный (просто много процессов, всё пошардено), рейс-кондишенов не бывает.

Немного наврал - бывают, но когда два участника синхронно поставили лайк на сообщеньку, когда там лайков ещё не было - нужно перевернуть 0 в 1 в одном байте. Но это делается CAS-ом.

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

Ок, а какие минусы вы видите в том что предлагаю я?

Если можно - ещё раз для тупых кратко: кратко схема, ПЛЮСЫ, минусы. Или забейте, завтра перечитаю ещё раз весь тред, попробую реконструировать вашу схему, пока я просто не уловил.

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

О, решение огонь, можно им троллить кого-нибудь на собеседах. Математически всё пруфаемо,

тут пруфаемо, что это то же самое что просто ставить биты.

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

то есть произведения простых однозначно отображаются на битовые массивы… так произведение двух первых простых это 11(двоичное). произведение первого и третьего - 101.

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

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

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

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

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

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