сервер чата: какой лучший (оптимизированный) способ сохранить журнал разговоров

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

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

  1. с помощью базы данных MySQL и

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

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

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

Спасибо


person Doua Beri    schedule 28.12.2011    source источник
comment
почему не на стороне клиента? Есть ли конкретная причина не делать этого? Есть ли какие-либо другие требования, помимо простого обслуживания? Кстати, что такое огромное? Как настроен ваш сервер?   -  person Caspar Kleijne    schedule 28.12.2011
comment
Наиболее естественным решением будет 1 (б) из вашего вопроса. Если «разговоры» происходят только между двумя пользователями, их идентификаторы и поле автоинкремента могут сформировать достойный первичный ключ. Или, если у вас есть концепция разговора (т. е. разговор = один сеанс чата), вы используете его в качестве идентификатора. Почему это будет медленно?   -  person ArjunShankar    schedule 28.12.2011
comment
Я бы не слишком беспокоился о скорости в 1b. Даже если разговор длится годами с несколькими сотнями сообщений в день, шансы, что вы когда-нибудь захотите увидеть все строки сразу, минимальны. Даже если пользователь открывает окно чата с полной историей, вы можете легко предварительно выбрать всего несколько строк вокруг текущего окна просмотра. В любом случае MySQL сможет получить строки намного быстрее, чем их сможет прочитать любой человек.   -  person Fredrik    schedule 28.12.2011
comment
@CasparKleijne - Одной из причин может быть желание иметь доступ к истории независимо от того, с какого клиента подключается пользователь.   -  person ArjunShankar    schedule 28.12.2011
comment
@Caspar Kleijne существует несколько клиентов (настольный, браузерный, мобильный), также пользователь может использовать разные компьютеры.   -  person Doua Beri    schedule 28.12.2011
comment
@Fredrik на самом деле этот метод в настоящее время реализован, и после 1 месяца использования у меня есть около 400 тысяч строк, и выбор начинает замедляться ... и мой сервис в настоящее время находится в бета-версии, всего 30-40 различных разговоров в день.   -  person Doua Beri    schedule 28.12.2011
comment
@DouaBeri звучит так, будто вам нужно либо посмотреть журнал медленных запросов, чтобы увидеть, не пропал ли индекс, либо подумать, делаете ли вы разумный выбор. Выбор всех строк и отображение 300 на экране может быть не самым разумным решением... 400 тыс. строк - это немного.   -  person Fredrik    schedule 28.12.2011
comment
@Fredrik 400 000 — это немного… но мы достигли этой цифры за 1 месяц с небольшим количеством бета-тестеров, и мы намерены вести журнал разговоров более 1 года.   -  person Doua Beri    schedule 28.12.2011
comment
@Doua На первый взгляд: вы выбираете только несколько строк при каждом выборе, и запрос должен быть проиндексирован. Следовательно, размер всей таблицы не должен сильно влиять на производительность. (Ну, если ваши данные становятся слишком большими для одновременного хранения в ОЗУ, у вас ЕСТЬ проблемы, но тогда использование другой базы данных не поможет)   -  person Voo    schedule 28.12.2011
comment
@DouaBeri Вы, должно быть, делаете это неправильно, вы НИКОГДА не будете показывать даже тысячу строк за раз на дисплее, поэтому на самом деле не имеет значения, есть ли у вас 2 миллиарда строк или 1500. Сумма, которую вы получаете от один запрос должен быть одинаковым (то, что отображается, и буфер с каждой стороны, достаточно большой для плавной прокрутки). Конечно, если вы получите всю беседу за один выбор, у вас будет много данных для переноса, но делать это таким образом было бы просто неправильно.   -  person Fredrik    schedule 29.12.2011
comment
Каким был ваш опыт спустя 10 лет? Достаточно ли было 1.b для поддержания приемлемой производительности чата?   -  person Éder Rocha    schedule 20.04.2021


Ответы (2)


Знаете, это большая архитектурная проблема. Такие компании, как facebook и twitter, потратили много времени и денег, чтобы надежно решить вашу проблему. Если ваш чат-сервер простой (как вы написали), используйте способ 1.b, но сделайте уровень абстракции (что-то вроде saveConversation, getConversation). Если в будущем скорость вас не удовлетворит, подумайте о более эффективном представлении, таком как база данных NoSQL (LevelDB или что-то в этом роде). Не думайте сейчас о производительности, сделайте прототип с хорошей абстракцией и подключаемой архитектурой.

person korifey    schedule 28.12.2011
comment
спасибо за Ваш ответ. В настоящее время мы уже используем 1.b, и даже если мы находимся на стадии бета-тестирования с закрытой группой бета-тестеров, у нас есть около 30-40 разговоров в день, и через месяц сервер mysql начинает работать немного медленнее. Я думаю в будущем переместить всю базу данных в Apache Cassandra, но на данный момент многое нужно изменить, а также мне нужно потратить больше денег на оборудование, так как apache cassandra требует ресурсов. - person Doua Beri; 28.12.2011
comment
@Doua Первая мысль, что не должно быть О, мы действительно должны использовать решение NoSQL, но больше, что, во имя бога, мы делаем неправильно, что выбор из нескольких сотен строк разговора (вы действительно не можете показать своим пользователям намного больше на раз) таблицы из 400 тыс. строк занимает так много времени? Так что лучше посмотрите, все ли индики используются правильно (и существуют ли), вы выбираете только те данные, которые вам действительно нужны, и так далее. Там можно многое сделать. Задавая здесь вопрос со схемой таблицы, запрос и объяснение тоже не помешают после того, как вы убедитесь, что очевидные вещи верны. - person Voo; 28.12.2011
comment
@ Воу, хорошо. возможно, я написал плохой пример. мы достигли 400 тысяч строк за 1 месяц с минимальным количеством клиентов (бета-тестеров). как только мы откроем сервис для публики, мы ожидаем, что у нас будет 500-1000 или даже больше клиентов, что приведет к нескольким миллионам строк в месяц и, вероятно, к нескольким сотням миллионов через 1 год. Таблица очень простая. он имеет 3 столбца: разговор_ид (int), сообщение (varchar), add_date (дата-время), разговор_ид и добавить_дата индексируются, и выбор выглядит примерно так: выберите сообщение из разговора_лога, где разговор_ид = 1 порядок по add_date - person Doua Beri; 28.12.2011
comment
@Doua Если предположить, что индексы используются правильно, не должно иметь большого значения, насколько велика таблица, поскольку мы никогда не будем выполнять полное сканирование таблицы. Лично я бы первым делом добавил ограничение — зачем читать тысячи строк, если вы можете показать своему пользователю только несколько сотен? (Если позже он захочет увидеть больше/более старые данные, просто спросите заново). Затем проверьте, объясните, что именно делает MySQL. О, а затем спросите здесь снова, потому что мои знания о MySQL начинаются и заканчиваются примерно на It's a DB - so is oracle... so I assume they work more or less the same? ;) - person Voo; 28.12.2011
comment
Этот ответ настолько совершенен, что это больно - person Nicolas Durán; 11.01.2017

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

Это прототип с парадигмами: -

  1. Чтобы вести беседу с несколькими участниками.
  2. Возможность подтверждения чатов.
  3. Опция обновлений действий.
  4. Возможность размещать вложения.
  5. Возможность поделиться координатами карты, как ваше текущее местоположение.
  6. Возможность покинуть чат.

Типы

Coordinates(
  lat FLOAT( 10, 6 ) NOT NULL ,
  lng FLOAT( 10, 6 ) NOT NULL 
)

Схема

dbo.User(UserId,<..Details..>)

dbo.Conversation(ConversationId,Title,CreationDate,LastActivityDate)

Индекс: LastActivityDate, ConversationId

dbo.Participants(ConversationId,UserId)

коллекция таблиц - создается динамически

msg.msg_*ConversationId*(MessageId,FromUserId,Message,Attachment,LocationCoordinates,Action,LogDate,AcknowledgeDate)

Абстракция

usp_TouchMessage(@Which):
   if (not exists (select 1 from INFORMATION_SCHEMA.TABLES where TABLE_SCHEMA+'.'+TABLE_NAME = 'msg.msg_'+CAST(@Which as nvarchar(max)))
   begin
        @dynamic_sql = 'create table msg.msg_'+CAST(@Which as nvarchar(max))+' (MessageId uniqueidentifier,FromUserId uniqueidentifier,Message nvarchar(max),Attachment BLOB,LocationCoordinates Coordinates,Action nvarchar(max),LogDate DateTime, AcknowledgeDate DateTime);';
        exec(@dynamic_sql);
        @dynamic_sql = 'CREATE TRIGGER msg.trig_msg_'+CAST(@Which as nvarchar(max))+'  ON msg.msg_'+CAST(@Which as nvarchar(max))+' AFTER INSERT AS Delete From msg.msg_'+CAST(@Which as nvarchar(max))+' where MessageId in (Select MessageId,ROW_NUMBER() OVER (order by LogDate Desc,AcknowledgeDate Desc,MessageId Desc) RN from msg.msg_'+CAST(@Which as nvarchar(max))+') where RN>=5000';
        exec(@dynamic_sql);

     end

usp_GetParticipants(@Where) :
    Select User.UserId,User.FullName 
          from Participants 
                inner join Users on (
                        Participants.UserId = Users.UserId 
                    And Participants.ConversationId = @Where
                );


usp_AddParticipants(@Who, @Where) :
    insert into Participants(ConversationId,UserId) values (@Where, @Who);
    @action = ( select User.FullName + ' has joined the chat' from User where User.UserId = @Who );
    update Conversation set LastActivityDate = GETUTCDATE() where ConversationId = @conversation_id;
    exec usp_TouchMessage(@Where);
    @dynamic_sql = 'insert into msg.msg_'+CAST(@Where as nvarchar(max))+' (MessageId,FromUserId,Action,LogDate) values (NewId(),@Who,@Action,GETUTCDATE())';
    sp_executesql @dynamic_sql, N'@Who uniqueidentifier,@Action nvarchar(max)', @Who = @Who, @Action = @Action;


usp_GetConversationList() :
    With Participation As (
        Select User.UserId,User.FullName,Participants.ConversationId 
          from Participants 
                inner join Users on (Participants.UserId = Users.UserId)
    ) select Conversation.*,
            (STUFF((
                    SELECT ',' + CAST([FullName] AS nvarchar(max)) 
                      FROM Participation 
                     WHERE (Participation.ConversationId = Conversation.ConversationId) 
              FOR XML PATH ('')
             ),1,1,'')) ParticipantList
        from Conversation 
    order by Conversation.LastActivityDate;


usp_GetConversationById(@Which,@Lite = 0,@Page = 1) :
    Select User.UserId,User.FullName 
          from Participants
            inner join Users on (Participants.UserId = Users.UserId and Participants.ConversationId = @Which);
    @dynamic_sql = 'select * from
                        (select u.UserId,u.FullName,m.MessageId,'
                                +(case when @Lite=1 then 'm.Message,m.LocationCoordinates,m.Attachment,m.Action,' else '' end)
                                +'m.LogDate,m.AcknowledgeDate, ROW_NUMBER() Over (order by m.LogDate Desc,m.AcknowledgeDate Desc,m.MessageId Desc) RN
                          From msg.msg_'+CAST(@Which AS nvarchar(max))+' m 
                            inner join User u on (m.FromUserId = u.UserId) 
                        ) tmp 
                    where RN Between ((@Page-1)*20+1) AND (@Page*20+1)
                ';
    sp_executesql @dynamic_sql, N'@Page bigint', @Page = @Page;
    If @Page = 1 And @Lite=0
    begin
        @dynamic_sql = 'update msg.msg_'+CAST(@Which as nvarchar(max))+' Set AcknowledgeDate = GETUTCDATE() where AcknowledgeDate is null and FromUserId <> @Who';
        sp_executesql @dynamic_sql, N'@Who uniqueidentifier', @Who = @Who;
    end

usp_GetConversation(@Who,@WithWhome,@Lite = 0,@Page = 1) :
    @conversation_id = (
        Select top 1 ConversationId 
          from Participants self 
            inner join Participants partner 
                on ( self.ConversationId = partner.ConversationId and self.UserId = @Who and partner.UserId = @WithWhome)
            where (Select count(1) from Participants where ConversationId = self.ConversationId) = 2
    );  
    if(@conversation_id is not null)
    then
        exec usp_GetConversationById(@conversation_id, @Lite, @Page);
    end


usp_PostConversationById(@Who,@Which,@WhatMessage,@WhichLocation,@WhatAttachment) :
    update Conversation set LastActivityDate = GETUTCDATE() where ConversationId = @Which;
    exec usp_TouchMessage(@Which);
    @dynamic_sql = 'insert into msg.msg_'+CAST(@Which as nvarchar(max))+' (MessageId,FromUserId,Message,Attachment,LocationCoordinates,LogDate) values (NewId(),@Who,@WhatMessage,@WhichLocation,@WhatAttachment,GETUTCDATE())';
    sp_executesql @dynamic_sql, N'@Who uniqueidentifier,@WithWhome uniqueidentifier,@WhatMessage nvarchar(max),@WhichLocation Coordinates,@WhatAttachment BLOB', @Who = @Who, @WhatMessage = @WhatMessage, @WhichLocation = @WhichLocation, @WhatAttachment = @WhatAttachment;


usp_PostConversation(@Who,@WithWhome,@WhatMessage,@WhichLocation,@WhatAttachment) :
    @conversation_id = (
        Select top 1 ConversationId 
          from Participants self 
            inner join Participants partner 
                on ( self.ConversationId = partner.ConversationId and self.UserId = @Who and partner.UserId = @WithWhome)
            where (Select count(1) from Participants where ConversationId = self.ConversationId) = 2
    );  
    if(@conversation_id is not null)
    then
        @conversation_id = newid()
        insert into Conversation(ConversationId,CreationDate) values (@conversation_id,GETUTCDATE());
        exec usp_AddParticipants(@Who,@conversation_id);
        exec usp_AddParticipants(@WithWhome,@conversation_id);
    end
    exec usp_PostConversationById(@Who,@conversation_id,@WhatMessage,@WhichLocation,@WhatAttachment);


usp_UpdateConversationAlias(@Who,@Which,@WithWhat) :
    @action = ( select User.FullName + ' has changed title'+isnull(' from <b>'+Conversation.Title+'</b>','')+isnull(' to <b>'+@WithWhat+'</b>','') from User inner join Conversation on (Conversation.ConversationId = @Which and User.UserId = @Who));
    update Conversation set LastActivityDate = GETUTCDATE(), Title = @WithWhat where ConversationId = @conversation_id;
    if (not exists (select 1 from INFORMATION_SCHEMA.TABLES where TABLE_SCHEMA+'.'+TABLE_NAME = 'msg.msg_'+CAST(@Which as nvarchar(max)))
    exec usp_TouchMessage(@Which);
    @dynamic_sql = 'insert into msg.msg_'+CAST(@Which as nvarchar(max))+' (MessageId,FromUserId,Action,LogDate) values (NewId(),@Who,@Action,GETUTCDATE())';
    sp_executesql @dynamic_sql, N'@Who uniqueidentifier,@Action nvarchar(max)', @Who = @Who, @Action = @Action;


usp_LeaveConversation(@Who,@Which) :
    delete from Participants where ConversationId = @Where and UserId = @Who;
    if(not exists (Select 1 From Participants Where ConversationId = @Which))
    begin
        @dynamic_sql = 'drop table msg.msg_'+CAST(@Which as nvarchar(max))+';
        exec @dynamic_sql;
        delete from Conversation where ConversationId = @Which;
    end
    else
    begin
        @action = ( select User.FullName + ' has left the chat' from User where User.UserId = @Who );
        update Conversation set LastActivityDate = GETUTCDATE() where ConversationId = @conversation_id;
        exec usp_TouchMessage(@Which);
        @dynamic_sql = 'insert into msg.msg_'+CAST(@Which as nvarchar(max))+' (MessageId,FromUserId,Action,LogDate) values (NewId(),@Who,@Action,GETUTCDATE())';
        sp_executesql @dynamic_sql, N'@Who uniqueidentifier,@Action nvarchar(max)', @Who = @Who, @Action = @Action;
    end
person Projjwal K Maiti    schedule 26.06.2017
comment
О чем ты говоришь? Вы читаете вопрос ОП? - person kidnan1991; 28.01.2021