一年前做过两个比较肤浅的消息队列的总结,消息队列作用,消息队列应用-使用异步队列就解耦了吗
。如今回过头来再梳理一下对消息队列的认知。
某些大厂的云服务文档对了解一些消息队列基本知识还是比较有帮助比如亚马逊消息队列文档
本文不梳理技术细节,仅总结自己在应用层面的认知。
消息队列模型
消息队列主要是两种模型,队列模型与主题模型。
- 队列模型(queue)
也即点对点或者一对一模式,1个消息只会被一个消费者消费。可以有一个或者多个消费者同时消费一个队列,但是被消费的消息不会重复。 - 主题模型(topic)
也即发布订阅模式,领域设计中也常说扇出设计模式。消息可以被多个消费者消费。此类队列一般被称为一个topic,所有订阅该topic的consumer都可消费所有消息。
消息队列具体应用
消息队列功能
部分内容参考消息队列功能
消息队列通常具备的功能如下:
- 异步通信
消息的生产者在消息成功发送到队列中即可立即返回,不需等待消费者接收并处理消息,生产和消费完全异步。该功能在队列模型和主题模型均适用。 - 延迟队列
许多消息队列都支持为消息设置特定的传送时间。如果需要为所有消息设置相同延迟,可以设置一个延迟队列。 - 死信队列
死信队列可以接收其他队列发来的未成功处理的消息。这便于将此类消息放在一边以进行深入检测,而不会妨碍队列处理或将 CPU 周期耗费在可能永远无法成功处理的消息上。 - FIFO (先进先出) 队列
消息本身是有时间的先后顺序的。生产者生产的消息到队列也有到达顺序,消息队列通常会提供尽力确保消息大致按其发送的顺序进行传送,且消息至少传送一次。消费者的消费顺序受网络、确切到达时间、处理速度等的影响,但有些消息队列(比如kafka),采用特定规则(比如相同用户的消息发送到一个partition,线程封闭的消费)可以保证消息的有序消费。 - 至少一次 & 最多一次 & 精确一次
通常消息队列都是支持至少一次消费的。
精确一次可以通过至少一次+幂等梳理。
最多一次会丢消息,除非消息可以丢失,不然一般不用。 - 队列模型&主题模型
上文已讨论 - 路由功能
topic模型下可以支持消息的路由。比如指定不同的路由key,topic根据路由key分发到不同的consumer。再比如kafka中的partition其实也是一种路由方式,消息经过hash后到达特定的partition,由某一个consumer线程消费。 - 消息复用
topic模型下,同一个消息可以分发给多个consumer
消息队列使用场景
消息队列的作用大概可以用这么几个词来概括:异步、削峰、解耦、提高性能。
异步,这是消息队列最核心的功能,其他功能都依赖异步性实现。生产者将消息发送到消息队列后可以立即返回,不用等待消费者的响应。
削峰,可以理解为异步带来的好处之一,消息队列能够平衡生产和消费的速度。生产的消息快可以暂时将消息留在队列中,消费者慢慢消费。
解耦,这里主要指的是系统组件之间的通信方式的解耦,也是异步带来的好处之一,生产者不需要等待消费者响应,在组件通信中,消息生产和消费方都依赖消息队列,两者直接就无需直接耦合了。还有另外一种业务上的耦合,下文讨论。
提高性能,通过异步化,一方面可以对消息在消费者里做聚合计算或者批量更新等处理,提高性能。另一方面topic模型可以提高不同组件的并行度,下文讨论。
围绕着这几个作用,消息队列的使用场景很多,举几个例子说下消息队列的部分使用场景:
- 提高通信的可靠性
网络通信不可靠或者下游不可靠的情况下,可以使用消息队列来确保消息的可靠投递,增加系统的可靠性。消息生产者不需要过多关注下游或通信链路的稳定性,交给消息队列解决。为了提高消息投递可靠性,可能会造成消费者对消息的重复消费。 - 一对多消息发布
topic模型的应用,生产者向多个订阅者提供消息。比如一个订单支付成功后,可能要很多其他组件做相应处理,比如供应链发货、赠送优惠券、核销优惠券、发送短信通知。可以由订单系统发送一个topic消息,供应链组件、优惠券组件、通知组件订阅消息,并行处理即可,解耦并且提高性能。另外多用于领域事件发布场景,下文讨论。
这个订单的例子除了技术上的解耦外,也是一种业务上的解耦。订单服务,不再依赖供应链、优惠券、通知服务来完成订单了。 - 屏蔽语言差异
有些公司存在多种技术栈并行的情况,比如java、c#、Python等。用消息队列,只需要与消息中间件交互即可,不存在语言差异。 - 系统组件的解耦
比如这么两方面,生产者不需要等待消费者响应、上文提到的订单和其他组件的解耦。 -
削峰
上文有提到。具体的case比如:秒杀场景下接口请求量巨大,除了用缓存等措施有效增强计算性能外,还可以通过消息队列对接口请求做削峰处理。
- 提高计算性能
消费方可以将消息聚合后做批量计算,比如批量写入数据库,可以缓解数据库压力。另外如上文订单例子,提高了计算并行度。 - 优先级队列
根据topic的路由功能,可以针对某个特定的路由key加大计算量,优先处理。比如根据kafka的partition实现。
消息队列与解耦
通常情况下,我们说的消息队列的解耦功能,主要是由于其异步的特性带来的技术上的解耦,消息生产者不需要关注消费者的消费状态,可直接返回。
业务上来看,还是存在耦合的。比如业务耦合例子。耦合是不能完全避免的,需要我们做设计的过程中去权衡。这里根据经验做下总结:
- 如果消息队列仅涉及到两个系统组件,使用队列模型,以消费方为主和生产方协商好格式即可。
- 如果消息需要被重复消费,或者对接该消息的组件多于1个,通常采用主题模型,由消息生产者定义格式,消费者接收消息转换成自己的逻辑语言处理。
- 涉及到领域事件的发布,通常使用主题模型,事件消费方将所依赖的上下文的领域事件转换自己所在的上下文通用语言做处理。
总结来看。消息队列的解耦可以包括两方面,技术上的解耦和业务上的解耦。业务上的耦合是不可避免的,尽量做到降低耦合性以及消息的复用,我们的设计不能脱离具体的业务场景。
消息队列与领域模型
DDD(领域驱动设计)中的有一个概念叫领域事件。消息中间件是领域事件常用的存储方式。
本文仅讨论和消息队列相关的内容,其他领域事件的用途,比如用来做bug跟踪、预测分析、操作的撤销(很多分布式存储中的undo),这里不做讨论。
关于领域事件可以参考《实现领域驱动设计》这本书。看到一篇不错的文章,也是对该书内容的总结 ,同时扩展了一些实现方式领域事件
领域事件
暂时没有找到特别明确的定义,《实现领域驱动设计》中有过一些概念:领域事件是领域专家所关心的发生在领域中的一些事件等。
可以这么理解,对于限界上下文中发生的每一件事,我们都用事件的形式予以捕获并发布给订阅方处理。领域事件也应该作为领域模型的通用语言的一部分。(限界上下文和通用语言是领域模型战略设计的核心)
领域事件的主要用途
- 保证聚合间的数据一致性
- 替换批量处理
- 实现事件源模式
- 进行限界上下文集成
这些也可以理解为消息队列的一些使用场景,当然领域事件是不限于实现方式的。
领域事件的实现
最简单的领域事件的发布模式就是观察者模式,消息队列或者领域设计中我们通常叫做发布-订阅模式。
领域事件的消费者可以是本地模块也可以是远程模块,对应的领域事件存储的方式包括共享内存形式、restful资源形式、以及消息中间件等。
- 共享内存的形式比较好理解,也即用代码实现一个观察者模式,将捕获到的事件通过接口调用通知给订阅方接口。
- restful资源的形式,可以这么理解,消息发布者通过restful的接口的形式来发布资源,消费者根据消息区间(可以是时间或者id范围)来拉取信息,是一种拉的模式。这种方式,我在实践过程中,在不同公司之间的数据同步场景用的比较多。这种情况下
- 消息中间件的方式就是本文要讨论的重点内容了。在实践过程中,公司内部多个系统组件之间的领域事件发布多采用这种形式。
这里主要讨论共享内存的形式无法处理的向远程限界上下文发布领域事件的方法。在不同的限界上下文见采用领域事件的形式通信时,必须要保证最终一致性。领域事件的发布通常是一个异步的过程,受限于各系统吞吐量等因素,一个模型的改变可能要过一段时间才能提现到另外一个模型中。
《实现领域驱动设计》书中讨论了向远程限界上下文发布领域事件的三个问题:
- 消息设施的一致性
无论是什么形式发布领域事件,要想保证最终一致性,我们至少要保证两个存储的最终一致性:领域模型所使用的的持久化存储和消息设施所使用的的持久化存储。这样保证了持久化领域模型是,领域事件也得以存储并发布成功。如果这两者不一致,会导致最终两个限界上下文中的状态错误。
保证这个一致性有几个办法:
—领域模型和消息设施共享存储。在这种情况下,模型和事件的提交在一个事务中完成,从而保证两种的一致性。
—领域模型的持久化和消息的持久化采用XA事务,有个概念叫做事务消息。这种情况下,模型和消息所用的持久化存储可以分离,但会降低系统性能。
—领域模型的存储中留一块区域存储领域事件,从而在本地事务中完成领域和事件的存储。然后,通过后台服务将事件异步发送到消息队列中。该方式和与消息设施共享存储很像,区别在于该方式可以保证在一个本地事务提交,还可通过restful的方式暴露事件资源。
一般情况下,第三种,是比较优雅的解决方案。
- 自治服务和系统
自治可以理解成没有对远程RPC服务的调用,具备高度的独立性。RPC调用是具有可靠性、稳定性风险。
通过领域事件的方式,自治服务将领域事件转化为自己的通用语言进行存储。这里要注意,消费者不是对事件生产者的简单复制,而是要转化成自己限界上下文的通用语言。 - 容许延时
事件的方式必然存在延时,数据的延时可能有比较严重的影响,也可能几无影响,只需要保证最终一致性即可,这块使我们设计系统要考虑的重要因素。
事务消息
消息生产者本地事务处理与消息发送可能存在不一致的情况,事务消息是用来实现消息生产者本地事务与消息发送的原子性,保证消息生产者本地事务处理成功与消息发送成功的最终一致。事务消息是分布式事务的一种解决方案。
事务消息有多种实现方式,
有些是用户配合消息中间件实现类似 X/Open XA 的分布事务功能。
有些是利用数据库本地事务,比如去哪儿开源的qmq事务消息
也就是上文提到的消息一致性的第三种方案。
幂等性与消息去重
领域事件通常可以理解为是一种值对象。值对象可以没有唯一标识。
但是在有些情况下,消息系统可能多次向消费者发送重复的消息,这时候就需要做好消息的幂等性处理,也就是消息的去重。
做消息去重可以在消息队列处理也就是保证精确发送一次,也可以在订阅方(消费者)处理。利用消息队列处理将会很麻烦。通常情况下我们在订阅方在消费时基于自己的领域模型做幂等处理。而一种简单的方式便是发布方在发布事件消息时设置一个唯一的消息id作为事件的唯一标识。
事件唯一标识,将它作为通用属性进行管理,本身对领域建模影响不大,但对技术处理好处巨大。当我们需要将领域事件发布到外部的限界上下文时,唯一标识就是一种必然。为了保证事件投递的幂等性,在发送端,我们可能会进行多次发送尝试,直至明确发送成功为止;而在接收端,当接收到事件后,需要对事件进行重复性检测,以保障事件处理的幂等性。此时,事件的唯一标识便可以作为事件去重的依据。