客服一站式工作台包含了在线、电话、工单和工具类四大功能模块。其中很多通用的模块,比如工单详情、订单详情都是通过iframe的形式嵌套的,在系统加载过程中会比较耗时,再加上在线消息通信模块强依赖tinode第三方SDK,很多方法都是直接调用tinode提供的API,同时也继承了tinode很多不合理的方式,从使用tinode到目前为止,因迭代资源的投入,一直没有对tinode源码做一些优化和改进,当消息通信的模式改成广播之后,会话卡顿问题就暴露出来了。
通过对tinode源码消息链路模块的阅读,发现了有不少的优化空间,本文则是针对消息链路这块阐述的具体优化实现。
2.1 消息数据处理流程存在缺陷
经过对tinode第三方SDK源码的阅读,发现其中客服在“接收”和“发送”消息的链路上有很大的优化空间,在原有的逻辑中,从发送消息到快速渲染页面再到tinode响应返回结果再去刷新渲染页面,以及客服接收到消息的时候,会对整个消息进行刷新,反序列化、排序、去重、状态处理等等,都需要多次的循环,再加上通信模式改为广播模式,大数据量循环任务,对于性能来说是个严峻的挑战。


图中红色区域有较多的for循环是耗时最多的场景,原因是要获取用户与客服沟通记录(原有tinode中提供的方式,topic.message() 会执行n次),反序列化、会话状态处理、排序、去重都会将所有聊天消息进行遍历,其中 反序列化为耗时最高的场景,如果客服跟用户之前的聊天消息越多,遍历次数就越多,耗时就越久,再加上JavaScript是单线程,遍历次数多了就会形成阻塞,导致客服在快速切换会话的时候,循环还未结束,页面未渲染完成,就出现卡顿现象。
每一位用户从客户端进线到坐席客服工作台的时候,会生成的一个会话id(sessionId),每一个会话id下面的每一条人工消息中都会有一个消息id(msgid)客服在跟用户之间来回沟通的消息回合比较多,为了减少“老代码”中多次循环降低性能的操作,想到最核心的任务就是尽量避免去遍历聊天的消息数据(因为消息太多了),遵循能不遍历聊天消息就不遍历的原则,对于原逻辑中的“去重”和“排序”逻辑做了重写,这个时候,上面提到的会话id和消息id就起到了非常重要的作用。
3.1 去重
本次优化方案中采用全局维护一个msgidCacheMaps Map数据结构,这个数据结构有两个维度,sessionId 和 msgid ,用来保存当前会话(sessionId)中每条消息的msgid,消息对话中,人工客服发送的消息会经历从虚拟消息到真实消息两个阶段(这里的的虚拟消息指的是在人工会话中,客服向网关发送消息后,为了快速让消息展示在聊天区域,通过前一个消息seq + 0.002生成虚拟的seq即:virtualSeq,等到网关返回真实的seq后,再将virtualSeq替换成真实的seq),虚拟消息阶段会保存msgid到Map中,对于系统推送的消息,没有msgid,不需要经历这个过程,直接放进会话池,真实消息(tinode返回seq)阶段,根据msgid到msgidCacheMaps Map数据结构中进行查询,存在此msgid,说明是重复数据,配合seq进行替换即可。
3.2 排序
本次的优化方案是采用 二分查找插入排序的方法,全局维护一个seqCacheMaps Map数据结构,这个数据结构跟上面去重有些类似,也有两个维度,sessionId和seq,二分查找插入排序的方法,用seq(真实seq)和virtualSeq(虚拟seq)作为查找的依据,每次消息进来,根据二分法快速找到当前seq可插入位置,虚拟消息阶段,直接插入,真实消息阶段(msgidCacheMaps存在此msgid),直接替换,但是这个时候遇到一个问题,因为在人工会话过程中客服向用户发的每一条消息都会在网关进行敏感词校验,没有触发到敏感词就会将消息发送到客服端展示给用户,如果触发到敏感词,含有敏感词的消息就会被网关拦截,消息也不会到达用户侧,此时网关也不会返回seq,那么没有返回seq,又该如何处理呢?那就是在tinode返回阶段,会把前面virtualSeq替换为前一个消息seq + 0.002,确保其位置有序不会错乱的展示在在聊天区域。

3.3 缓存回收(结束会话销毁)
在上面 去重 和 排序 中提到,为了减少遍历次数,全局维护两个数据仓库(msgidCacheMaps Map数据结构、seqCacheMaps Map数据结构), 但是每位客服每天的会话量在100+,再加上每条会话中客服和用户的来回消息数约40+,如果客服再去查看历史消息,一页20条,如果只存不删,存储的数据量还比较庞大的,容易导致内存溢出,那么什么时候删除比较合适呢?根据业务情况,最后选择在结束会话、会话转接、推送离线的情况下会对挂载在全局的hash map进行销毁,释放内存。

3.4 消息状态
这里的消息状态指:已读、未读、已接收、发送中、发送失败等。
在客服和用户沟通过程中,客服侧和用户侧所展示的消息状态都是实时更新的,客服发送消息给用户,当用户读了这条消息后会返回info协议(推送已经消息通知)告诉h5侧该条消息已读,然后h5侧对该条消息进行状态更新。
原处理方式:当客服给用户发送消息后,对当前会话中这个用户的所有历史消息进行遍历,进行全部重置操作,这个时候如果遇到用户与客服沟通的消息很多的情况,就会导致遍历次数多,产生严重消耗性能等问题。
优化方案:先过滤掉历史消息和非客服发送的消息,通过二分法的方式去找到该消息,然后直接改变状态。在收到用户发送的消息后,对messagePools(当前用户所有会话)中的客服发送的消息倒序进行状态更新为已读,因为既然用户都发消息过来了,说明客服发送的消息已经被阅读过了,就不需要按照之前老的逻辑再去给每个消息都遍历去设置状态了,浪费性能,除发送中和发送失败的消息外,全部渲染为已读。
发送消息:目前发送消息只会执行2次,第一次会快速将消息展示到沟通页面,然后再进行消息的发送(wss),当收到ack后,会进行二次消息状态更新,只通过msgid会找到需要更新的消息进行更新,不再需要利用 tinode 提供的topic.message方法进行全量遍历了;
接收消息:客服接收用户消息只会触发一次消息更新,不需要再对当前用户的全量数据进行遍历更新新状态了,同时也会回ack;
具体实现:客户端推送长链note事件告诉H5,H5侧记录已读的这条消息的seq,对于小于等于seq的客服发送的消息数据进行状态更新,即:recv(已接收) => read(已读)。
3.5 敏感词拦截处理
IM聊天页面在用户进线后,对于用户和坐席客服之间发送的消息会进线敏感词监控(仅监控 和 禁止发送)。
原方案:坐席客服在编辑消息后,点击发送,调用后端敏感词接口,没有触发到敏感词校验通过后才能发出去,如果出现网络波动接口返回慢的时候,就会让客服感觉发消息卡一下才能出去的情况。
优化方案:通过网关拦截,客服发送消息的时候,直接渲染到聊天区域,网关去检验发送的消息是否触发到敏感词,如果有触发到敏感词,那么网关会返回一个状态告诉h5,h5再根据返回的结果更改状态去提示客服。

优化链路技术方案实现整体在某天发布上线,所以以发布当天为时间截点,拉取了优化前后的数据对比,具体如下所示。
4.1 优化前

如上图所示,统计了某一时间段的总进线的两个数据指标:
平均首次响应时长:8.40秒
平均响应时长:19.9秒
4.2 优化后

如上图所示,统计了某一时间段总进线的两个数据指标:
平均首次响应时长:6.82秒,比优化前减少了1.58秒
平均响应时长:18.22秒,比优化前减少了1.68秒
一般来说 IM 产品的用户量和活跃度通常都很大,在一些特殊的时间点经常容易造成流量的波峰,因此技术上需要能够应对突发的量级,同时IM一般主要包含这4个特点:实时性、可靠性、一致性、安全性,对于IM的优化还有很长的路要走,在保证业务稳定情况下,后续我们也会围绕着四个特点继续努力打磨,让符合得物自己的IM SDK越来越完善,形成行业消息通信的标杆。
*文/余波
关注得物技术,每周一三五晚18:30更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
目录H2数据库入门以及实际开发时的使用1.H2数据库的初识1.1H2数据库介绍1.2为什么要使用嵌入式数据库?1.3嵌入式数据库对比1.3.1性能对比1.4技术选型思考2.H2数据库实战2.1H2数据库下载搭建以及部署2.1.1H2数据库的下载2.1.2数据库启动2.1.2.1windows系统可以在bin目录下执行h2.bat2.1.2.2同理可以通过cmd直接使用命令进行启动:2.1.2.3启动后控制台页面:2.1.3spring整合H2数据库2.1.3.1引入依赖文件2.1.4数据库通过file模式实际保存数据的位置2.2H2数据库操作2.2.1Mysql兼容模式2.2.2Mysql模式
2022年底,OpenAI的预训练模型ChatGPT给人工智能领域的爱好者和研究人员留下了深刻的印象和启发,他展现的惊人能力将人工智能的研究和应用热度推向高潮,网上也充斥着和ChatGPT的各种聊天,他可以作诗、写小说、写代码、讨论疫情问题等。下面就是一些他的神回复:人命关天的坑: 写歌,留给词作者的机会不多了。。。 回答人类怎么样面对人工智能: 什么是ChatGPT?借用网上的一段介绍,ChatGPT是由人工智能研究实验室OpenAI在2022年11月30日发布的全新聊天机器人模型,一款人工智能技术驱动的自然语言处理工具。它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动
我希望Ruby的解析器会进行这种微不足道的优化,但似乎并没有(谈到YARV实现,Ruby1.9.x、2.0.0):require'benchmark'deffib1a,b=0,1whileb由于这两种方法除了在第二种方法中使用预定义常量而不是常量表达式外是相同的,因此Ruby解释器似乎在每个循环中一次又一次地计算幂常数。是否有一些Material说明为什么Ruby根本不进行这种基本优化或只在某些特定情况下进行? 最佳答案 很抱歉给出了另一个答案,但我不想删除或编辑我之前的答案,因为它下面有有趣的讨论。正如JörgWMittag所说,
我正在尝试从数据库中读取大量单元格(超过100.000个)并将它们写入VPSUbuntu服务器上的csv文件。碰巧服务器没有足够的内存。我正在考虑一次读取5000行并将它们写入文件,然后再读取5000行,等等。我应该如何重构我当前的代码以使内存不会被完全消耗?这是我的代码:defwrite_rows(emails)File.open(file_path,"w+")do|f|f该函数由sidekiqworker调用:write_rows(user.emails)感谢您的帮助! 最佳答案 这里的问题是,当您调用emails.each时,
文章目录前言约束硬约束的轨迹优化Corridor-BasedTrajectoryOptimizationBezierCurveOptimizationOtherOptions软约束的轨迹优化Distance-BasedTrajectoryOptimization优化方法前言可以看看我的这几篇Blog1,Blog2,Blog3。上次基于MinimumSnap的轨迹生成,有许多优点,比如:轨迹让机器人可以在某个时间点抵达某个航点。任何一个时刻,都能数学上求出期望的机器人的位置、速度、加速度、导数。MinimumSnap可以把问题转换为凸优化问题。缺点:MnimumSnap可以控制轨迹一定经过中间的
我对为我的RubyonRails3.1.3应用优化我的Unicorn设置的方法很感兴趣。我目前正在高CPU超大实例上生成14个工作进程,因为我的应用程序在负载测试期间似乎受CPU限制。在模拟负载测试中,每秒大约20个请求重放请求,我的实例上的所有8个内核都达到峰值,盒子负载飙升至7-8个。每个unicorn实例使用大约56-60%的CPU。我很好奇可以通过哪些方式对其进行优化?我希望能够每秒将更多请求汇集到这种大小的实例上。内存和所有其他I/O一样完全正常。在我的测试过程中,CPU越来越低。 最佳答案 如果您受CPU限制,您希望使用
美团外卖搜索工程团队在Elasticsearch的优化实践中,基于Location-BasedService(LBS)业务场景对Elasticsearch的查询性能进行优化。该优化基于Run-LengthEncoding(RLE)设计了一款高效的倒排索引结构,使检索耗时(TP99)降低了84%。本文从问题分析、技术选型、优化方案等方面进行阐述,并给出最终灰度验证的结论。1.前言最近十年,Elasticsearch已经成为了最受欢迎的开源检索引擎,其作为离线数仓、近线检索、B端检索的经典基建,已沉淀了大量的实践案例及优化总结。然而在高并发、高可用、大数据量的C端场景,目前可参考的资料并不多。因此
目录1古彝文与古典保护2古文识别的挑战2.1西文与汉文OCR2.2古彝文识别难点3合合信息:古彝文保护新思路3.1图像矫正3.2图像增强3.3语义理解3.4工程技巧4总结1古彝文与古典保护彝文指的是云南、贵州、四川等地的彝族人使用的文字,区别于现代意义上的彝文,古彝文指的是在民间流通使用的原生态彝文,多达87046字。古彝文的起源距今至少数千年,是世界上最古老的文字之一。对古彝文字集研究有助于理解尚未被翻译成汉文、用字尚未规范化的古籍,更深层、透彻地作用于传统文化保护。古彝文字义对照图(网络资料+邵文苑供图)古籍是不可再生的宝贵资源,应当得到妥善保护。中国的古籍在历史上迭经水火兵燹等自然灾害、
RTS在阿里云视频直播的基础上进行底层技术优化,通过集成阿里云播放器SDK,支持在千万级并发场景下节点间毫秒级延时直播的能力,弥补了传统直播存在3~6秒延时的问题,确保了超低延时、低卡顿、秒开流畅的直播观看体验。本文介绍了基于RTS超低延迟直播优化强互动场景体验的最佳实践方案,并以阿里云播放器Aliplayer为例,详细介绍RTS超低延迟拉流接入、自动降级、排障信息获取等逻辑的实现,助力企业打造互动直播行业的产品竞争力。适用场景该方案适用于对超低延迟直播有诉求的客户,尤其是业务中存在强互动场景直播的场景。强互动场景直播主要是指对主播和观众存在互动,或观众存在更高实时性观看、画面互动需求的情况,
我目前正在研究Ruby2.1.1的改进,但遇到了一些奇怪的事情。我正在尝试改进String类并定义一个名为FOO的常量。沙箱.rbmoduleFoobarrefineStringdoFOO="BAR"deffoobar"foobar"endendendusingFoobarputs"".class::FOO#=>uninitializedconstantString::FOO(NameError)puts"".foobar#=>"foobar"这给了我未初始化的常量String::FOO(NameError)。但是我可以调用"".foobar这让我相信我在正确的范围内。奇怪的是,如果我