拯救祭天的程序员——事件溯源模式
一、事前
你相信吗?曾经有一段日子,我几乎没接到过合格的产品需求。
开局几句话,技术全靠猜。
总是以为简单的需求
曾经,我从产品那里接到过这么一个需求:
“对系统的用户进行分级,不同级别的用户有不同的福利。
依然如常,无图无文档,只是这么一句话。我知道,需求一句话,分析五日功嘛。为了项目能持续发展,我只好自己分析自己搞了。
从业务上看,目前的用户对象尚无等级一说,我们先为用户对象加上个级别属性。又因为不同的用户等级,可享受到不同的福利。比如:达到 3 级的用户,可以享受购物 9.5 折优惠,物流费用全免,客服快速回复等。
所以,我做出设计如下:
首先,我把每个等级用户该享受的福利放到一个列表里。这个用来供前端展示用户当前可享受到的福利。
然后,在每一项福利中,我去设定一个可享受此福利的最低级别。只有用户的级别超过这个最低级别的时候,才可以享受到此项福利。比如,支付优惠 9.5 折,我只需要在支付服务中打包个支付权利 9.5 折这种东西,然后设定个最低级别即可。
这事儿看着是如此简单,所以,实现方案也没什么特殊的。当用户每次升级的时候,我只需要更新用户级别即可。
这个时候,需求比较初级,要求也不高。在满足升级条件后,需要用户主动点击升级。同时,再填写一些相关信息,申请一些专属的福利就可以了。
好,设计,开发,上线一条龙走起来!
需求变成坑
过了一阵子,我们的运营们勇于探索,勤于开拓,去搞了一堆资源互换回来。当我听说此事时,心里已经预感不妙了。
果然,没两天,我们的产品高高兴兴地通知我,由于兄弟团队愿意和我们的项目进行合作,因此用户的福利将得到极大的丰富,那些更加丰富的福利全都由兄弟团队提供。
所以,请我简单的搞一下,对接上这些合作方,进一步提升我们系统的粘性。
如常,依然没有任何文档,我依然只能自己分析。
现在,根据我丰富的被折腾经验,我知道开始有坑了。当我对接合作方接口的时候,他们都需要我传入一些特定的用户标识过去,可以让双方共享用户。
需求开始复杂了,不过庆幸的是,我改改代码就可以了,还好还好,我松了口气……
好,设计,开发,上线一条龙走起!
可惜,我们的业务就像一群群的蜜蜂一样,你永远不知道他们会给你带来什么样的花朵。
没过过久,产品告诉我,几个兄弟团队想和我们一起搞一次超级大活动。我觉得天黑了……
没文档没有产品原型,依然就是微信中的来来往往。
我知道此时,我得往深里想想了。需求是可以肆意妄为的,而我能阻止业务需求的肆意妄为吗?不能,所以,我要考虑一整套弹性的方案,能应对这些千变万化,又漫天飞舞的需求。
二、初见
隐患的伊始
来看看这个见鬼的大活动吧。
首先,按照设计,如果合作方们想要和我们一起大联欢,那么我们就要把用户升级的信息告诉他们。这样,合作方们才能进行验证,并提供用户级别对应的福利。所以,当我们的用户升级的时候,我需要每次都把这件事同步给我们的合作方。
又因为我们是和多个兄弟团队合作,比如,和物流团队合作,和支付团队合作。在这种情况下,不同合作方的互动逻辑是分布在不同的服务中的。
此时,我有两种方案可供选择:
1. 在用户服务里,用户升级时,立即主动的通过接口去调用分布在不同的服务上的相关逻辑,把用户升级这件事同步到合作方那里。但是,这个方案有个很大的问题——因为我们需要调用其他服务的接口,这就造成服务和服务之间耦合起来了。将来有点小改动,可能都需要我们改代码。
2. 在微服务里,其实是很推崇使用消息队列的。当用户升级时,我只需发送消息到消息队列中,然后让相关的服务去订阅这个消息即可。这个方案,使用消息队列可以解耦服务之间的关系。
因为微服务本身的目的就是解耦和灵活,并且第二个方案和我们架构是适配的,因此我选择了第二个方案。
在第二个方案中,正因为消息可以把服务之间进行解耦,所以,当用户升级的时候,我只需要操作用户服务数据库中的用户表进行升级,并把升级这事儿包裹成消息扔到消息队列中即可。
我甚至可以把更新用户表和发送升级消息到队列包装成一个事务。
好,设计,开发,上线一条龙走起!
这就是能应对后续不断变化的技术方案吗?事实证明,并不能,因为,这套方案即将会被变化的需求给彻底击垮。
问题的大爆发
斗转星移,时空变幻。需求如滚滚的流水般涌来,而我们的技术方案如同一套无论如何增强也不够健壮的大坝。
经过几度需求的变换,此时用户升级已经变成了满足条件后自动升级;我们合作的兄弟团队也日益增多;我们的服务也越拆越多……在这些汩汩涌出的变化中,问题已经如同潜伏在水底的鳄鱼,即将爬上岸来猎取几个程序员来祭天了。
问题的迹象一开始出现在用户升级的数据上。那时,我们接连被运营们提的问题所困扰。
有些运营人员发现,某些用户升级过快了,用户的升级速度已经远远超出了当初设计时预估的速度了。
而这种过快的升级不仅使得运营人员无法及时构思和设计后续的运营活动,还使得我们的运营成本快速的上涨,并因此给公司经营带来了一定的损失。
当然,如同以往一样,业务是从来不会出错的,出错的永远是技术。这不,出问题的原因都给我们安排的明明白白了:
“很可能是程序出了 bug,因为出了某些技术性的故障,导致用户升级的时候没有一级级的升上去,出现了跳跃性的升级…………
在追踪问题的时候,我们猛然发现了这个技术方案的一个缺陷:由于根本没有预料到用户升级的重要性,我们的很多用户升级相关的日志并未开启,并且没有存储任何用户升级的历史记录。
这瞬间成了一笔糊涂账,我无 fuck 可说。
雪上加霜的是,又有用户们投诉,他们总是在某些时候会出现一些卡顿。我们再一查,发现是用户升级导致的数据库问题。
最早的设计是用户升级直接更新数据库表,但是大意了:
当用户数量出现大涨的时候。 新用户初期升级难度小,所以升级很频繁。
忽略了这两个因素,这就造成了我们的数据库有点承受不住这种频繁的更新。
而且,在查这些问题的时候,以前有些用户投诉的问题也随之被挖了出来。比如,用户升级后有些福利却没有给他们,悲催的是这些痕迹也没有被完整的留下来……
糊涂账加糊涂账成了笔烂账。
啊,我要被祭天了吗?
跺脚后智商重新占领高地
现在来看看我们要面临的问题吧。
首先出场的是用户升级没法追根溯源的问题。因为我们每次用户升级,需要通知相关的服务,然后还得保证每个相关的服务处理成功了,到此时,用户升级才算真正的成功。所以,为了能还技术们一个清白,能别搞得成为烂账,就必须把用户的每次升级给记录下来,并且还得把每个相关服务对升级事件的处理也记录在案。
下一个要解决的小兄弟是数据库更新的问题。这个数据库更新该怎么办?缓存后同步?那缓存本身的更新出现了问题怎么办?验证呗!怎么验证?每次升级时候去和历史记录核对一遍吗?
这时候,我的脑袋里开始进入了混沌状态。不知道该怎么办了。
有点着急啊,怎么办呢?只好去看看网上有没有什么方案可以提供一些思路。
最终,这就促成了我对事件溯源(Event Sourcing)模式的初见。
当我看到事件溯源的时候,我脚一跺,我感觉我的智商回来了。
事件溯源拯救快被祭天的我
首先,咱们看看事件溯源是什么样的。
以咱们现在搞得用户升级为例,说一下事件溯源模式:
用户升级时,我们只需要把用户升级这件事通过 Event Store 这个中间件传给支付服务、物流服务等这些相关的服务。然后,支付服务、物流服务之类的处理完用户升级通知给他们的事件后,会也创建一个事件对象,放到 Event Store 里。
这里的 Event Store 其实主要是用来做两件事:
传递事件 存储事件历史
那么,事件溯源是怎么来搞定我面临的这些问题的呢?
首先,如果我们要追根溯源,就需要把用户升级和用户升级后相关服务做得处理都要存起来,形成一个完整的业务链条。有了这个链条,才能被称为追根溯源。
事件溯源模式正好告诉大家,有事儿就要存起来!
其次,当我们用户升级的时候把事件存储下来之后,我们还需要实时去更新级别吗?
我们来分析一下:用户升级的真正目的是什么?从业务角度来说,其实就是通过提供各种福利去提升用户的活跃度。那么,这件事需要实时吗?似乎不必须,因为用户几乎不太可能升级后马上去使用对应的福利。
好,如果可以不实时,那么用户升级这件事儿就能避免实时更新数据库了。
如果我们在开始把历史事件存储下来了之后,其实可以在凌晨的时候去定时根据用户级别发生的事件,去把用户的级别升级到正确的级别。
所以可以看到了,事件溯源在这事儿上把我的两个问题全解决了。
这就是我和事件溯源模式的初见。而在今后的技术生涯中,它将会经常陪伴着我。
三、认识
真正认识下事件溯源模式吧
事件溯源总结下来其实只有如下二个核心特点:
1. 把触发业务数据变化的原因包装成了事件对象——如果把这件事儿抽象的看待一下,就是我们可以把业务中任何需要注意的情况发生变化时,都可以包装成事件。
2. 这些包装成事件的业务数据会按照事件发生的顺序,被持久化存储到专门的地方——需要专门说一下这个事件按照顺序存放的问题,在事件溯源模式中,按照事件发生的顺序持久化存储是非常重要的一件事。如果一个模式中的事件没有严格按照事件顺序进行持久化存储,其实很难说这个模式会是一个合格的事件溯源模式。
所以事件溯源模式就做了两件事:
定义什么样的业务逻辑可以被定义为事件; 把定义好的事件在发生后给按顺序记录下来。
事件溯源常伴吾身
认识到了事件溯源的核心特点后,我在后面的开发生涯里反复的使用了这个模式去帮我解决不同业务的特定场景的问题。比如订单的状态更新,再比如秒杀活动的性能问题。
在不断地使用事件溯源过程中,我总结出了需要使用事件溯源的一些场景。当遇到类似的场景时,我总是会第一时间尝试用事件溯源模式来解决问题。
这些场景是:
想知道关键数据被更改时,意图、原因或者目的时;
更新数据确实性能出现了问题,一时之间也没办法通过硬件升级或者大规模集群去解决这个问题;
还原某些现场,或者想通过一些数据重复的还原线上环境是非常重要的事情;
而事实证明,在这些场景中使用事件溯源也确实不负我望,并且还带来了很多额外的好处:
1. 由于事件可以按照顺序存储,所以可以搞成追加方式去持久化,而这种追加操作来持久化事件的方式可以放到前台,对用户体验或者性能要求很高的地方。这样不会引发前台卡顿。同时呢,可以让事件能跟水流一样,被引入到后台任务中慢慢处理。
2. 事件本身是一种场景记录,所以,利用这些记录的时候,可以根据自身情况,在任何合适的时间,合适的环境,去根据事件去实施或者复现某些业务状态。
3. 事件的存储本身可以被当成一种审计日志,只要记录的信息够全,事件溯源本身就会天然的变成可靠安全的审计数据。
4. 事件溯源本身可以和各种事件驱动的系统相融合,非常适合扩展和对接各类靠事件驱动的应用和系统。
5. 事件溯源不会给已经非常复杂的业务对象增加复杂度。比如,一个订单对象,根据订单对象设计订单表的时候,可能还得搞个备注字段用来存储一些更新时的说明;可能还得搞个最近更新时间记录下最近更新发生在什么时候;甚至可能由于本身业务状态的复杂,还得特意拆解成几个不同的状态字段……
总之,随着我对事件溯源认识的逐渐加深,我觉得自身已经开始有了微服务专家的气质。
四、不满
当然,太阳底下没啥新鲜事儿。任何新东西的引入总会带来一些不足,同时呢,随着使用事件溯源模式的次数增多,我也愈发认识到了这个模式的不足。
1. 要存储的事件数据太多了,导致查询得引入另一个查询职责分离模式(CQRS),才能解决大部分的查询问题。
2. 使用事件溯源的时候由于事件发生的顺序存储非常重要,所以,使用多线程,多进程,集群的时候,就必须要严格保证事件顺序存储的正确性,一般来说,得给事件对象搞个时间戳不说,可能还得引入全局唯一标识符产生器去产生事件 ID。
3. 由于事件本身是个业务对象了,所以,你知道了,它自身一定会进化的。所以,还得考虑老版本新版本的共存问题,这种一般至少得给事件结构弄个版本字段去标识事件对象的版本。
4. 事件存下来了,而且大部分时候都是附加形式的顺序存储。这就导致查询事件的时候没办法,只能按照事件标识符和事件的时间之类的做查询,而这样的话,其实就是查询出来了一个事件流。如果要场景重现和分析业务对象状态的时候,就非得把这个事件流给整个重新处理一遍。
5. 事件溯源这事儿其实就是人为的松绑了业务的一致性要求。但是,业务需要的一致性问题依然还是需要另外的处理。比如,我们搞了电商网站,同时呢,又通过事件溯源模式去落地了库存商品数量更新的业务,又恰巧把库存的存货减少的各种原因给设计成了不同的事件,那么,当库存因为非客户下单减少发生时,又恰好客户在下单,这时候,就需要单独的处理他们之间的冲突,去保证状态的一致性。
6. 事件这东西本身可能因为业务原因需要各种传递,而在这期间,不管使用什么方式去传播事件,没人会给你保证事件不会重复传播。这时候,就得考虑处理事件的幂等性。这也是事件溯源带来的麻烦。
五、结尾
事件溯源模式虽然解决了我的很多问题,但是同时又因为引入这个模式,我又增加了很大的工作量。真是金无足赤啊。
也许这世上根本不存在什么溯源模式,有的只是防止背锅的无奈罢了。
你好,我是四猿外。
一家上市公司的技术总监,管理的技术团队一百余人。
我从一名非计算机专业的毕业生,转行到程序员,一路打拼,一路成长。
我会通过公众号,
把自己的成长故事写成文章,
把枯燥的技术文章写成故事。
我建了一个读者交流群,里面大部分是程序员,一起聊技术、工作、八卦。欢迎加我微信,拉你入群。
推荐阅读