消息队列
消息队列MQ的使用有下面几个好处,或者说我们往往处于这些目的来考虑引入MQ:
1. 异步处理:类似于订单这样的流程一般可以定义出一个核心流程,这个流程用于处理核心订单的状态机,需要尽快同步落库完成,然后围绕订单会衍生出一系列和用户相关的库存相关的后续的业务处理,这些处理完全不需要卡在用户点击提交订单的那刹那进行处理。下单只是一个确认合法受理订单的过程,后续的很多事情都可以慢慢在几十个模块中进行流转,这个流转过程哪怕是消耗5分钟,用户也无需感受到。
2. 流量洪峰:互联网项目的一个特点是有的时候会做一些toC的促销,免不了有一些流量洪峰,如果我们引入了消息队列在模块之间作为缓冲,那么backend的服务可以以自己既有的舒服的频次来被动消耗数据,不会被强压的流量击垮。当然,做好监控是必不可少的,下面再细说一下监控。
3. 模块解耦:随着项目复杂度的上升,我们会有各种来源于项目内部和外部的事件(用户注册登陆、投资、提现事件等),这些重要事件可能不断有各种各样的模块(营销模块、活动模块)需要关心,核心业务系统去调用这些外部体系的模块,让整个系统在内部纠缠在一起显然是不合适的,这个时候通过MQ进行解耦,让各种各样的事件在系统中进行松耦合流转,模块之间各司其职也相互没有感知,这是比较适合的做法。
4. 消息群发:有一些消息是会有多个接收者的,接收者的数量还是动态的(类似指责链的性质也是可能的),在这个时候如果上下游进行一对多的耦合就会更麻烦,对于这种情况就更适用使用MQ进行解耦了。上游只管发消息说现在发生了什么事情,下游不管有多少人关心这个消息,上游都是没有感知的。
这些需求互联网项目中基本都存在,所以消息队列的使用是非常重要的一个架构手段。在使用上有几个注意点:
1. 我更倾向于独立一个专门的listener项目(而不是合并在server中)来专门做消息的监听,然后这个模块其实没有过多的逻辑,只是在收到了具体的消息之后调用对应的service中的API进行消息处理。listener是可以启动多份做一个负载均衡的(取决于具体使用的MQ产品),但是因为这里几乎没有什么压力,不是100%必须。注意,不是所有的service都是需要有一个配到的listener项目的,大多数公共基础服务因为本身很独立不需要感知到外部的其它业务事件,所以往往是没有listener的,基础业务服务也有一些是类似的原因不需要有listener。
2. 对于重要的MQ消息,应当配以相应的补偿线作为备份,在MQ集群一切正常作为补漏,在MQ集群瘫痪的时候作为后背。我在日千万订单的项目中使用过RabbitMQ,虽然QPS在几百上千,远远低于RabbitMQ压测下来能抗住的数万QPS,但是整体上有那么十万分之一的丢消息概率(我也用过阿里的RocketMQ,但是因为单量较小目前没有观察到有类似的问题),这些丢掉的消息马上会由补偿线进行处理了。在极端的情况下,RabbitMQ发生了整个集群宕机,A服务发出的消息无法抵达B服务了,这个时候补偿Job开始工作,定期从A服务批量拉取消息提供给B服务,虽然消息处理是一批一批的,但是至少确保了消息可以正常处理。做好这套后备是非常重要的,因为我们无法确保中间件的可用性在100%。
3. 补偿的实现是不带任何业务逻辑的,我们再梳理一下补偿这个事情。如果A服务是消息的提供者,B-listener是消息监听器,听到消息后会调用B-server中具体的方法handleXXMessage(XXMessage message)来执行业务逻辑,在MQ停止工作的时候,有一个Job(可配置补偿时间以及每次拉取的量)来定期调用A服务提供的专有方法getXXMessages(LocalDateTime from, LocalDateTime to, int batchSize)来拉取消息,然后还是(可以并发)调用B-server的那个handleXXMessage来处理消息。这个补偿的Job可以重用的可配置的,无需每次为每一个消息都手写一套,唯一需要多做的事情是A服务需要提供一个拉取消息的接口。那你可能会说,我A服务这里还需要维护一套基于数据库的消息队列吗,这个不是自己搞一套基于被动拉的消息队列了吗?其实这里的消息往往只是一个转化工作,A一定在数据库中有落地过去一段时间发生过变动的数据,只要把这些数据转化为Message对象提供出去即可。B-server的handleXXMessage由于是幂等的,所以无所谓消息是否重复处理,这里只是在应急情况下进行无脑的过去一段时间的数据的依次处理。
4. 所有消息的处理端最好对相同的消息处理实现幂等,即使有一些MQ产品支持消息处理且只处理一次,靠自己做好幂等能让事情变得更简单。
5. 有一些场景下有延迟消息或延迟消息队列的需求,诸如RabbitMQ、RocketMQ都有不同的实现方式。
6. MQ消息一般而言有两种,一种是(最好)只能被一个消费者进行消费并且只消费一次的,另一种是所有订阅者都可以来处理,不限制人数。不用的MQ中间件对于这两种形式都有不同的实现,有的时候使用消息类型来做,有的使用不同的交换机来做,有的是使用group的划分来做(不同的group可以重复消息相同的消息)。一般来说都是支持这两种实现的。在使用具体产品的时候务必研究相关的文档,做好实验确保这两种消息是以正确的方式在处理,以免发生妖怪问题。
7. 需要做好消息监控,最最重要的是监控消息是否有堆积,有的话需要及时增强下游处理能力(加机器,加线程),当然做的更好点可以以热点拓扑图绘制所有消息的流向流速一眼就可以看到目前哪些消息有压力。你可能会想既然消息都在MQ体系中不会丢失,消息有堆积处理慢一点其实也没什么问题。是的,消息可以有适当的堆积,但是不能大量堆积,如果MQ系统出现存储问题,大量堆积的消息有丢失也是比较麻烦的,而且有一些业务系统对于消息的处理是看时间的,过晚到达的消息是会认为业务违例进行忽略的。
8. 图上画了两个MQ集群,一套对内一套对外。原因是对内的MQ集群我们在权限上控制可以相对弱点,对外的集群必须明确每一个Topic,而且Topic需要由固定的人来维护不能在集群上随意增删Topic造成混乱。对内对外的消息实现硬隔离对于性能也有好处,建议在生产环境把对内对外的MQ集群进行隔离划分。