最后
很多程序员,整天沉浸在业务代码的 CRUD 中,业务中没有大量数据做并发,缺少实战经验,对并发仅仅停留在了解,做不到精通,所以总是与大厂擦肩而过。
我把私藏的这套并发体系的笔记和思维脑图分享出来,理论知识与项目实战的结合,我觉得只要你肯花时间用心学完这些,一定可以快速掌握并发编程。
不管是查缺补漏还是深度学习都能有非常不错的成效,需要的话记得帮忙点个赞支持一下
整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~
涉及客户端架构升级,必然会带来一些新的用户体验,需要管理好存量用户的预期。本次重构范围大,产品质量不下降既是要求也是挑战。
产研团队较新,对原有业务功能缺乏足够了解
业务研发团队很依赖领域专家的业务知识指导,子领域间和模块间的职责和边界划分,数据归属等理解需要建立在业务理解的基础上。这些对现有团队是个不小的挑战。
因此,抓主要矛盾,分阶段小步快跑是本次重构的基调。
三、纾困:解决问题
=========
仅仅从技术层面做重构只能解决眼前的技术问题,随着业务快速迭代,纯技术重构的成果很容易消失殆尽。考虑到需要对业务和技术层面双管齐下做出改变,在现有复杂业务基础上仍能保持高效的产研交付效率,加上隔壁兄弟团队之前在线索管家产品已经收获了 DDD 改造的收益,因此本次技术重构决定结合 DDD 来做,从产品到技术来一次认知升级、架构升级。
3.1 定位:确定产品方向及核心痛点
产品定位及差异价值
产品定位:选择『不做什么』更加重要
-
聚焦在售前接待场景,帮助商家获取联系方式,不做售后服务场景;
-
聚焦在广告营销场景,帮助广告主接待推广流量并优化效果;
-
由于是 ToB SaaS 模式,所以暂时聚焦企业客户需求,不做平台型针对企业的上层需求。
产品使用角色:谁是我们的用户?
- 聚焦在B端客服角色。剥离其他角色相关功能,比如跟进线索的名片功能归到线索管家模块(销售角色),反哺功能归到 oCPC 反哺模块(SEM角色)。
差异化价值:客户为什么会选择我们?
-
全链路闭环:从推广开始到访客进站、对话、留资,直至标记会话反馈oCPC目标,全程无缝衔接;
-
与线索管家结合:智能识别会话和留言板中的线索信息,自动沉淀至线索管家,有效节省线索梳理工作;
-
智能营销:访客意图智能分析识别,千人千话引导访客开口留资;
-
多端共用:支持 Web、App、PC 端同时使用,随时随地实现沟通。
3.2 分析:识别核心领域和模块,拆解业务逻辑
3.2.1 事件风暴:剖析流程和对齐认知的好帮手
针对主要业务流程,产研团队通过事件风暴的方式梳理了事件流,定义了每个事件相关的角色、动作、规则条件和事件结果。最重要的是对齐了团队的业务认知,靠集体智慧剖析了整体业务细节。
3.2.2 边界是合作的基础:划分领域和模块,形成统一语言
根据产品定位及产品价值分析,结合梳理好的业务流程,需要划分子领域,相应配比合适的资源投入。
【核心域】
-
访客域和客服域属于核心域比较自然,同时作为底层的基础能力,协议连接域包括tcp、websocket、http、long polling协议,协议报文格式,连接状态维护等也应该是核心域。其次会话域也是核心域,互发消息才算进入真正沟通,会话内容里的意图表达和留资才是沟通的主要目的;
-
核心域的策略是围绕产品价值,重点投入资源。尽可能把非核心功能从核心域剥离,警惕容易引起团队失焦的投入。
【支撑域】
-
数据分析域是必要的功能但目前还不是重点,线索域对沟通来说是后链路必经环节,但应该更多利用爱番番线索管家的能力。广告域包含访客推广信息解析,会话效果反哺,照理是核心能力。但这里划为支持域是因为关键的能力在搜索团队已提供,沟通团队做好数据接入和数据供给工作;
-
支撑域的策略是尽可能以较少资源建设必要能力。当然,随着业务的发展支撑域也可能在未来变成核心域。
【通用域】
-
账号权限功能是大多数系统的通用能力。访客场景属于ToC场景,会遇到黑产流量攻击,包括访客进站和访客发送消息需要引入风控反作弊能力。爱番番沟通主要借助了爱番番策略团队和厂内安全部的能力;
-
通用域的策略是尽可能不亲自建设系统,借助外部能力快速完成能力建设。
3.3 架构:搭建整体技术架构
架构目标及设计要点
-
根据流量南北向把各种服务按照职责类别分为多个层次,用户界面、接入网关、业务前后台、沟通协议连接等5层由沟通团队建设维护,底下基础服务和存储层主要借助基础技术能力。分层建设能够定义服务不同等级、高效使用团队研发资源、承接不同流量类型(实际用户流量、后台用户流量、异步调用流量、定时任务流量等)、简化请求涉及的数据链路、根据层次不同建设非功能性需求(技术栈选择、熔断限流、弹性伸缩等)。
-
技术架构匹配业务架构。服务模块边界符合业务边界。核心服务内需设计领域模型,围绕领域层和应用层构建业务逻辑,搭建DDD四层分层架构,做到领域模型和技术细节分离,不稳定实现依赖稳定实现。
-
符合典型微服务架构。服务职责内聚,服务和数据一体。数据归服务私有,服务间不共享业务逻辑,服务间通过API或领域事件进行协作。
-
数据架构合理。尽可能采用数据最终一致性策略。每种数据非必要不多处存储,多处存储须有最终一致性方案保证。涉及nosql类存储如Redis、HBase、ES(Elastic Search)时,防止大key造成分片不均,业务数据按需进行分库分表存储。
3.4 突破:架构设计的关键技术
3.4.1 落地真正的微服务架构
随着子领域和模块的划分确定后,需要调整对应的模块职责及模块间协作关系进行改造,重点改造点包括:
合并老模块
改造前服务端有45+服务模块,服务职责划分不当,服务粒度不合适。具体表现为:
-
有些功能粒度太细,徒增维护成本,可以合并。
-
某些类似功能散落在多个服务,比如5个模块都有提供访客相关信息查询,可以合并。
-
有些服务随着老客户端的升级,功能改造后更合适合并到其他服务,原服务可以下线。
-
反向代理层职责划分不合理导致服务集群太多,绝大部分可以迁移至公司级的 BFE 进群,少数包含很多 lua 逻辑 Nginx 集群暂时保留,但可以合并。
经过合并下线改造后,服务数量减少了 15+。
拆分新模块
有些功能很重要,需要形成独立的模块重点建设。比如:
-
访客广告信息解析服务。广告信息对于客服刻画访客画像,理解访客非常重要。但之前的解析逻辑散落在多个模块且实现不统一,解析准确率不高,没有足够的补偿策略保证必要的解析成功率。
-
机器人智能回复服务。这也是产品定位的一个差异化价值。为了让客服更高效接待访客,引导访客多留资,这块的产品演进越来越多,复杂度也随着加大。
-
线索服务。这里的线索服务是爱番番沟通和线索管家产品的边界,主要是针对会话内容或者留言内容提取联系方式,然后通过接口或事件的方式流转到线索管家,同时也要形成咨询到线索的闭环数据。
模块间不共享业务逻辑
改造前的后端业务服务不是真正的微服务,虽然都是独立部署,各自暴露接口,但服务实现层耦合严重:
-
通过公共库( 即 java 的 jar 包 )共享业务逻辑。同一段业务代码被多个业务服务依赖,既降低了代码可维护性,也降低了服务的可测试性。
-
通过缓存( Redis )传递数据。一个 redis key 经常既有多个服务在写入,也有多个服务在读取。
-
通过 DB 共享数据,直接读取属于其他服务职责的数据表。
改造原则:不共享包括业务逻辑的公共库,让微服务垂直划分,相关业务数据(包括缓存数据)归服务私有,通过 API 接口提供能力,或者通过领域事件推动下游流程。
最终一致性前提下的高可用性
可用性的关键手段是数据复制。可以借助不同的数据同步方法,结合不同特点的存储类型完成多样化业务场景的高可用性。常用的数据复制/同步手段有:
-
发布/订阅模式:上游服务利用消息队列把相关数据以消息为载体发,下游服务订阅该消息并做相应的持久化。整个沟通服务端在大量使用这种方法,也是服务解耦的一大利器。
-
CDC 模式( Change Data Capture ):简单说就是通过监听 MySQL 的binlog 感知到上游服务的数据变化(包括新增、更新、删除),解析日志并做一些处理(比如关联表查询等)后发送到消息队列,下游按需订阅处理。
CDC 模式和发布订阅模式配合使用能满足很多场景,分离读写服务和选取异构存储介质。比如访客进站记录写入 MySQL 和访客历史记录查询ES,会话写入 Table 和会话分析服务查询 Doris 。即能有效满足各自场景的数据存取需求也能提高场景的可用性。
当然,这种可用性往往会牺牲一定时效性内的数据一致性,需要根据实际业务场景做出权衡。根据经验判断在马上得到答案和得到正确答案之间,大多数人更想要的其实是马上得到答案。
3.4.2 数据链路治理
改造前主要场景包括进站、离站、自动回复、会话内容校验、线索识别、结束会话等的数据流的必经节点是实时计算服务,其核心实现是 storm,但因为多种原因该集群很不稳定,会引发出上述提到的大量客户问题。深层分析现状主要有以下弊端:
-
storm 拓扑设计不合理,拓扑节点职责不清;
-
拓扑节点中存在大量的业务逻辑,普遍利用 redis 传递数据,redis 键设计混乱,可维护性很差;
-
storm 集群是几年前引入的,版本低,一直没升级。
经过分析业务需求,只升级 storm 集群版本不会解决实际问题,另外实时计算框架在现阶段不是必须项,因此得出了以下改造思路:
-
去除这个集中式的计算集群,按业务场景梳理各自数据流,避免互相干扰。让对应业务服务模块承接业务逻辑,如需提高业务响应可通过缓存集群加速;
-
服务模块间尽可能通过异步方式( kafka 消息队列 )传递数据,目前消息队列也能达到近实时效果,同时增强消息队列的灾备功能和订阅情况监控;
-
访客一段时间不说话需要自动回复等延时场景通过延时任务的方案解决;
-
redis key 重新梳理,优化大 key( 一个 key 承载的内容特别大,比如一个key 就包含全系统访客的部分信息,这样的 key 设计显然太大 ),尽量不跨服务模块直接操作 redis。
业务程序的灵魂是数据,技术架构时要多花时间考虑数据存储和读取的方方面面。比如用什么存储系统( 存储系统不可能读也最快,写也最快,需要权衡 )、什么时候用缓存,整个业务流程的数据传输链路应该怎么样,沟通系统涉及到很多写放大还是读放大的权衡等等。本次重构也涉及到了这些方面的梳理和改造,在此不一一介绍。
3.4.3 沟通协议优化
为什么要做协议优化?
针对 1.2 章节中提到的客户端上经常出现丢访客,消息不上屏等问题,简单的打补丁方式已经难以将问题彻底解决,因此必须从协议层进行彻底的改造优化。详细痛点如下:
-
现有协议缺乏鲁棒性,从协议层面埋藏着隐患。一个事件(如进站、建立沟通、离站)需要多个包来完成交互,如果一个访客操作频繁,访客状态也会频繁做变更,很容易出错。
-
富客户端模式,端上维护了过多的状态信息,过度依赖推送包的顺序,而且缺乏容错、自恢复恢复机制,容易出现访客不展示,消息不上屏等问题。
如何优化?
-
通知模块采用分布式锁控制并发,并为报文增加SeqId来确认早晚顺序,为客户端提供判断依据。
-
优化状态协议,简化掉动作通知类报文,采用以访客状态为主的报文,如下图所示,将动作报文简化掉,只保留状态报文,报文数量减少约 60%,降低客户端处理复杂度,减小出错概率。
- 客户端侧,由 socket 长连接改为为 http + socket 推拉结合的方式,当断网重连、或者报文丢失、错乱时,则客户端主动拉取最新状态,彻底接解决访客状态不对,消息不上屏等问题。
猜你想问:
1、上面提到分布式锁控制并发,会因锁竞争而增加请求处理时间吗?
答:锁粒度为单个访客粒度,粒度足够小,而且同一个访客在快速操作( 如频繁快速打开页面、发起沟通 )时,才会出现锁竞争的情况,对单访客来说,常规的操作并发不大。
2、既然协议优化收益这么搞,为什么不早点做协议优化呢?
答:之前受限于业务边界划分不清晰,访客状态变更散落在业务前台、业务后台、原 storm 集群多个地方,无法做统一管控。只有在完成了前期建构优化、数据链路治理完成之后,站在原有的工作成果至上,才能做协议优化。
3、客户端的推拉结合为什么不早点做呢?
答:如前文 2.1 中第 2 条所说,客户端技术栈基于 C++,只能艰难维护,无力承接新功能需求。因此想改动客户端的协议,可谓异常艰难,这也是下文 3.5 章节客户端架构升级的一大原因。
小结
-
访客、客服、会话管理模块的 DDD 改造。
-
由贫血模型改为富血模型,通过状态机控制状态变更。
-
客户端请求以 http 为主,同步得到返回值,降低出错概率。socket 主要用于给端上的通知。
-
协议包简化, 以访客状态维度进行交互,极大减少包的数量。
3.4.4 去除自运维中间件
如前面所述由于历史技术栈原因爱番番沟通团队内部运维了好几种中间件,先不说引入这些中间件的正确与否,现状是没有足够知识储备,既给系统带来了很多不稳定因素,也降低了团队的研发效率。因此本次重构在这个方面的改造原则是优先考虑下线架构中不必要的中间件,必要的中间件也不另行维护,迁移到部门基础技术团队运维。
集群改造下线
-
Zookeeper 集群:改造前主要用来做业务配置中心,迁移到 k8s 更友好的ConfigMap( 由基础技术团队运维 );
-
Nginx 集群:改造前有好几套反向代理集群,其中既有路由转发逻辑,也有业务逻辑。业务逻辑下沉至对应的 gateway 服务,由团队维护。路由转发逻辑迁移至 bfe 集群,由基础技术团队统一运维;
-
Storm 集群:逻辑改造,下线。细节上面已交代;
-
Solr 集群:下线,相应查询逻辑改造迁移至 ES 集群。
集群迁移
此部分集群虽然不能下线,但团队内不另行维护,转而迁移至部门集群。包括Kafka 和 Prometheus 集群。
3.5 扩展:客户端架构实践
3.5.1 客户端跨平台架构
随着原客户端维护代价越来越大,结合客户对 mac 端的诉求,因此选择了跨平台的 Electron 框架。
为什么选择 Electron ?
-
开源的核心扩展比较容易。
-
界面定制性强,原则上只要是 Web 能做的它都能做。
-
是目前最廉价的跨平台技术方案,HTML + JS 的技术储备,而且有海量的现存 UI 库。
-
相对其他跨平台方案( 如 QT GTK+ 等 ),更稳定,bug 少, 只要浏览器跑起来了,问题不会太多 。
-
方便拓展,可以直接嵌入现有 web 页面。
Electron 系统架构
爱番番前端团队的技术栈是 Vue,所以我们选择使用 Electron-Vue 来搭建项目。Electron 有两个进程,分别为主进程( main )和渲染进程( renderer )。主进程中包含了客户端自动更新、插件核心、系统 API 等。渲染进程是 vue + webpack 的架构,两个进程间通过 ipc 进行通信。
爱番番客户端主要是IM业务,所以通信方面使用 websocket 来进行消息通知,由于客服发送消息包含样式设置,所以传输内容包含富文本,这样就很容易引起一些xss 问题。我们使用 xss 白名单的方式来过滤 xss 攻击,并且所有内容都会通过策略过滤,拦截黄反等不良文本。
爱番番沟通考虑到今后能更灵活地接入更多业务垂类并且支持第三方自主开发个性化功能。同时需要兼顾平台代码的稳定性和易用性,我们采用了插件化架构的方式来实现客户端。
开发中遇到的问题
Electron 带来很大便利的同时,其本身也有很多硬伤。如常被人吐槽的内存占用高、和原生客户端性能差异、API 系统兼容性问题等。这些问题在开发过程中需要提前考虑到。下面是开发过程中必然会遇到的几个问题。
1、性能优化
性能优化是在开发完需求功能后经常需要考虑的。在 Electron 中,最好的分析工具就是 Chrome 开发者工具的 Performance ,通过火焰图,JS 执行过程的任何问题都可以直观的看到。
2、Window7 系统下白屏问题
因为在测试过程中 QA 同学使用的一直都是 Win10 的系统,所以白屏问题一直没有被发现。直到客户端正式上线,白屏问题被集中反馈,至此我们开始重视白屏问题并积极解决。
由于我们使用的 electron 版本是 9.x 的版本,该版本下默认开启 GPU 加速,但是 Win7 下启用 GPU 加速需要管理员权限,如果没有管理员权限去执行的话进程就会卡住,导致首页白屏。所以解决此问题方法就可以从两方面解决,第一是开启管理员权限,第二是关闭 GPU 加速。考虑到客户端使用的人群大部分是客服,公司电脑配置较低且一般没有管理员账号权限,所以我们选择通过关闭 GPU 加速( app.disableHardwareAcceleration() )来解决次问题。
3、其他问题
在 Electron 开发过程中还要注意一些常见问题。如读写文件的编码问题,客户端安全问题存在 rce,可被任意执行命令,内存使用率过高问题等。
3.5.2 微内核/插件化架构
什么是插件化架构
插件化架构就是软件本身只提供插件运行时的核心( pluginCore ),并为插件运行时提供访问的接口( pluginAPI ),通过插件平台下载插件( plugin )后可以在软件上完美运行。
最基本的例子就是 webpack,作为主流的构建工具,webpack 只抽象了一个软件运行时的环境,在不关心和改动这个系统已有的代码前提下,却能独立开发新的插件来充实整个系统的能力。
pluginCore: 插件运行时核心;pluginAPI:为插件运行提供访问接口;plugin:实现具体功能的插件。
插件化架构优势
插件化架构是开闭原则在跨系统级别的最佳实践。在插件核心和接口不变情况下,系统可以持续接入新插件,来丰富系统的功能。在一个非插件化的系统中,随着功能模块的增多,代码量激增,在引入新功能和修复BUG都会越来越困难和低效。但插件化架构不管已有系统功能多复杂,开发新的功能的复杂度始终一样。而且随着系统的平台化,第三方接入差异化功能也不会影响系统的稳定性。
爱番番插件化现状
为了满足其他第三方平台的定制化需求,如电商平台的商品及订单模块,CRM平台的客户模块,售后场景中的评价模块,爱番番客户端的插件化架构的设计要点:
- 插件化架构方案
-
提供两种接入方式:JS-SDK 接入、Webview 方式嵌入。
-
第三方插件与爱番番客户端存在两种通信机制:事件广播、实例注入。
-
番番客户端插件分类:左侧菜单插件、会话工具栏插件、会话侧边栏插件。
-
插件配置文件说明:
{
“version”:“0.0.1”, // 版本号
“id”:“demo-name”, // 绑定事件ID
“name”:“组件名称”, // 插件名称
“viewUrl”:“”, // 如果是菜单插件需要提供webview地址
“target”:“toolbar”, // menuList——菜单插件、toolbarList——沟通区插件、infoList——右侧工具栏插件
“dependent”: {
“method”: [],
“version”:“1.0.6” // 依赖客户端版本
}
}
复制代码
四、欢喜:解决效果
=========
4.1 产品架构升级
总结
我个人认为,如果你想靠着背面试题来获得心仪的offer,用癞蛤蟆想吃天鹅肉形容完全不过分。想必大家能感受到面试越来越难,想找到心仪的工作也是越来越难,高薪工作羡慕不来,却又对自己目前的薪资不太满意,工作几年甚至连一个应届生的薪资都比不上,终究是错付了,错付了自己没有去提升技术。
这些面试题分享给大家的目的,其实是希望大家通过大厂面试题分析自己的技术栈,给自己梳理一个更加明确的学习方向,当你准备好去面试大厂,你心里有底,大概知道面试官会问多广,多深,避免面试的时候一问三不知。
大家可以把Java基础,JVM,并发编程,MySQL,Redis,Spring,Spring cloud等等做一个知识总结以及延伸,再去进行操作,不然光记是学不会的,这里我也提供一些脑图分享给大家:
希望你看完这篇文章后,不要犹豫,抓紧学习,复习知识,准备在明年的金三银四拿到心仪的offer,加油,打工人!
mat,png)
总结
我个人认为,如果你想靠着背面试题来获得心仪的offer,用癞蛤蟆想吃天鹅肉形容完全不过分。想必大家能感受到面试越来越难,想找到心仪的工作也是越来越难,高薪工作羡慕不来,却又对自己目前的薪资不太满意,工作几年甚至连一个应届生的薪资都比不上,终究是错付了,错付了自己没有去提升技术。
这些面试题分享给大家的目的,其实是希望大家通过大厂面试题分析自己的技术栈,给自己梳理一个更加明确的学习方向,当你准备好去面试大厂,你心里有底,大概知道面试官会问多广,多深,避免面试的时候一问三不知。
大家可以把Java基础,JVM,并发编程,MySQL,Redis,Spring,Spring cloud等等做一个知识总结以及延伸,再去进行操作,不然光记是学不会的,这里我也提供一些脑图分享给大家:
[外链图片转存中…(img-EuqlZtev-1714888752971)]
[外链图片转存中…(img-kptt7KR6-1714888752972)]
[外链图片转存中…(img-mygoAOV1-1714888752973)]
希望你看完这篇文章后,不要犹豫,抓紧学习,复习知识,准备在明年的金三银四拿到心仪的offer,加油,打工人!