聊一聊关于聊天记录的存储
背景
即时通讯(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 对聊天记录这个场景的存储还是有一定优势的,可以应对高速的数据增长,而不用在业务代码层做过多的适配;部署相对简单,无特殊依赖,运维成本相对较低。