• 聊一聊关于聊天记录的存储


    背景

    即时通讯(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("患者					医生");
    
    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}					");
        }
        else
        {
            Console.WriteLine($"					{msg_body}");
        }
    }
    

    写在最后

    存储的选择其实还是有点门道的,根据不同的应用场景,找出比较适合当前场景的几个方案,再选择一个成本没这么高的。

    Cassandra 对聊天记录这个场景的存储还是有一定优势的,可以应对高速的数据增长,而不用在业务代码层做过多的适配;部署相对简单,无特殊依赖,运维成本相对较低。

    如果您认为这篇文章还不错或者有所收获,可以点击右下角的【推荐】按钮,因为你的支持是我继续写作,分享的最大动力!
    声明: 本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。如果您发现博客中出现了错误,或者有更好的建议、想法,请及时与我联系!!如果想找我私下交流,可以私信或者加我微信。
  • 相关阅读:
    货币计算请避免使用float和double
    4、刷新令牌
    Advanced Go Concurrency Patterns
    Go Concurrency Patterns: Pipelines and cancellation
    Go Concurrency Patterns: Context At Google, we require that Go programmers pass a Context parameter as the first argument to every function on the call path between incoming and outgoing requests.
    OSS的SDK网络超时处理
    Redis 到底是怎么实现“附近的人”这个功能的呢?
    阿里云 Redis 开发规范
    百度云的「极速秒传」使用的是什么技术?
    linux中高并发socket最大连接数的优化详解
  • 原文地址:https://www.cnblogs.com/catcher1994/p/14998595.html
Copyright © 2020-2023  润新知