引入消息队列之后该如何保证其高可用性
采用ZooKeeper+Replicated LevelDB集群
异步投递Async Sends
背景
对于一个Slow Consumer,使用同步发送消息可能出现Producer堵塞的情况,慢消费者适合使用异步发送
概述
- ActiveMQ支持同步,异步两种发送的模式将消息发送到broker,模式的选择对发送延时有巨大的影响。producer能达到怎么样的产出率(产出率=发送数据总量/时间)主要受发送延时的影响,使用异步发送可以显著提高发送的性能。
- ActiveMQ默认使用异步发送的模式
- 除非明确指定使用同步发送的方式或者在未使用事务的前提下发送持久化的消息,这两种情况都是同步发送的。
- 如果你没有使用事务且发送的是持久化的消息,每一次发送都是同步发送的且会阻塞producer知道broker返回一个确认,表示消息已经被安全的持久化到磁盘。确认机制提供了消息安全的保障,但同时会阻塞客户端带来了很大的延时。
- 很多高性能的应用,允许在失败的情况下有少量的数据丢失。如果你的应用满足这个特点,你可以使用异步发送来提高生产率,即使发送的是持久化的消息。
- 异步发送
- 它可以最大化producer端的发送效率。我们通常在发送消息量比较密集的情况下使用异步发送,它可以很大的提升Producer性能;不过这也带来了额外的问题
- 就是需要消耗更多的Client端内存同时也会导致broker端性能消耗增加;
- 此外它不能有效的确保消息的发送成功。在userAsyncSend=true的情况下客户端需要容忍消息丢失的可能。
- 它可以最大化producer端的发送效率。我们通常在发送消息量比较密集的情况下使用异步发送,它可以很大的提升Producer性能;不过这也带来了额外的问题
官网配置方式
异步消息如何确定发送成功?
-
异步发送丢失消息的场景是:生产者设置
userAsyncSend=true
,使用producer.send(msg)
持续发送消息。 -
如果消息不阻塞,生产者会认为所有send的消息均被成功发送至MQ。
-
如果MQ突然宕机,此时生产者端内存中尚未被发送至MQ的消息都会丢失。
-
所以,正确的异步发送方法是需要接收回调的。
-
同步发送和异步发送的区别就在此
-
同步发送等send不阻塞了就表示一定发送成功了,
-
异步发送需要客户端回执并由客户端再判断一次是否发送成功
-
异步生产者示例
package com.demo; import org.apache.activemq.ActiveMQConnectionFactory; import org.apache.activemq.ActiveMQMessageProducer; import org.apache.activemq.AsyncCallback; import javax.jms.*; import java.util.UUID; /** * 异步投递 */ public class Producer { private static final String ACTIVEMQ_URL = "tcp://192.168.67.130:61616"; private static final String ACTIVEMQ_QUEUE_NAME = "Queue-异步投递回调"; public static void main(String[] args) throws JMSException { ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(); activeMQConnectionFactory.setBrokerURL(ACTIVEMQ_URL); //开启异步投递 activeMQConnectionFactory.setUseAsyncSend(true); Connection connection = activeMQConnectionFactory.createConnection(); connection.start(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); Queue queue = session.createQueue(ACTIVEMQ_QUEUE_NAME); //向上转型到ActiveMQMessageProducer ActiveMQMessageProducer activeMQMessageProducer = (ActiveMQMessageProducer) session.createProducer(queue); for (int i = 0; i < 3; i++) { TextMessage textMessage = session.createTextMessage("message-" + i); textMessage.setJMSMessageID(UUID.randomUUID().toString() + "----orderAtguigu"); String textMessageId = textMessage.getJMSMessageID(); //使用ActiveMQMessageProducer的发送消息,可以创建回调 activeMQMessageProducer.send(textMessage, new AsyncCallback() { @Override public void onSuccess() { System.out.println(textMessageId + "发送成功"); } @Override public void onException(JMSException exception) { System.out.println(textMessageId + "发送失败"); } }); } activeMQMessageProducer.close(); session.close(); connection.close(); } }
-
延迟投递和定时投递
四大属性
属性名称 | 类型 | 说明 |
---|---|---|
AMQ_SCHEDULED_DELAY | long | 延迟投递的时间 |
AMQ_SCHEDULED_PERIOD | long | 重复投递的时间间隔 |
AMQ_SCHEDULED_REPEAT | int | 重复投递次数 |
AMQ_SCHEDULED_CRON | String | Cron表达式 |
演示示例
(1)要在activemq.xml中配置schedulerSupport属性为true
(2)Java代码里面封装的辅助信息类型:ScheduledMessage
(3)示例代码
package com.demo;
import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.ActiveMQMessageProducer;
import org.apache.activemq.AsyncCallback;
import org.apache.activemq.ScheduledMessage;
import org.springframework.scheduling.annotation.Scheduled;
import javax.jms.*;
import java.util.UUID;
/**
* 延迟投递
*/
public class Producer_yanchi {
private static final String ACTIVEMQ_URL = "tcp://192.168.67.130:61616";
private static final String ACTIVEMQ_QUEUE_NAME = "Queue-延迟投递";
public static void main(String[] args) throws JMSException {
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory();
activeMQConnectionFactory.setBrokerURL(ACTIVEMQ_URL);
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Queue queue = session.createQueue(ACTIVEMQ_QUEUE_NAME);
//向上转型到ActiveMQMessageProducer
MessageProducer messageProducer = session.createProducer(queue);
long delay = 3 * 1000; //延迟投递的时间
long period = 4 * 1000; //每次投递的时间间隔
int repeat = 5; //投递的次数
for (int i = 0; i < 3; i++) {
TextMessage textMessage = session.createTextMessage("message-延时投递" + i);
//给消息设置属性以便MQ服务器读取到这些信息,好做对应的处理
textMessage.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY, delay);
textMessage.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_PERIOD, period);
textMessage.setIntProperty(ScheduledMessage.AMQ_SCHEDULED_REPEAT, repeat);
messageProducer.send(textMessage);
}
messageProducer.close();
session.close();
connection.close();
}
}
分发策略
Queue的分发策略
- 可插拔的分发策略只适用于topic。queue的分发策略比较固定:轮询(默认)或按照严格顺序。同时我们也应该了解prefect的意义。
- ActiveMQ的prefetch缺省参数是针对处理大量消息时的高性能和高吞吐量而设置的,因此默认的prefect值很大,默认的分发策略会尽快尝试将预取缓冲区填满(prefetch buffers)。
- 然而在有些情况下,例如只有少量的消息而且单个消息的处理时间比较长,那么在缺省的prefetch和dispatch policies下,这些少量的消息总是倾向于被分发到个别的consumer上。这样就会因为负载的不均衡分配而导致处理时间的增加。
- 对于队列,可以选择使用轮询分发策略(Round Robin Dispatch Policy)或按严格顺序分发策略(strictOrderDispatch)。
- strictOrderDispatch表示在直到当前消费者的prefetch缓冲区满了之后才选择下一个消费者进行消息的分发。
Topic的分发策略
-
所有实现了
org.apache.activemq.broker.region.policy.DispatchPolicy
的都可以。 -
默认实现是
org.apache.activemq.broker.region.policy.SimpleDispatchPolicy
,它将消息传递给所有的订阅者。 -
一个更高级的实现示例是
org.apache.activemq.broker.region.policy.PriorityNetworkDispatchPolicy
,它只会分发给拥有最高优先级的网络消费者。这在循环网络拓扑结构中非常有用,因为在这种拓扑结构中,到消费者的路由不止一条。 -
ActiveMQ Destination消息目的地策略说明
-
在节点destinationPolicy配置策略,可以对单个或者所有的主题和队列进行设置,使用流程监控,当消息达到
memoryLimit
的时候,ActiveMQ会减慢消息的产生甚至阻塞,destinationPolicy配置如下<!-- Destination specific policies using destination names or wildcards --> <!-- wildcards意义见http://activemq.apache.org/wildcards.html --> <destinationPolicy> <policyMap> <policyEntries> <!-- 这里使用了wildcards,表示所有以Msg开头的topic --> <policyEntry topic="Msg.>" producerFlowControl="false" memoryLimit="10mb"> <!-- 分发策略 --> <dispatchPolicy> <!-- 按顺序分发 --> <strictOrderDispatchPolicy/> </dispatchPolicy> <!-- 恢复策略--> <subscriptionRecoveryPolicy> <!-- 只恢复最后一个message --> <lastImageSubscriptionRecoveryPolicy/> </subscriptionRecoveryPolicy> </policyEntry> </policyEntries> </policyMap> </destinationPolicy>
-
producerFlowControl表示是否监控流量,默认为true;如果设置为false,消息就会存在磁盘中以防止内存溢出
-
memoryLimit表示在producerFlowControl=”true”的情况下,消息存储在内存中最大量,当消息达到这个值 时,ActiveMQ会减慢消息的产生甚至阻塞。
-
-
Destination策略配置示例
<destinationPolicy> <policyMap> <policyEntries> <policyEntry topic="FOO.>"> <dispatchPolicy> <roundRobinDispatchPolicy/> </dispatchPolicy> <subscriptionRecoveryPolicy> <lastImageSubscriptionRecoveryPolicy/> </subscriptionRecoveryPolicy> </policyEntry> <policyEntry topic="ORDERS.>"> <dispatchPolicy> <strictOrderDispatchPolicy/> </dispatchPolicy> <!-- 1 minutes worth --> <subscriptionRecoveryPolicy> <timedSubscriptionRecoveryPolicy recoverDuration="60000"/> </subscriptionRecoveryPolicy> </policyEntry> <policyEntry topic="PRICES.>"> <!-- lets force old messages to be discarded for slow consumers --> <pendingMessageLimitStrategy> <constantPendingMessageLimitStrategy limit="10"/> </pendingMessageLimitStrategy> <!-- 10 seconds worth --> <subscriptionRecoveryPolicy> <timedSubscriptionRecoveryPolicy recoverDuration="10000"/> </subscriptionRecoveryPolicy> </policyEntry> <policyEntry tempTopic="true" advisoryForConsumed="true"/> <policyEntry tempQueue="true" advisoryForConsumed="true"/> </policyEntries> </policyMap> </destinationPolicy>
ActiveMQ消息重试机制
具体哪些情况会引发消息重发
- Client用了transactions且再session中调用了rollback
- Client用了transactions且再调用commit之前关闭或者没有commit
- Client再CLIENT_ACKNOWLEDGE的传递模式下,session中调用了recover
请说说消息重发时间间隔和重发次数
- 间隔:1
- 次数:6
- 每秒发6次
有毒消息Poison ACK
- 一个消息被redelivedred超过默认的最大重发次数(默认6次)时,消费的回个MQ发一个“poison ack”表示这个消息有毒,告诉broker不要再发了。这个时候broker会把这个消息放到DLQ(私信队列)。
相关属性说明
死信队列
概述
ActiveMQ中引入了“死信队列”(Dead Letter Queue)的概念,即一条消息在被重发了多次后(默认重发6次,redeliveryCounter=6),将会被ActiveMQ移入“死信队列”,开发人员可以在这个Queue中查看处理出错的信息,进行人工干预。
死信队列的使用:处理失败的消息
- 一般生产环境中在使用MQ的时候设计两个队列:核心业务队列和死信队列
- 核心业务队列,就是类似上图专门用来让订单系统发送订单消息的,然后另一个死信队列就是用来处理异常情况的
- 假如第三方物流系统故障此时无法请求,那么仓储系统每次消费到一条订单消息,尝试通知发货和配送都会遇到对方的接口报错。此时仓储系统就可以把这条消息拒绝访问或者标志位处理失败。一旦标志着这条消息处理失败之后,MQ就会把这条消息转入提前设置好的一个私信队列中。然后会看到的就是,在第三方物流系统故障期间,所有订单消息全部处理失败,全部会转入死信队列。然后仓储系统得专门有一个后台线程,监控第三方物流系统是否正常,能否请求的、不停的监视。一旦发现对方恢复正常,这个后台线程就从私信队列消费处理失败的订单,重新执行发货和配送的通知逻辑。
死信队列的配置介绍
SharedDeadLetterStrategy
-
将所有的DeadLetter保存在一个共享的队列中,这是ActiveMQ broker端默认的策略
-
共享队列默认为:“ActiveMQ.DLQ”,可以通过“deadLetterQueue”属性来设定
<deadLetterStrategy> <sharedDeadLetterStrategy deadLetterQueue="DLQ_QUEUE"/> </deadLetterStrategy>
IndividualDeadLetterStrategy
-
把DeadLetter放入各自的死信队列中
- 对于Queue而言,死信队列的前缀默认为:“ActiveMQ.DLQ.Queue."
- 对于Topic而言,死信队列的前缀默认为:“ActiveMQ.DLQ.Topic”
-
例如队列Order,它对应的死信队列为“ActiveMQ.DLQ.Queue.Order"
-
可以使用“queuePrefix”、“topicPrefix”来指定上述前缀
-
默认情况下,无论是Topic还是Queue,broker将使用Queue来保存DeadLetter,即死信队列通常为Topic
<policyEntry queue="order"> <deadLetterStrategy> <individualDeadLetterStrategy queuePrefix="DLQ." useQueueForQueueMessages="false"/> </deadLetterStrategy> </policyEntry>
-
将队列Order中出现的DeadLetter保存在DLQ.Order中,不过此时的DLQ.Order为Topic
-
属性“useQueueForQueueMessages”,此值表示是否将Topic的DeadLetter保存在Queue中,默认为true
配置示例
(1)自动删除过期的消息
有时需要直接删除过期的消息而不需要发送到死信队列中,“processExpired”表示是否将过期消息放入死信队列,默认为true
<policyEntry queue=">">
<deadLetterStrategy>
<sharedDeadLetterStrategy processExpired="false"/>
</deadLetterStrategy>
</policyEntry>
(2)存放非持久消息到死信队列中
-
默认情况下,ActiveMQ不会把非持久额死信消息发送到死信队列中
-
“processNonPersistent”表示是否将“非持久化”消息放入死信队列,默认为false
-
非持久行如果你想把非持久的消息发送到死信队列中。需要设置属性
processNonPersistent=true
<policyEntry queue=">"> <deadLetterStrategy> <sharedDeadLetterStrategy processNonPersistent="true"/> </deadLetterStrategy> </policyEntry>
如何保证消息不被重复消费呢?
-
幂等性问题(重复消费)
-
网络延迟传输中,会造成进行MQ重试中,在重试过程中,可能会造成重复消费
-
如果消息是做数据库的插入操作,给这个消息做一个唯一主键,那么就算出现重复消费的情况,就会导致主键冲突,避免数据库出现脏数据
-
准备一个第三方服务方来做消费记录。以Redis为例,给消息分配一个全局ID,只要消费过该消息,将<id,message>以KV形式写入Redis。那么消费者开始消费前,先去Redis中查询有没有消费记录即可。