消息队列,在复杂系统中应用广泛。虽然说加入mq后,系统的复杂度提高、系统可用性也变低而且可能引发数据出现一致性的问题,那么为什么还要用MQ呢?其实主要是其在特殊的场景下能解决我们很多问题。
一、MQ使用的主要场景
使用MQ主要场景有三种:解耦、异步以及削峰。下面就三大使用场景进行简单介绍:
1、解耦
以生产者-消费者模式为例,系统A要为系统B、C发送消息,A不仅要维护现有接入系统的数据接受是否超时、失败重发等情况,还要维护新增接入系统的D、或者系统B不需接收消息等动态变化的情况。
那么MQ的“发布-订阅消息模型”,就可以把A与其他系统间的通信进行解耦。这个场景下,主要考虑点是消息的调用是否需要直接同步,不需要的话即可使用MQ进行系统解耦。
2、异步
在完成一个业务流程中,各个结点不一定需要全部是同步进行的。比如,刷卡付款和短信通知扣款这就是可以异步进行的业务节点。
日常开发中,我们也经常碰到系统A收到请求报文后,进行本地入库后,给其他两个系统B、C分别发消息,然后等待全部消息被处理返回后,在返回给用户。假设A入库需要10ms,B、C返回结果分别是500ms,那么总耗时就是10ms+500ms+500ms=1010ms。
使用MQ,A发消息给MQ,MQ同步给B和C。总耗时最多也是10ms+500ms=510ms,几乎效率提升了一倍。
3、削峰
一些电商平台的流量在一天内会出现明显的抖动。在平常并发数大概也就50个/秒,但是高峰期可能达到5000以上的并发。而这些平台的数据库都是并发支持上限在2000左右的mysql,因此很快久把数据库给搞崩了,系统也没法再正常使用。
常用的做法是把请求写入到MQ里面,然后系统A按数据库可以支持的并发量去拉取数据。这样就限制住了整个系统的处理速率,等待高峰期过后,系统访问流量下降到50个时,依旧用2000个/秒的处理速度,即可快速消化积压的请求。
二、MQ系统设计
RabbitMQ是一款常用的MQ中间件,我们就以它为例,讲述下MQ系统设计的过程。
我们了解MQ是基于“发布-订阅消息模型”设计的,那么这里就有三个主要的节点:消息队列、生产者、消费者。
上一节我们也说过了解耦、异步、削峰的三大使用场景或者好处,但是坏处在引起系统可用性降低、系统复杂度提高以及数据一致性问题。我们设计时前面两个问题比较难解决,我们重点解决一致性问题。接下来我们就从消息队列、生产者、消费者进行优化设计
1、消息队列
RabbitMQ的信息在默认情况下,只保存在内存中,不做持久化到硬盘的操作。这种情况下,如果出现MQ节点宕机或者重启的话,消息就被丢失了。所以我们设计时,要考虑把消息进行持久化。
从RabbitMQ的架构图de 架构图来看,要实现消息持久化,需要同时满足三个条件:
1)Exchange设置了持久化
2)Queue设置持久化
3)Message持久化
2、生产者
在生产者这端,遇到的主要问题是不能确定消息是否真的到达了MQ服务器,并被正常接收。对于一般消息来说,丢失了影响可能不大,但是对于类似银行卡的账号余额变动,扣款等消息,那么影响就非常大了。那么我们可以在生产者端设置Confirm和Return两种机制。
在配置文件中加入:
publisher-confirm-type: CORRELATED
publisher-returns: true
3、消费者
在正常流程中,只要RabbitMQ把消息推送给了消费者,即可认为投递成功,那么MQ就会把内存或者磁盘中的消息给删掉。
但是如果消费者节点意外挂掉了,如逻辑处理时间过程超时了、网络中断、消费者节点被停掉等多种异常情况。那么消息可能就被丢失了,没法正常消费掉。
所以我们可以把消费者端的自动ACK模式,改为手动ACK模式。修改后,消费者在处理消息成功后,手动ACK给MQ服务端,这时候服务端才把消息从内存删掉。如果一个消费者端出问题了,没有消费成功这个消息,那么消息还可以给其他同功能的消费者去进行消费,保证消息不丢失。这种机制就叫ACK确认机制。
4、应对意外情况
上面从发布-订阅消息模型中的三个节点进行MQ系统设计的优化,可以保证99.99%的消息被正常处理,但是还是有一些常见的意外场景值得我们在深思的。例如,生产者在发送消息到MQ前突然宕机,MQ在准备持久化到磁盘时宕机。那么我们应该如何应对呢?
消息补偿机制就是一个很好的解决方法。一般会抽取一个独立的的微服务,定时轮询数据库中消息发送情况,并把未发送成功的消息进行重新发送。
一般消息持久化入库,会设定补偿次数,创建时间、以及消息状态。消息补偿服务会定时把补偿次数小于阈值的未发送成功的信息进在行重发。
当然也需要预留业务处理的事件,一般情况下,我们可以设定创建时间在5分钟内的消息,不进行重发。