如何保证RabbitMQ的高可用
我们在线上是采用了RabbitMQ的镜像集群模式,这种模式下,集群中的每个RabbitMQ节点都会有其他节点中Queue队列里面的全部数据,当我们写消息到queue里面,RabbitMQ会自动把消息同步到其他实例的queue里面。这样的话,任何一台机器宕机了,其他机器都包含宕掉机器中queue里面的完整数据。不过这种集群方式性能开销比较大,消息同步到所有机器上,会导致网络带宽压力和消耗很重。
// 镜像集群中,队列会变成Mirrored-queue。
这里的节点中的全部数据就包括了元数据(名字和属性)还有节点中的message和节点位置以及各节点的关系。其中元数据就包括了所有Queue的元数据、Exchange的元数据、Binding元数据以及Vhost元数据。
RabbitMQ的集群模式
- 单一模式:即单机情况不做集群,就单独运行一个rabbitmq而已。
- 普通模式:默认模式,以两个节点(rabbit01、rabbit02)为例来进行说明。对于Queue来说,消息实体只存在于其中一个节点rabbit01(或者rabbit02),rabbit01和rabbit02两个节点仅有相同的元数据,即队列的结构。当消息进入rabbit01节点的Queue后,consumer从rabbit02节点消费时,RabbitMQ会临时在rabbit01、rabbit02间进行消息传输,把A中的消息实体取出并经过B发送给consumer。所以consumer应尽量连接每一个节点,从中取消息。即对于同一个逻辑队列,要在多个节点建立物理Queue。否则无论consumer连rabbit01或rabbit02,出口总在rabbit01,会产生瓶颈。当rabbit01节点故障后,rabbit02节点无法取到rabbit01节点中还未消费的消息实体。如果做了消息持久化,那么得等rabbit01节点恢复,然后才可被消费;如果没有持久化的话,就会产生消息丢失的现象。
- 镜像模式:把需要的队列做成镜像队列,存在与多个节点属于**RabbitMQ的HA方案。**该模式解决了普通模式中的问题,其实质和普通模式不同之处在于,消息实体会主动在镜像节点间同步,而不是在客户端取数据时临时拉取。该模式带来的副作用也很明显,除了降低系统性能外,如果镜像队列数量过多,加之大量的消息进入,集群内部的网络带宽将会被这种同步通讯大大消耗掉。所以在对可靠性要求较高的场合中适用。
如何保证消息的可靠性传输
以RabbitMQ为例,一般我们保证消息传输的三个阶段都正常来确定消息的可靠性传输。分别是消息在生产者阶段、在MQ阶段和在消费者阶段。
生产者阶段的话,RabbitMQ有生产者确认模式,就是我们将信道设置成Confirm模式,每个发布到信道上的消息会被指派一个唯一的ID,当我们的消息被投递到目标队列里面,信道会发送一个Ack确认信号给生产者,其中就包含了消息的唯一ID。如果发布到目标队列失败的话,则信道会回复Nack信号,这样生产者就可以决定之后的操作。(生产者还有一种事务模式,不过是同步操作,吞吐量下降,而且消耗较大,一般不推荐使用)
MQ阶段的话,我们可以通过设置Queue的durable值为True来开启Queue的持久化,还有在发送消息时将模式中的deliveryMode设置为2来开启消息的持久化。(当然,全消息都持久化会降低性能,所以一般将重要的消息持久化即可)这样的话即使中间MQ挂了,重启后也能恢复数据。如果消息还没持久化到硬盘,MQ就挂了,可以通过引入mirrored-queue来解决。
消费者阶段的话,RabbitMQ有接收方确认模式,这个模式下有自动确认和手动确认两种模式,自动确认会出现丢失消息的问题,即消息从MQ发送到消费者后就自动确认接收成功,但如果消费者刚接收到消息还没处理,结果消费者挂了,但MQ已经把数据从Queue中删除了,这就出现了问题。所以我们一般会关闭自动确认机制而使用手动确认机制(设置autoAck = manual ,isAutoack= false)就是代码执行完后,消费者确认了消费行为,再给MQ发送ack信号,这样即使消费过程中消费者挂了,MQ也会因为没有收到ACK信号将消息保存在MQ,然后发送给下一个订阅的消费者,当然,为了避免消息被重复消费,可以通过bizID去重。
注:只有消费者挂了,MQ才会重新投递,发送给下一个订阅的消费者。如果是长时间消费者没响应,那么rabbitMQ也会一直等下去。
补充:事实上,生产者发布消息时会先传递到Exchange交换机上,再通过Exchange转发给对应的Queue队列上。如果在生产者阶段中如果发布到一个不存在的Exchange交换机上,RabbitMQ会将信道关闭。 如果消息在交换机中因为没有匹配的Queue队列所以无法路由,则可以使用RabbitMQ内置的备份交换机,做法是创建备份交换机,当业务交换机碰到无法路由的消息时会将消息转交给备份交换机,之后备份交换机可以绑定Queue,来让报警处理机制通过这个Queue来获取需要手动处理的消息内容。(还可以设置消息的mandatory值为true,这样消息没有被传递到Queue里面会向生产者返回没有投递成功的信息)
如何保证消息不被重复投递或重复消费
在消息生产时,MQ内部针对每条生产者发送的消息生成一个inner-msg-id,作为去重和幂等的依据(消息投递失败并重传),避免重复的消息进入队列;(可以使用Redis来记录)
在消息消费时,要求消息体中必须要有一个bizId(对于同一业务全局唯一,如支付ID、订单ID、帖子ID等)作为去重和幂等的依据,避免同一条消息被重复消费。
RabbitMQ中的一些概念
channel:是建立在真实的TCP连接上的虚拟连接,它可以定义Queue、定义Exchange、绑定Queue和Exchange、发布消息等。
queue:是存放message的区域,RabbitMQ的内部对象。
exchange:主要用于控制消息到队列的路由,根据具体的exchange type将消息传给需要的队列或者直接废弃。
RabbitMQ的发布 / 订阅模式
之前的例子都基本都是1对1的消息发送和接收,即消息只能发送到指定的queue里,但有些时候你想让你的消息被所有的Queue收到,类似广播的效果,这时候就要用到exchange了,定义的类型有三种:
- fanout: 所有绑定到此exchange的queue都可以接收消息
- direct: 通过routingKey和exchange决定的那个唯一的queue可以接收消息
- topic: 所有符合routingKey(此时可以是一个表达式)的routingKey所bind的queue可以接收消息
TIPS:以上三种模式都是广播形式,时时接收,如果消费者不在线该条消息将不会再次接收,类似收音机。
死信队列&死信交换器 ----> 延时队列
死信交换器:DLX 全称(Dead-Letter-Exchange),当消息变成一个死信之后,如果这个消息所在的队列存在x-dead-letter-exchange参数,那么它会被发送到x-dead-letter-exchange对应值的交换器上,这个交换器就称之为死信交换器,与这个死信交换器绑定的队列就是死信队列。
所以说死信交换器其实是个普通的交换器,只不过是专门用来处理死信而已。
出现死信消息的情况:
- 消息被拒绝(Basic.Reject或Basic.Nack)并且设置 requeue 参数的值为 false;
- 消息过期了,也就是消息过期了还没被消息;过期时间可以通过设置queue的“ x-message-ttl ”参数达到Queue中所有消息有统一的过期时间或者设置单条消息的过期时间“Expiration”;
- 队列达到最大的长度。
注:设置队列过期时间后,队列中每条消息都会有相同过期时间,当过期时间到了,消息则会被队列清除或者移到死信队列中。而设置单条消息过期时,则是RabbitMQ检查到对应的消息,发现过期了才去清除或移到死信队列。
而且如果队列中第一条消息有很长的过期时间,而第二条过期时间较短,如果没有做处理,也只会是先将第一条消息移到死信队列后再去移动第二条。当然这种情况可以去RabbitMQ官网安装一个叫“rabbitmq_delayed _message_exchange”的插件就可以解决。
死信交换机 VS 备用交换机
备用交换器: 1.消息无法路由时转到备用交换器 2.备用交换器是在声明主交换器的时候定义的
死信交换器: 1.消息已经到达队列,但是被消费者拒绝等的消息会转到死信交换器。2.死信交换器是在声明队列的时候定义的
死信队列使用场景:
一般用在较为重要的业务队列中,确保未被正确消费的消息不被丢弃,一般发生消费异常可能原因主要有由于消息信息本身存在错误导致处理异常,处理过程中参数校验异常,或者因网络波动导致的查询异常等等,当发生异常时,当然不能每次通过日志来获取原消息,然后让运维帮忙重新投递消息。
通过配置死信队列,可以让未正确处理的消息暂存到另一个队列中,待后续排查清楚问题后,编写相应的处理代码来处理死信消息,这样比手工恢复数据要好太多了。
延时队列:
在rabbitMQ中不存在延时队列,但是我们可以通过设置消息的过期时间和死信队列来模拟出延时队列。思路是,一条到了过期时间的消息还没有被消费,然后被投递到死信队列里,然后让消费者去消费死信队列的消息即可。具体做法是,生产者生产一条延时消息,根据需要延时时间的不同,利用不同的routingkey将消息路由到不同的延时队列,每个队列都设置了不同的TTL属性,并绑定在同一个死信交换机中,消息过期后,根据routingkey的不同,又会被路由到不同的死信队列中,消费者只需要监听对应的死信队列进行处理即可。
注:延时队列还有很多其它选择,比如利用Java的DelayQueue,利用Redis的zset,利用Quartz或者利用kafka的时间轮,这些方式各有特点。
延时队列的使用场景:
- 订单在十分钟之内未支付则自动取消。
- 新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。
- 账单在一周内未支付,则自动结算。
- 用户注册成功后,如果三天内没有登陆则进行短信提醒。
- 用户发起退款,如果三天内没有得到处理则通知相关运营人员。
- 预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议。