消息中心一致性解决方案
1、介绍
消息发送一致性是指产生消息的业务动作和消息的发送一致,两者要不同时成功或失败。在确定使用rabbitmq作为消息中心的实现框架后,消息发送的一致性应结合实际的框架实现。
rabbitmq官方推荐不使用事务实现消息发送的一致性,而是采用异步的confirm结合mandatory和immediate(RabbitMQ 3.0采用TTL和DLX替代immediate实现)标志位进行实现。
2、解决方案
消息发送者的一致性包括broker持久化消息和publisher知道消息已经成功持久化。主要有同步和异步两种方式实现,如下:
2、1采用事务方式
每个消息都必须经历以上两个步骤,就算一次事务成功,如果业务类型要求数据一致性非常高,可以采用低效率的事务型解决方案,官方给出来的性能是:It takes a bit more than 4 minutes to publish 10000 messages(发布10000条消息需要4分钟多一点)。
采用事物的方式固然能解决发送的一致性问题,但是消息发送变成了业务应用的必要依赖,如果消息存储或应用于消息的网络异常都会导致业务操作无法正常完成,所有可将业务系统与消息发送进行解耦合。实现的大致设计架构流程如下:
如果采用标准的 AMQP 协议,则唯一能够保证消息不会丢失的方式是利用事务机制 -- 令channel处于transactional模式、向其 publish 消息、执行 commit 动作。在这种方式下,事务机制会带来大量的多余开销,并会导致吞吐量下降 250% 。为了补救事务带来的问题,引入了 confirmation 机制(即 Publisher Confirm)。一般不使用事务提交的方式。
对于要求强一致性的应用结合选型实现框架Rabbitmq,应用的接入需要channel-transacted="true",在消息发送端实现消息发送和业务放在同一事务中。结合spring如下:
2、2异步的方式监听实现
默认情况RabbitMQ流转如下,publisher->server,server不会将publisher的请求的执行情况,返回给publisher。换句话说,默认,publisher只知道执行了生产消息的动作,不知道server是否已成功存储msg,更不知道msg是否已被consumer消费。
异步的方式是指publisher发送消息后,不进行等待,而是异步监听是否成功。这种方式又分为两种模式,一种是return,另一种是confirm. 前一种是publisher发送到exchange后,异步收到消息。第二种是publisher发送消息到exchange,queue,consumer收到消息后才会收到异步收到消息。可见,第二种方式更加安全可靠(如果一旦出现broker挂机或者网络不稳定,broker已经成功接收消息,但是publisher并没有收到confirm或return,就会参数重复发生消息的问题,后面会对重复消息进行详细讲解)。官方给出的性能是:It takes a bit more than 2 seconds to publish 10000 messages(发布10000条消息需要2秒多一点)。结构图大致如下:
具体的异步返回的几个标志位置介绍如下:
1)confirm
如果使用confirm模式,publisher->server,server只会告知publisher,是否接收到了请求。publisher只知道server接收到了msg,但不知道msg是否成功存储到queue。
可见仅仅使用confirm并不能完全保证消息发送的一致性,需要引入更远的标志位。Mandatory和immediate是AMQP协议中basic.pulish方法中的两个标志位,它们都有当消息传递过程中不可达目的地时将消息返回给生产者的功能。
2)mandatory
如果使用了mandatory标志位,publisher->server,server会告知publisher,是否正确找到对应的queue,并把msg保存到了queue中。当mandatory标志位设置为true时,如果exchange根据自身类型和消息routeKey无法找到一个符合条件的queue,那么会调用basic.return方法将消息返还给生产者;当mandatory设为false时,出现上述情形broker会直接将消息扔掉。
mandatory属性的使用和Publisher Confirm机制没有必然关系,只有将mandatory属性和Publisher Confirm机制结合使用,才能真正实现消息的可靠投递。(如果只使用Publisher Confirm机制,消息丢失了,却仍旧可以收到来自服务器的Ack,这也是实际使用中容易犯的错误。官方说明:“对于无法路由的信息,broker会在确认了通过exchange无法将消息路由到任何queue后,发送回客户端basic.ack进行确认(其中包含空的queue列表)。如果客户端发送消息时使用了mandatory属性,则会发送回客户basic.return + basic.ack信息。” 其中说,发回的basic.ack中会包含一个空的queue列表,但是确实没看到。既然如此还是最好使用mandatory属性)。
3)immediate
如果使用了immediate标志位,publisher->server,server上如果该消息关联的queue上有消费者,则马上将消息投递给它,如果所有queue都没有消费者,直接把消息返还给生产者,不用将消息入队列等待消费者了。当immediate标志位设置为true时,如果exchange在将消息route到queue(s)时发现对应的queue上没有消费者,那么这条消息不会放入队列中。当与消息routeKey关联的所有queue(一个或多个)都没有消费者时,该消息会通过basic.return方法返还给生产者。RabbitMQ 3.0.0以后的版本中去掉immediate参数支持,因为immediate标记会影响镜像队列性能,增加代码复杂性,并建议采用"设置消息TTL"和"DLX"等方式替代(详细说明参见《RabbitMQ的immediate代替方案之TTL 详解》和《RabbitMQ的immediate代替方案之DLE》)。具体使用中结合spring如下:
1. <rabbit:queue id="zkcloud.subsystem.dlx.queue" name="#{dlxNaming['zkcloud.subsystem.dlx.queue']}">
2. <rabbit:queue-arguments>
3. <entry key="x-message-ttl">
4. <value type="java.lang.Long">86400000</value>
5. </entry>
6. <entry key="x-max-length">
7. <value type="java.lang.Long">100</value>
8. </entry>
9. </rabbit:queue-arguments>
10. </rabbit:queue>
11.
12. <rabbit:fanout-exchange id="zkcloud.subsystem.dlx.exchange" name="#{dlxNaming['zkcloud.subsystem.dlx.exchange']}">
13. <rabbit:bindings>
14. <rabbit:binding queue="zkcloud.subsystem.dlx.queue" />
15. </rabbit:bindings>
16. </rabbit:fanout-exchange>