当前位置: 首页 > 科技观察

读写分离太深,你抓不住,让大叔来——命令查询权责分离

时间:2023-03-18 17:14:08 科技观察

很多年前,那时候我还年轻,在技术上宅在家.我什至希望自己能当一辈子一线程序员。但是我有两个小愿望要实现:一是赚更多的钱;二是在项目的技术栈和架构选择上拥有更多的主动权。挣钱多的原因是刚结婚,有自己的计划生育,所以挣钱的欲望比较强烈。之所以想在技术上有更多的主动权,是因为当时领导对我很赏识,有些事情也逐渐委托给我去做一些事情。尝到了甜头,所以也有了自己的一些小抱负。而就在那个时候,领导给了我一个机会,这个机会在我现在的职业生涯中似乎是相当重要的。当时广告联盟如火如荼,公司想参与进来,于是决定从头开始搭建广告平台。而我恰好也有过一些类似的开发经验,做事还算靠谱,所以领导要我担任这个系统的技术负责人。如果我能把系统做好,对我来说肯定是一个证明自己的机会,对以后实现我的两个小愿望也有好处。对我很有诱惑力。只是当上帝为你打开一扇门的时候,他总会为你关闭一扇窗。这个机会不仅给我的领导,还有另一个部门的老大。不得已,在高层会议上进行了讨论。讨论讨论的结果是借鉴当时其他公司的做法和内部竞争。两个部门做一套平台,然后上线运行一段时间。谁做得好,谁就有机会全面投资公司。好吧,机会变成了冒险。只是到了这个时候,我不能退缩了。一旦退缩,就会伤害赏识我的领导,我以后在公司的发展也会受到严重阻碍,只能仓促行事。为了赢得这次比赛,我也和这个系统的产品负责人交流了很长时间。最后要达到两个目标:1.这个系统要有尽可能多的功能,尤其是对相关业务人员。之所以这样,是因为现在是内部竞争。至于内部竞争,使用我们系统的业务人员其实是有很大发言权的,他们的满意度很可能是最终评价的赢家。同时,我们还计划为投放我们系统的广告主准备一些体验非常好的数据跟踪和分析功能,以最大限度地发挥我们产品的吸引力。2、本系统对稳定性和可靠性要求很高,有时值得为此做一些超设计和实现。这里我们想说明一下稳定性和可靠性在我们当时的场景中的意义。稳定就是要保证性能稳定,也就是说我们的系统响应时间要尽量保证在很短的时间内做出响应。而可靠性就是我们的系统要尽最大努力去保证没有错误,因为错误很可能会造成用户的流失,导致我们产品的失败。设定好目标,给出产品需求后,我和团队就进入了难度极大的开发工作。那时候,我真的付出了我的全部心血。其实我本来就是一个享受生活胜过努力工作的人。虽然之前工作很忙,但是业余时间过得很愉快。听听音乐,看看电影,有时和老婆一起找家餐厅享受美食,时不时踢一场酣畅淋漓的足球。然而,自从我开始投入开发这个广告系统后,悠闲的日子一去不复返了。记得那时候,我摇摇晃晃的下班,摇摇晃晃的回到工作岗位。我当时最大的愿望就是有一张床,躺下就再也没有人叫醒我。但即使如此努力,我还是遇到了无数的问题。这些发展道路上的硬骨头,让我的发展目标一次次调整。其中最头疼的就是高并发的性能问题。那时候,我的阅历还很浅,围绕Java的生态还不完善。可以用于宿主访问的是缓存和数据库。同时由于版权等问题,我只能选择MySQL数据库。为了解决这些性能问题,我还把MySQL官方手册打印出来,每天研究。一开始为了抵挡预期的超高并发,我采用了当时很流行的读写分离模式。但是,经过实际测试,总有各种不尽如人意的地方。最麻烦的问题之一是各种复杂查询的性能。我说为了赢得内部竞争,我们要尽可能的依靠高并发和多功能这两个目标来做这个系统。所以,为了这两个目标,这个系统其实有很多方便业务人员使用的功能,而这个功能的目标就是在高并发下保持稳定和流畅。其中,最典型的服务之一就是可以实时更新的广告投放排名功能。这个广告投放排名要求是这样的:首先,我们的用户必须能够在管理后台看到自己的广告投放排名,排名是根据消费金额、点击次数等指标进行排名的。其次,在我们的后台,我们也对业务人员做了这样一个排名。不同的是,它是全球排名,是对我们所有客户投放的广告进行综合排名。然后,这个排名必须能够根据消费金额和点击次数的变化实时变化。当然这个实时也可以做成准实时,只要延迟不要太大即可。至于本身,由于做排行榜的指标很多,需要写很复杂的SQL去数据库查询。另外,如果需要实时变化,还得不断查询数据库。而对于这种情况,不管我怎么优化,总不能得到满意的结果。如果我把这个排名缓存起来,因为这个排名需要进行各种统计和排序,从数据库中查询出来之后,需要进行各种模型转换。如果并发增加,查询又会转换,性能真的会飞速下降。那时候压力很大,无时无刻不在想着性能问题,手里的MySQL手册几乎是烂页和丢页。甚至回家睡觉的时候,我都会闭上眼睛思考如何解决这些问题。临近最后上线的时间,手上的项目却卡在这些性能问题上,难以推进。竞争对手不时听到消息,内部竞争对手在一定程度上进展顺利。我实在受不了了,心里劝自己放弃的声音越来越大。以前我觉得我是一个很有韧性的人,现在看来我只是一个普通的打工仔。我想逃避,我想跟产品商量,就这样上线,我不想管,看天理,我打赌对方也遇到我的问题,还不如像我一样。只是在即将上线产品,最终决定就这样上线的时候,内心强烈的不甘心阻止了我。我想在我放弃之前,我需要知道我的竞争对手在做什么,他们有什么计划和想法供我参考。我找遍了公司里所有的熟人,不断打听竞争对手的情况。但是结果并不好,因为对方比我强,他们进行封闭开发,警惕性很高。最后只得到一个关键词:CQRS。对方用CQRS解决了性能问题!!!我读书的时候还小。那时我没有手机。但是现在有了手机,看书的时候总是时不时分心看看手机上的资料。有时为了好好读书,我不得不故意把手机离得远一点,以免分心。而CQRS就是这种思路。这种模式与其说是一种架构模式,不如说是一种想法。CQRS认为系统中的操作可以分为两类:读和写。如果一个系统没有专门针对读写分别进行优化,那么这个系统就是两用的,就像我在用手机看书一样,各自的性能不会因为相互影响而达到最优。所以读写要具体分开,分别优化。在CQRS中,写入行为称为命令,读取行为称为查询。因为我要把他们分开,所以CQRS模式的中文翻译叫做权责分离命令查询模式。知道这套思路后,一开始并没有在意,因为乍一看,这套东西其实和我用的数据库的读写分离是一样的,就是读写分离写作。然而,我的技术直觉告诉我,事情并没有那么简单。在计算机世界里,一个词不会无缘无故出现,也不会无缘无故流行起来。如果真的和数据库的读写分离一样,那就叫数据库读写分离吧。一定有什么不同。对中文搜索结果不满意,直接去MartinFlower的网站看原版。然后,我找到了这样一个架构图。结合他的原文,一下子明白了,原来是模型,模型不一样啊!原来的数据库读写分离确实把读和写这两种行为分开了,但是它还有一件很重要的事情没有做到,那就是职责分离。什么是责任分离?意思就是读写方不要搞同一套模型。数据库读写分离的问题就出在这里,它使用的是同一个模型。这里使用同一个模型带来的问题是,这个模型既要考虑读数据不太难,又要考虑写数据不太难。而这恰恰违背了CQRS中的核心思想:读写完全自由。如果我们用CQRS的思维,假设写不需要关心读,读数据不需要关心写,双方是不是可以完全放开自己?比如写数据既然不需要考虑读,那么我可以用Json格式,XML格式等非标准格式,甚至直接写日志。而且读数据根本不需要考虑写的问题,我什至可以做成索引格式方便查找。而CQRS,在我看来,就是解决我性能问题的灵丹妙药。以广告排名问题为例。排名广告的麻烦在于,每次加载排行榜时,都需要进行非常复杂的查询才能从数据库中读取数据。如果我能把排行榜的读取和排行榜所依赖的点击、消费指标的更新完全分开,那么我困扰的排行榜性能问题就迎刃而解了。经过努力,我遵循了CQRS最初的思路,想出了这样一个设计思路:这里,数据统计就是广告排名需要的点击、消费等数据。这些数据将被放入一个单独的数据库中,只用于写入,不用于读取。那么展示广告排名的函数会直接从缓存中读取展示广告排名的模型,不需要做任何特殊的转换。复杂查询也没有问题。但是我们的需求是让广告排名能够准实时的根据点击、消费等数据自动更新,那么写数据和读数据的模型分离了怎么办呢?很多年前,当我第一次在网上购物的时候,心里有一个疑问:我下了单,卖家是怎么知道的?我必须一直盯着它看吗?直到我开发了电子商务系统,我才知道这一点。当我们下单的时候,需要给对应的商家发送一个通知,告诉商家哪个客户购买了哪个产品。所以就有了自动更新广告排名的方案,这和电商下单通知商家的道理是一样的。当写入数据时,我们复制写入的数据并通知读取数据的模型。好了,现在整套逻辑就完成了。不过,我并不急于马上将CQRS模型应用到实际项目中。因为,我发现我连CQRS模型的缺点是什么都不知道。要知道,世界上没有完美的解决方案,都是有利有弊的。而CQRS我觉得完美的解决了我的问题,说明我对这个模型的理解还是有问题的。那个时候距离约定的发射时间越来越近了,差不多还有一周的时间。我真想闭上眼睛实施计划。但是,不,我总是喜欢把事情想清楚,然后再做事情。我决定冒险花两天时间实现两个功能点,然后亲身体验一下引入CQRS的利弊。两天后,终于找到问题所在:引入CQRS模式后,最大的问题是引入了过多的复杂性。由于需要读写分离,我们开发的工作量无形中增加了一倍。然后引入了CQRS,它变得更加复杂。因为我们发现,对于不同的功能,只有采用不同的读写模型,才能充分发挥CQRS的优势。例如,广告排名可能会使用缓存中间件来访问现成的排名。要根据关键字搜索各种合适的广告,您可能不得不考虑开源搜索引擎中间件。每次引入都会增加开发成本、服务器成本和更多的复杂性。终于,我们的广告系统准时上线了。只是CQRS模式还没有被广泛采用。我只是对最重要的功能点使用了CQRS,我决定暂时搁置其余与性能相关的问题。之所以会这样,是因为我觉得大部分的问题其实都是我们过度设计造成的。即使我因此而失败,我也承认。不想给自己搭建的系统埋下巨大的隐患,不想给团队带来不必要的工作量。我不想以这种方式参与其中。上线之后,特别是在运营的前两个月,我很忐忑。我不知道我的妥协会不会造成很大的问题,我也不知道我所做的是否真的是正确的事情。两个系统之间的竞争在上线两个月后就开始了。这么快就得到了结果,正是因为我的对手大量使用了CQRS模式。他从设计之初就想做一鸣惊人,在他的系统中引入了七八种中间件。将大量的函数拆分成读写两部分,造成了巨大的灾难,过于复杂,让整个系统难以控制。最麻烦的是,由于引入了CQRS,他们必须通过消息传递来通信和读写两套组件。但是,当读取组件收到消息后,发现写入失败了。结果,用户看到相应的数据后,过了一段时间,发现数据与之前看到的不符。比如一开始点击次数是1000次,但是两个小时后发现是999次。这种问题每天都在发生,而且由于系统过于复杂,大大拉长了排查问题、定位问题、解决问题的时间。最后客户陆续退出,公司不得不把客户转移到我的平台上。比赛结束了,我赢了,可是我真的高兴不起来。因为今天他因为引入错误的新技术而失败,那我明天为什么不会因为误用新技术和新思想而失败呢?他今天不是我明天吗?愿天下程序员深思熟虑,万事通。当心!本文转载自微信公众号“思源外”,可通过以下二维码关注。转载本文请联系思源外公众号。