Как хранить чат в базе данных
Перейти к содержимому

Как хранить чат в базе данных

  • автор:

Система личных сообщений

Подскажите, пожалуйста, как устроена система личных сообщений между пользователями в крупных проектах (Вконтакте, Одноклассники, Topface и т.п) с учетом масштабирования? Интересует именно хранение данных о пользователях/чатах/сообщениях и доступ к этим данным.

Допустим у нас есть 100 000 000 пользователей и необходимо сделать горизонтальное масштабирование (шардинг) этих данных на 200 MySQL серверов. На данный момент, я вижу это следующим образом: разделяем всех пользователей и данные на 200 серверов по user_id, получается примерно 500 000 юзеров на каждый сервер. Можно еще разделить данные на споты по 1000 юзеров и получится, что на каждом сервере БД будет 500 спотов по 1000 юзеров (всего 500 000 юзеров на сервер).

Доступ к серверам БД/спотам можно вычислять по user_id, например spot_id = user_id % 1000. Каждый спот будет хранить данные в виде таблиц, например:

Spot1: — spot1_users (информация о пользователях) — spot1_chats (информация о чатах между пользователями) — spot1_messages (сообщения из чатов) .

Spot2: — spot2_users — spot2_chats — spot2_messages . Проблема возникает тогда, когда необходимо хранить/получать общие данные между юзерами. Например, 2 пользователя начинают переписку между собой. В этом случае необходимо создать чат в таблице spotN_chats и поместить туда информацию chat_id (id чата), receiver_id(id получателя), sender_id (id отправителя). Сообщения будут хранится в таблице messages (chat_id, message, time).

Теперь начинается самое интересное — пользователи начинают переписку между собой. Здесь необходимо сделать такие базовые операции: 1) Создание нового чата между 2 пользователями 2) Получение информации о чате или списке чатов конкретного пользователя 3) Создание нового сообщения 4) Получение списка сообщений по chat_id

Также есть 2 варианта развития событий: 1) пользователи находятся на одном споте (например, spot1); 2) пользователи находятся на разных спотах (например, spot1 и spot2);

Задача 1. Пользователь1 решил начать переписку с пользователем2. В этом случае необходимо создать новый чат в БД. Если пользователи на одном споте, то можно просто создать новый чат в таблице spot1_chats, получать chat_id, а дальше создавать новые сообщения в таблице spot1_messages с полученным chat_id. Но если пользователи находятся на разных спотах (spot1, spot2), то такой подход не будет работать, поскольку чтобы каждый пользователь увидел список своих чатов, то их нужно дублировать на 2 споты одновременно. Но в таком случае chat_id будут разными для 2 таблиц(spot1_chats, spot2_chats) если использовать поле autoincrement для chat или же нужно строить какой-нибудь общий для 100 млн. пользователей генератор id для новых чатов. Кроме того, если 2 пользователи на одном споте, то при дублировании чатов все равно будет создан только 1 чат в таблице spot1_users, а вот если мы решим перенести 1 пользователя на другой спот, то как дублировать информацию о чатах?

Задача 2. Пользователь1 отправляет сообщение пользователю2, chat_id у нас уже есть после создания нового чата. Здесь возникает та же самая проблема, что и в первой задаче. Если 2 пользователи на одном споте, то мы просто добавляем новое сообщение в таблицу spot1_messages, но если в будущем захотим перенести пользователя на другой спот, то как дублировать сообщения? Если же пользователи на разных спотах, то для отправки сообщения необходимо создать новое сообщение в таблице spot1_messages и в таблице spot2_messages. Кроме того, если мы хотим обновлять какой-нибудь счетчик новых сообщений или время последнего сообщения в чатах, то нужно будет также обновлять информацию о чатах в таблицах spot1_chats и spot2_chats. Получается для простой отправки одного сообщения необходимо будет сделать несколько запросов в БД, а именно: создать новое сообщения в таблицах spot1_messages и spot2_messages, а также обновить информацию о чате в таблицах spot1_chats и spot2_chats.

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

БД для хранения истории сообщений

Добрый день. Пишу чат на qt. Нужно сделать так, чтобы при нажатии на определенного пользователя подгружалась история переписки с ним. Возникло несколько вопросов. Какую бд посоветуете для хранения сообщений? Я пока остановился на sqlite или postgresql. И как можно сделать так, чтобы более старые сообщения подгружались по мере необходимости, ведь если переписка будет огромная и будет подтягивать сразу целиком, то будут проблемы с производительностью. Буду признателен за помощь.

Рекомендуем хостинг TIMEWEB

Рекомендуем хостинг TIMEWEB

Стабильный хостинг, на котором располагается социальная сеть EVILEG. Для проектов на Django рекомендуем VDS хостинг.

Подписка на обсуждение 2
Подписка на раздел 237

Вам это нравится? Поделитесь в социальных сетях!

Evgenii Legotckoi

  • Evgenii Legotckoi
  • #
  • 25 мая 2018 г. 21:01

Для серверной части используйте postresql, если планируете и сервер писать для вашего чата.

Для кеширования сообщения в клиенте используйте однозначно sqlite. Использовать для клиента postgresql — это неправильно.

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

Примерный мехнизм может быть такой. Если вы используете для отображения сообщений что-нибудь наподобие QListView, то у него можно использовать изменение значений вертиклаьного скроллбара. Если значения изменились до крайних, например до 0, то нужно проверить ID, самого старого сообщения в списке и попытаться подгрузить например ещё 50, которые ещё старше.

  • evgenm27 → Evgenii Legotckoi
  • #
  • 26 мая 2018 г. 10:29

Спасибо за ответ, я с чатом раньше дела не имел, разъясните пару моментов? Получается на сервере будет postgre, где должна быть вся информация о пользователях (логин, пароль, имя, дата рождения к примеру). А на клиенте будет sqlite, где будет храниться именно переписка с пользователями?

Evgenii Legotckoi

  • Evgenii Legotckoi → evgenm27
  • #
  • 27 мая 2018 г. 11:08

Если вы делаете чат с клиент-серверной архитектурой, то да, на этом самом сервере имеет смысл использовать postresql, но поначалу можно использовать SQLite для тестирования и т.д. Подключить postgresql на серверное приложение труда не составит. По сути сервер может даже быть написан на чём-то ином, а не на Qt.

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

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

Задача по проектированию чат-сервера

Как бы вы подошли к проектированию чат-сервера? Предоставьте информацию о компонентах внутренней системы (backend), классах и методах. Перечислите самые трудные задачи, которые необходимо решить.

Обложка поста Задача по проектированию чат-сервера

Условие задачи

Как бы вы подошли к проектированию чат-сервера? Предоставьте информацию о компонентах внутренней системы (backend), классах и методах. Перечислите самые трудные задачи, которые необходимо решить.

Решение

Разработка чат-сервера — огромный проект, команды из многих людей проводят месяцы и годы, пытаясь создать хороший чат-сервер. Вы должны сосредоточиться на отдельном аспекте задачи. Ваше решение не обязано точно отражать реальность, но должно давать хорошее представление о фактической реализации.

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

Предполагается, что «дружба» взаимна: вы дружите со мной только в том случае, если и я дружу с вами. Наша чат-система будет поддерживать как групповые, так и приватные беседы. Мы не будем рассматривать голосовой чат, видеочат и пере­дачу файлов.

Какие конкретные операции должен поддерживать чат?

Мы приведем несколько примеров:

  • Вход и выход из чата;
  • Добавление запроса (отправка, приём и отклонение);
  • Обновление информации о статусе;
  • Создание приватных и групповых чатов;
  • Добавление новых сообщений в приватные и групповые чаты.

Это далеко не полный список. Если вам хватит времени, добавьте дополнительные действия.

Что можно узнать из этих требований?

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

Какие базовые компоненты должны присутствовать в системе?

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

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

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

Примечание переводчика Ещё лучше для передачи данных подойдёт формат JSON.

Сервер будет состоять из множества компьютеров. Данные будут распределены между машинами, что требует «переключения» с одного устройства на другое. Возможно, некоторые данные придется перераспределять между машинами. Чтобы сократить время поиска, нужно избегать узких мест. Например, если аутентифика­цией пользователей занимается только одна машина, то её выход из строя закроет доступ к системе миллионам пользователей.

Какие основные объекты и методы должны поддерживаться системой?

Ключевые объекты нашей системы — пользователи, беседы и сообщения о статусах. Всё это можно реализовать в классе UserManager . Если бы мы уделяли больше вни­мания сетевым аспектам задачи или другим компонентам, вероятно, нам пришлось бы создать для них дополнительные объекты.

/* UserManager — центральный класс для основных действий пользователя. */ public class UserManager < private static UserManager instance; /* Связывает идентификатор с пользователем */ private HashMapusersById = new HashMap(); /* Связывает имя учётной записи с пользователем */ private HashMap usersByAccountName = new HashMap(); /* Связывает идентификатор пользователя с подключённым пользователем */ private HashMap onlineUsers = new HashMap(); public static UserManager getInstance() < if (instance == null) < instance = new UserManager(); >return instance; > public void addUser(User fromUser, String toAccountName) < User toUser = usersByAccountName.get(toAccountName); AddRequest req = new AddRequest(fromUser, toUser, new Date()); toUser.receivedAddRequest(req); fromUser.sentAddRequest(req); >public void approveAddRequest(AddRequest req) < req.status = RequestStatus.Accepted; User from = req.getFromUser(); User to = req.getToUser(); from.addContact(to); to.addContact(from); >public void rejectAddRequest(AddRequest req) < req.status = RequestStatus.Rejected; User from = req.getFromUser(); User to = req.getToUser(); from.removeAddRequest(req); to.removeAddRequest(req); >public void userSignedOn(String accountName) < User user = usersByAccountName.get(accountName); if (user != null) < user.setStatus(new UserStatus(UserStatusType.Available, "")); onlineUsers.put(user.getId(), user); >> public void userSignedOff(String accountName) < User user = usersByAccountName.get(accountName); if (user != null) < user.setStatus(new UserStatus(UserStatusType.Offline, "")); onlineUsers.remove(user.getId()); >> > 

Метод receivedAddRequest класса User оповещает пользователя В о том, что пользо­ватель А запросил добавление его в список контактов. Пользователь В соглашается или отклоняет запрос (при помощи UserManager.approveAddRequest или rejectAddRequest ), а класс UserManager обеспечивает добавление пользователей в списки контактов двух пользователей.

Метод sentAddRequest класса User вызывается UserManager для добавления AddRequest в список запросов пользователя А. Поэтому последовательность операций должна выглядеть так:

  1. Пользователь А нажимает кнопку «добавить пользователя» в клиентской программе, запрос отправляется на сервер.
  2. Пользователь В вызывает requestAddUser(User В) .
  3. Этот метод вызывает UserManager.addUser .
  4. UserManager вызывает методы UserA.sentAddRequest и UserB.receivedAddRequest .

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

import java.util.ArrayList; import java.util.Date; import java.util.HashMap; public class User < private int id; private UserStatus status = null; /* Связывает идентификатор пользователя другого участника с чатом */ private HashMapprivateChats = new HashMap(); /* Список групповых чатов */ private ArrayList groupChats = new ArrayList(); /* Связывает идентификатор другого пользователя с полученным запросом на добавление */ private HashMap receivedAddRequests = new HashMap(); /* Связывает идентификатор другого пользователя с отправленным запросом на добавление*/ private HashMap sentAddRequests = new HashMap(); /* Связывает идентификатор пользователя с объектом пользователя */ private HashMap contacts = new HashMap(); private String accountName; private String fullName; public User(int id, String accountName, String fullName) < this.accountName = accountName; this.fullName = fullName; this.id = id; >public boolean sendMessageToUser(User toUser, String content) < PrivateChat chat = privateChats.get(toUser.getId()); if (chat == null) < chat = new PrivateChat(this, toUser); privateChats.put(toUser.getId(), chat); >Message message = new Message(content, new Date()); return chat.addMessage(message); > public boolean sendMessageToGroupChat(int groupId, String content) < GroupChat chat = groupChats.get(groupId); if (chat != null) < Message message = new Message(content, new Date()); return chat.addMessage(message); >return false; > public void setStatus(UserStatus status) < this.status = status; >public UserStatus getStatus() < return status; >public boolean addContact(User user) < if (contacts.containsKey(user.getId())) < return false; >else < contacts.put(user.getId(), user); return true; >> public void receivedAddRequest(AddRequest req) < int senderId = req.getFromUser().getId(); if (!receivedAddRequests.containsKey(senderId)) < receivedAddRequests.put(senderId, req); >> public void sentAddRequest(AddRequest req) < int receiverId = req.getFromUser().getId(); if (!sentAddRequests.containsKey(receiverId)) < sentAddRequests.put(receiverId, req); >> public void removeAddRequest(AddRequest req) < if (req.getToUser() == this) < receivedAddRequests.remove(req); >else if (req.getFromUser() == this) < sentAddRequests.remove(req); >> public void requestAddUser(String accountName) < UserManager.getInstance().addUser(this, accountName); >public void addConversation(PrivateChat conversation) < User otherUser = conversation.getOtherParticipant(this); privateChats.put(otherUser.getId(), conversation); >public void addConversation(GroupChat conversation) < groupChats.add(conversation); >public int getId() < return id; >public String getAccountName() < return accountName; >public String getFullName() < return fullName; >> 

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

import java.util.ArrayList; import java.util.Date; public abstract class Conversation < protected ArrayListparticipants = new ArrayList(); protected int id; protected ArrayList messages = new ArrayList(); public ArrayList getMessages() < return messages; >public boolean addMessage(Message m) < messages.add(m); return true; >public int getId() < return id; >> public class GroupChat extends Conversation < public void removeParticipant(User user) < participants.remove(user); >public void addParticipant(User user) < participants.add(user); >> public class PrivateChat extends Conversation < public PrivateChat(User user1, User user2) < participants.add(user1); participants.add(user2); >public User getOtherParticipant(User primary) < if (participants.get(0) == primary) < return participants.get(1); >else if (participants.get(1) == primary) < return participants.get(0); >return null; > > public class Message < private String content; private Date date; public Message(String content, Date date) < this.content = content; this.date = date; >public String getContent() < return content; >public Date getDate() < return date; >> 

Классы AddRequest и UserStatus — простые классы с минимальной функциональ­ностью. Их основное назначение — группировка данных, используемых другими классами.

import java.util.Date; public class AddRequest < private User fromUser; private User toUser; private Date date; RequestStatus status; public AddRequest(User from, User to, Date date) < fromUser = from; toUser = to; this.date = date; status = RequestStatus.Unread; >public RequestStatus getStatus() < return status; >public User getFromUser() < return fromUser; >public User getToUser() < return toUser; >public Date getDate() < return date; >> public class UserStatus < private String message; private UserStatusType type; public UserStatus(UserStatusType type, String message) < this.type = type; this.message = message; >public UserStatusType getStatusType() < return type; >public String getMessage() < return message; >> public enum UserStatusType < Offline, Away, Idle, Available, Busy >public enum RequestStatus

Какие проблемы окажутся самыми трудными (или интересными)?

Попробуйте ответить на следующие вопросы:

Как определить активных пользователей?

Подключённый пользователь — не всегда активный пользователь. Если сеанс не был корректно завершён, сессия может «зависнуть», расходуя ресурсы сервера и искажая реальное количество активных пользователей. Как с достаточно большой точностью определить активность пользователей?

Что делать с информационными конфликтами?

Часть информации должна храниться в оперативной памяти, а часть — в базе данных. Что случится, если синхронизация между ними будет нарушена? Какую информацию считать правильной?

Что делать с возрастающим количеством пользователей?

Как на этапе проектирования заложить многократное увеличение количества пользователей?

Как защититься от DоS-атак?

Клиенты могут передавать данные на сервер, но что, если сервер попытаются «завалить» по­током запросов? Как предотвратить атаку?

Как хранить JSON в базе данных, не привлекая внимания DBA

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

Альтернатива этому всему — хранение таких данных в виде JSON. Но реляционные СУБД JSON не любят. Дошло даже до того, что Mongo, киллер-фичей которой как раз является хранение данных в виде JSON, набрала на этом нешуточную популярность. Но, к счастью, эти времена потихоньку отступают. Во многом благодаря усилиям разработчиков PostgreSQL. Раньше все попытки положить JSON в базу данных встречали враждебную реакцию со стороны DBA, аналитиков и QA. Эту реакцию можно описать так: «У нас же тогда будет не база данных, а помойка!»

Спикеры поговорят о связанных с этим проблемах, расскажут, как их можно решить не выходя из реляционной структуры, и продемонстрируют, что с этими решениями не так. Также вы узнаете, в каких случаях для решения этих проблем можно применить JSON и почему львиная доля опасений DBA, аналитиков и QA теперь неактуальна. А еще — как при использовании JSON не выстрелить себе в ногу, потому что нюансы есть.

  • # hibernate
  • # jpa
  • # json
  • # postgresql
  • # spring-data

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *