聊一聊關於聊天記錄的存儲

背景

即時通訊(Instant Messaging),也就是我們常說的 IM,其實在很多業務場景上都會有或多或少的應用,有的會是核心,有的會是輔助。

既然是聊天,那麼必然就會產生聊天記錄,而且聊天記錄隨着人數的增加和時間的推移,很容易出現爆炸式的增長,這個對存儲其實壓力是很大的。

舉個大家都很熟悉的例子,一個羣聊,幾分鐘不看,再打開就是 99+ 的未讀消息。

把即時通訊這個技術,放到醫療環境下,也是同樣適用的。

患者去線下醫院看病,肯定離不開和醫生的問答,這些問答,對系統來說,其實都是聊天記錄。

如果把這個場景放到線上進行,也就是正常的和我們在微信聊天那樣了。

說了那麼多有的沒的,也算是把大背景交代了一下,那麼接下來就看看這個聊天記錄存儲的選型吧。

技術選型

既然要存儲,那麼肯定就會有很多選擇,關係型數據庫,非關係型數據庫等。

當然這個很大程度上是和具體業務場景掛鉤的,離開了業務場景,基本就是在空談。

在醫患關係裏面的聊天記錄,是一個十分十分核心的內容,並且必需要長期保存,不能丟,可查詢。

並且這些聊天記錄是和某次問診強關聯的,所以單獨拿出幾條聊天記錄出來,是沒有意義的,因爲他們沒有關聯,串聯不起來。

按照以往的經驗看,一次問診,醫生和患者之間的聊天記錄在 50 條之內的佔據了大部分,超過50條的,佔少數。

這個和其他的 IM 情景可能不太一樣。

MySQL

業務開始,大概率就是選用 MySQL 去存了這些數據了,單庫單表,但是這種情況下很容易達到單表千萬和上億的級別。

後面面臨的基本就是分庫分表的操作了。

分庫分表,基本就是根據問診號去進行哈希,然後放到不同的庫不同的表。

這裏會有一個不確定因素,分多少個庫和分多少張表?

分的多,囊中羞澀;分的少,再一次達到量級的時候又要重新分,大動干戈,這個時候最怕的就是動到了哈希的規則。

所以選 MySQL 的話,到了中後期確實還是會有一點力不從心。線性擴展對這一塊還是非常重要的。

如果想改動小,避免分庫分表,或許可以試試 TiDB,但是它要的配置,也不是中小企業所能接受的,80% 以上的概率會被 Pass 掉。

https://docs.pingcap.com/zh/tidb/stable/hardware-and-software-requirements

Cassandra

Cassandra 是一個分佈式、無中心、彈性可擴展的 NoSQL 數據庫,基於 Amazon Dynamo 的分佈式設計和 Google Bigtable 的數據模型。

https://cassandra.apache.org/doc/latest/architecture/overview.html

爲什麼考慮選型 Cassandra 呢? 對上面說的醫患場景,嚴格上是屬於寫多讀少的,查詢基本只會基於問診號去查詢,這個是相對比較明確的。

Discord 在 2017 年的時候有一篇博客講述了他們是怎麼存儲數十億消息記錄的 ,說的比較詳細了。

https://blog.discord.com/how-discord-stores-billions-of-messages-7fa6ec7ee4c7

其實他們選數據庫的訴求,也是符合大部分涉及 IM 這一塊的。

老黃也是從這裏受到了啓發,認識了這個數據庫。

經常拿來比較的話,應該是 HBase,一個在國內火,一個在國外受歡迎。

可以看看這個對比,瞭解一下兩者的異同: https://www.scnsoft.com/blog/cassandra-vs-hbase

如果選擇要用 Cassandra, 那麼數據模型的設計,一定是所有環節中最爲重要的一步,如果這一步沒有做好的話,那後面基本上會是災難級別,基本不能愉快的玩耍。

那麼對醫患關係裏面的這個聊天模型其實比較簡單。

CREATE TABLE IF NOT EXISTS messages(
    inq_id text,
    send_time bigint,
    sender_id text,
    sender_role tinyint,
    msg_type tinyint,
    msg_body text,
    PRIMARY KEY (inq_id, send_time)
) WITH CLUSTERING ORDER BY (send_time ASC)

對照正常的 IM 羣聊, 這個問診號 (inq_id) 就可以認爲是一個羣聊,一個頻道。

爲什麼沒有消息Id這樣的字段呢?多來源,非自研,無實際意義。

下面再來看看如何在 C# 裏面進行操作, 這裏用的是 DataStax 提供的 CassandraCSharpDriver 客戶端。

寫入:

var cluster = Cassandra.Cluster.Builder()
                        .AddContactPoints("127.0.0.1")
                        .WithDefaultKeyspace("messaging")
                        .Build();

var inqId = "xxxxxx";
var sendTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var senderId = "xxxx";
var senderRole = 1;
var msgType = 1;
var msgBody = "zzzz";

string INSERT_SQL = @" INSERT INTO messages(inq_id, send_time, sender_id, sender_role, msg_type, msg_body)
VALUES (?, ?, ?, ?, ?, ?) ";

var session = cluster.Connect();

var stmt = session.Prepare(INSERT_SQL).Bind(inqId, sendTime, senderId, senderRole, msgType, msgBody));

session.Execute(stmt);

讀取:

var cluster = Cassandra.Cluster.Builder()
                .AddContactPoints("127.0.0.1")
                .WithDefaultKeyspace("messaging")
                .Build();

var inqId = "xxxxxx";

string GET_MSG_SQL = @" SELECT * FROM messages WHERE inq_id = ? ";

var session = cluster.Connect();

var stmt = session.Prepare(GET_MSG_SQL).Bind(inqId);

var rowset = session.Execute(stmt);

Console.WriteLine("患者\t\t\t\t\t醫生");

foreach (var row in rowset)
{
    // 解析從 cassandra 中返回的行
    var msg_body = row.GetValue<string>("msg_body");
    var sender_role = row.GetValue<sbyte>("sender_role");

    if (sender_role == 0)
    {
        Console.WriteLine($"{msg_body}\t\t\t\t\t");
    }
    else
    {
        Console.WriteLine($"\t\t\t\t\t{msg_body}");
    }
}

寫在最後

存儲的選擇其實還是有點門道的,根據不同的應用場景,找出比較適合當前場景的幾個方案,再選擇一個成本沒這麼高的。

Cassandra 對聊天記錄這個場景的存儲還是有一定優勢的,可以應對高速的數據增長,而不用在業務代碼層做過多的適配;部署相對簡單,無特殊依賴,運維成本相對較低。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章