为自己搭建一个分布式 IM 系统二【从查找算法聊起】

image

前言

最近这段时间确实有点忙,这篇的目录还是在飞机上敲出来了的。

言归正传,上周更新了 cim 第一版:为自己搭建一个分布式 IM(即时通讯) 系统;没想到反响热烈,最高时上了 GitHub Trending Java 版块的首位,一天收到了 300+ 的 star。

image

现在总共也有 1.3K+ 的 star,有几十个朋友参加了测试,非常感谢大家的支持。

在这过程中也收到一些 bug 反馈,feature 建议;因此这段时间我把一些影响较大的 bug 以及需求比较迫切的 feature 调整了,本次更新的 v1.0.1 版本:

  • 客户端超时自动下线。
  • 新增 AI 模式。
  • 聊天记录查询。
  • 在线用户前缀模糊匹配。

下面谈下几个比较重点的功能。

客户端超时自动下线 这个功能涉及到客户端和服务端的心跳设计,比较有意思,也踩了几个坑;所以准备留到下次单独来聊。

AI 模式

大家应该还记得这个之前刷爆朋友圈的 估值两个一个亿的 AI 核心代码

和我这里的场景再合适不过了。

于是我新增了一个命令用于一键开启 AI 模式,使用情况大概如下。

image

欢迎大家更新源码体验,融资的请私聊我🤣。

聊天记录

聊天记录也是一个比较迫切的功能。

image

使用命令 :q 关键字 即可查询与个人相关的聊天记录。

这个功能其实比较简单,只需要在消息发送及接收消息时保存即可。

但要考虑的一点是,这个保存消息是 IO 操作,不可避免的会有耗时;需要尽量避免对消息发送、接收产生影响。

异步写入消息

因此我把消息写入的过程异步完成,可以不影响真正的业务。

实现起来也挺简单,就是一个典型的生产者消费者模式。

image

主线程收到消息之后直接写入队列,另外再有一个线程一直源源不断的从队列中取出数据后保存聊天记录。

大概的代码如下:

image

image

写入消息的同时会把消费消息的线程打开:

image

而最终存放消息记录的策略,考虑后还是以最简单的方式存放在客户端,可以降低复杂度。

image

简单来说就是根据当前日期+用户名写入到磁盘里。

当客户端关闭时利用线程中断的方式停止了消费队列的线程。

image

image

这点的设计其实和 logback 写日志的方式比较类似,感兴趣的可以去翻翻 logback 的源码,更加详细。

回调接口

至于收到其他客户端发来的消息时则是利用之前预留的消息回调接口来写入日志。

image

收到消息后会执行自定义的回调接口。

image

于是在这个回调方法中实现写入逻辑即可,当后续还有其他的消息处理逻辑时也能在这里直接添加。

当处理逻辑增多时最好是改为责任链模式,更加清晰易维护。

查找算法

接下来是本文着重要讨论的一个查找算法,准确的说是一个前缀模糊匹配的算法。

实现的效果如下:

image

使用命令 :qu prefix 可以按照前缀的方式搜索用户信息。

当然在命令行中其实意义不大,但是在移动端中确是比较有用的。类似于微信按照用户名匹配:

image

因为后期打算出一个移动端 APP,所以就先把这个功能实现了。

从效果也看得出来:就是按照输入的前缀匹配字符串(目前只支持英文)。

在没有任何限制的条件下最快、最简单的实现方式可以直接把所有的字符串存放在一个容器中 (List、Set),查询时则挨个遍历;利用 String.startsWith("prefix") 进行匹配。

但这样会有几个问题:

  • 存储资源比较浪费,不管是 list 还是 Set 都会有额外的损耗。
  • 查询效率较低,需要遍历集合后再遍历字符串的 char 数组(String.startsWith 的实现方式)。

字典树

基于以上的问题我们可以考虑下:

假设我需要存放 java,javascript,jsp,php 这些字符串时在 ArrayList 中会怎么存放?

image

很明显,会是这样完整的存放在一个数组中;同时这个数组还可能存在浪费,没有全部使用完。

但其实仔细观察这些数据会发现有一些共同特点,比如 java,javascript 有共同的前缀 java;和 jsp 有共同的前缀 j

那是否可以把这些前缀利用起来呢?这样就可以少存储一份。

比如写入 java,javascript 这两个字符串时存放的结构如下:

image

当再存入一个 jsp 时:

image

最后再存入 jsf 时:

image

相信大家应该已经看明白了,按照这样的存储方式可以节省很多内存,同时查询效率也比较高。

比如查询以 jav 开头的数据,只需要从头结点 j 开始往下查询,最后会查询到 ava 以及 script 这两个个结点,所以整个查询路径所经历的字符拼起来就是查询到的结果java+javascript

如果以 b 开头进行查询,那第一步就会直接返回,这样比在 list 中的效率高很多。

但这个图还不完善,因为不知道查询到啥时候算是匹配到了一个之前写入的字符串。

比如在上图中怎么知道 j+ava 是一个我们之前写入的 java 这个字符呢。

因此我们需要对这种是一个完整字符串的数据打上一个标记:

image

比如这样,我们将 ava、script、p、f 这几个节点都换一个颜色表示。表明查询到这个字符时就算是匹配到了一个结果。

而查到 s 这个字符颜色不对,代表还需要继续往下查。

比如输入关键字 js 进行匹配时,当它的查询路径走到 s 这里时判断到 s 的颜色不对,所以不会把 js 作为一个匹配结果。而是继续往下查,发现有两个子节点 p、f 颜色都正确,于是把查询的路径 jspjsf 都作为一个匹配结果。

而只输入 j,则会把下面所有有色的字符拼起来作为结果集合。

这其实就一个典型的字典树。

具体实现

下面则是具体的代码实现,其实算法不像是实现一个业务功能这样好用文字分析;具体还是看源码多调试就明白了。

谈下几个重点的地方吧:

image

字典树的节点实现,其中的 isEnd 相当于图中的上色。

利用一个 Node[] children 来存放子节点。

image

为了可以区分大小写查询,所以子节点的长度相当于是 26*2

写入数据

image

这里以一个单测为例,写入了三个字符串,那最终形成的数据结构如下:

image

图中有与上图有几点不同:

  • 每个节点都是一个字符,这样树的高度最高为52。
  • 每个节点的子节点都是长度为 52 的数组;所以可以利用数组的下标表示他代表的字符值。比如 0 就是大 A,26 则是小 a,以此类推。
  • 有点类似于之前提到的布隆过滤器,可以节省内存。

debug 时也能看出符合上图的数据结构:

image

所以真正的写入步骤如下:

image
  1. 把字符串拆分为 char 数组,并判断大小写计算它所存放在数组中的位置 index
  2. 将当前节点的子节点数组的 index 处新增一个节点。
  3. 如果是最后一个字符就将新增的节点置为最后一个节点,也就是上文的改变节点颜色。
  4. 最后将当前节点指向下一个节点方便继续写入。

image

image

查询总的来说要麻烦一些,其实就是对树进行深度遍历;最终的思想看图就能明白。

所以在 cim 中进行模糊匹配时就用到了这个结构。

image

字典树的源码在此处:

https://github.com/crossoverJie/cim/blob/master/cim-common/src/main/java/com/crossoverjie/cim/common/data/construct/TrieTree.java

其实利用这个结构还能实现判断某个前缀的单词是否在某堆数据里、某个前缀的单词出现的次数等。

总结

目前 cim 还在火热内测中(虽然群里只有20几人),感兴趣的朋友可以私聊我拉你入伙☺️

image

再没有新的 BUG 产生前会着重把这些功能完成了,不出意外下周更新 cim 的心跳重连等机制。

完整源码:

https://github.com/crossoverJie/cim

如果这篇对你有所帮助还请不吝转发。

image
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容