一、高级特性之异步投递
ctiveMQ官网对异步投递的描述:http://activemq.apache.org/async-sends
ActiveMQ支持同步投递和异步投递两种模式将消息发送到broker,模式的选择对发送的延时有巨大的影响。生产者能达到的产出率主要受发送延时的影响,使用异步发送可以显著提高发送的性能。
ActiveMQ默认采用异步发送的模式,除非明确指定使用同步发送方式,或者在未使用事务的前提下发送持久化消息,这两种情况都是同步发送的。
如果没有使用事务且发送的是持久化的消息,每一次发送都是同步发送且会阻塞生产者,直到broker返回一个确认信息,表示信息已经被安全的持久化到硬盘了。确认机制提供了消息安全的保障,但同时会阻塞客户端带来很大延时。
许多高性能的应用,允许在失败的情况下有少量数据丢失。如果可以接受这一点,建议使用异步发送来挺高生产率,即使发送的是持久化的消息。
异步发送
可以最大化producer生产者的发送效率,通常在发送消息量比较密集的情况下使用异步发送,可以很大程度提升生产者的性能,不过也带来了额外的问题,这需要消耗较多的Client端内存,同时会导致broker性能消耗增加,这种方式也不能有效的保证消息发送成功,在useAsyncSend=true的情况下,客户端需要容忍消息丢失的可能。
如果使用同步方式,假设此时有一个慢消费者,那么就会导致生产者阻塞,这不是我们希望看到的,所以,如果有慢消费者时,要采用异步发送。
三种类开启异步投递的方法:
1.ACTIVEMQ_URL后面跟上参数jms.useAsyncSend=true。
//方式一
public static final String ACTIVEMQ_URL = "tcp://10.5.96.48:61616?jms.useAsyncSend=true";
public static final String QUEUE_NAME = "AsyncSend";
2.使用connectionFactory的set方法,((ActiveMQConnectionFactory)connectionFactory).setUseAsyncSend(true);
public static void main(String[]args) throws JMSException {
//1.创建连接工厂
//异步投递方式二
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
activeMQConnectionFactory.setAlwaysSyncSend(true);
3.使用connection的set方法,((ActiveMQConnection)connection).setUseAsyncSend(true);
//异步投递方式三
ActiveMQConnection connection = (ActiveMQConnection) activeMQConnectionFactory.createConnection();
connection.setUseAsyncSend(true);
二、高级特性之异步投递如何确认发送成功
异步发送丢失场景:生产者设置了useAsyncSend=true,使用producer.send(message);持续发送消息。由于消息不阻塞,生产者会认为所有send的消息都被成功发送到MQ了。如果此时MQ突然宕机,生产者端内存中尚未发送至MQ的消息就会丢失。
所以,正确的异步发送方法是需要接收回调函数的。
同步和异步的区别就在这里,同步发送send不阻塞就一定发送成功,异步发送需要接收回调函数并由客户端再判断一次是否发送成功。
package com.wsy.activemq;
import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.ActiveMQMessageProducer;
import org.apache.activemq.AsyncCallback;
import javax.jms.*;
import java.util.UUID;
public class JmsProducer_Async {
public static final String ACTIVEMQ_URL = "tcp://10.5.96.48:61616";
public static final String QUEUE_NAME = "queue_asyncSend";
public static void main(String[] args) throws JMSException {
// 创建连接工厂,按照给定的url地址采用默认的用户名和密码
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
// 设置消息发送模式是AsyncSend模式
activeMQConnectionFactory.setUseAsyncSend(true);
// 通过连接工厂,获取Connection并启动
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
// 创建Session
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 创建目的地(目的地有两个子接口,分别是Queue和Topic)
Queue queue = session.createQueue(QUEUE_NAME);
// 创建消息生产者,生产的消息放到queue中
ActiveMQMessageProducer activeMQMessageProducer = (ActiveMQMessageProducer) session.createProducer(queue);
for (int i = 0; i < 3; i++) {
TextMessage textMessage = session.createTextMessage("message-" + i);
textMessage.setJMSMessageID(UUID.randomUUID().toString());
String messageID = textMessage.getJMSMessageID();
// 设置回调函数,通过回调函数判断是否发送成功,成功走onSuccess()方法,失败走onException()方法
activeMQMessageProducer.send(textMessage, new AsyncCallback() {
@Override
public void onSuccess() {
System.out.println(messageID + " success!");
}
@Override
public void onException(JMSException e) {
System.out.println(messageID + " fail!");
}
});
}
// 按照资源打开的相反顺序关闭资源
activeMQMessageProducer.close();
session.close();
connection.close();
}
}
三、高级特性之延迟投递和定时投递
ActiveMQ官网对延迟投递和定时投递的描述:http://activemq.apache.org/delay-and-schedule-message-delivery.html
broker的schedulerSupport有4个属性。
Property name | type | description |
AMQ_SCHEDULED_DELAY | long | 延迟投递时间 |
AMQ_SCHEDULED_PERIOD | long | 重复投递时间间隔 |
AMQ_SCHEDULED_REPEAT | int | 重复投递次数 |
AMQ_SCHEDULED_CRON | String | cron表达式 |
延迟投递或定时投递默认是不开启的,所以需要在activemq.xml配置文件的broker结点加上schedulerSupport="true"属性来开启。修改完配置文件后,记得重启ActiveMQ。
<broker xmlns="http://activemq.apache.org/schema/core" brokerName="localhost" dataDirectory="${activemq.data}" schedulerSupport="true">
修改Producer的代码 ,延时3秒,间隔4秒发一次,发5次。
public class JmsProduce {
public static final String ACTIVEMQ_URL = "tcp://10.5.96.48:61616";
public static final String QUEUE_NAME = "delayAndSchedule";
public static void main(String[]args) throws JMSException {
//1.创建连接工厂
//异步投递方式二
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2.通过连接工厂,获得连接connection并启动访问
//异步投递方式三
ActiveMQConnection connection = (ActiveMQConnection) activeMQConnectionFactory.createConnection();
connection.start();
//3.创建会话 session
//两个桉树 ,第一个叫事务 第二个叫签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//4 .创建目的地(具体是队列还是主题topic)
Queue queue = session.createQueue(QUEUE_NAME);
//5. 创建消息的生产者
ActiveMQMessageProducer activeMQMessageProducer = (ActiveMQMessageProducer) session.createProducer(queue);
//消息的持久化设置(现在是非持久)
activeMQMessageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);
//6.通过使用messageProducer 生产3条消息发送到队列里面
for (int i = 0; i <3 ; i++) {
// 7.创建消息
TextMessage message = session.createTextMessage("Msg---" + i);
//延时3秒,间隔4秒,投递5次
message.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY,3000);
message.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_PERIOD,4000);
message.setIntProperty(ScheduledMessage.AMQ_SCHEDULED_REPEAT,5);
//8.消息生产者发送给mq
activeMQMessageProducer.send(message);
}
//9.关闭资源 顺着开启,倒着关闭
activeMQMessageProducer.close();
session.close();
connection.close();
System.out.println("**** 消息发布到MQ完毕 ****");
}
}
四、高级特性之消费重试机制
具体哪些情况会引起消息重发?
- Client用了事务且在session中调用了rollback()
- Client用了事务且在调用commit()前关闭或者没有commit()
- Client在CLIENT_ACKNOWLEDGE的传递模式下,在session中调用了recover()
消息重发的间隔和重发次数是多少?
消息重发的间隔是每秒钟重发6次。
对有毒消息Poison ACK的理解。
一个消息被redelivedred超过默认的最大重发次数(默认6次)时,消费端会给MQ发送一个poison ack表示这条消息有毒,告诉broker不要再发了。这时候broker会把这条消息放到死信队列中。
ActiveMQ官网对重试机制的解释:http://activemq.apache.org/redelivery-policy
initialRedeliveryDelay和maximumRedeliveries是最常用的两个参数。
Property | Default Value | Description |
backOffMultiplier | 5 | 重连时间间隔递增倍数,只有大于1和启用useExponentialBackOff参数时才生效。 |
collisionAvoidanceFactor | 0.15 | 设置防止冲突范围的正负百分比,只有启用useCollisionAvoidance参数时,才生效,在延迟时间上再加一个时间的波动范围。 |
initialRedeliveryDelay | 1000L | 初始重发延迟时间。 |
maximumRedeliveries | 6 | 最大重试次数,达到最大重连次数后抛出异常,-1表示不限制次数,0表示不进行重传。 |
maximumRedeliveryDelay | -1 | 最大传送延迟,只在useExponentialBackOff为true时有效。假设重连间隔10ms,倍数为2,第二次重连间隔为20ms,第三次重连间隔是40ms,当重连最大时间间隔达到直达重连时间间隔时,以后每次重连时间间隔都为最大重连时间间隔。 |
redeliveryDelay | 1000L | 重发延迟时间,当initialRedeliveryDelay=0时生效。 |
useCollisionAvoidance | false | 启用防止冲突功能。 |
useExponentialBackOff | false | 启用指数倍数递增的方式增加延迟时间。 |
创建JmsProducer_Redelivery.java和JmsConsumer_Redelivery.java。对于consumer,开启事务,并注释掉commit(),故意不执行commit()。
package com.wsy.activemq;
import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.ScheduledMessage;
import javax.jms.*;
public class JmsProducer_Redelivery {
public static final String ACTIVEMQ_URL = "tcp://10.5.96.48:61616";
public static final String QUEUE_NAME = "Redelivery";
public static void main(String[] args) throws JMSException {
// 创建连接工厂,按照给定的url地址采用默认的用户名和密码
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
// 通过连接工厂,获取Connection并启动
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
// 创建Session
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 创建目的地(目的地有两个子接口,分别是Queue和Topic)
Queue queue = session.createQueue(QUEUE_NAME);
// 创建消息生产者,生产的消息放到queue中
MessageProducer messageProducer = session.createProducer(queue);
// 使用messageProducer生产消息发送到队列中
for (int i = 0; i < 3; i++) {
// 创建一条消息,可以理解成字符串
TextMessage textMessage = session.createTextMessage("message-" + i);
messageProducer.send(textMessage);
}
// 按照资源打开的相反顺序关闭资源
messageProducer.close();
session.close();
connection.close();
}
}
package com.wsy.activemq;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
public class JmsConsumer_Redelivery {
public static final String ACTIVEMQ_URL = "tcp://192.168.0.123:61616";
public static final String QUEUE_NAME = "Redelivery";
public static void main(String[] args) throws JMSException {
// 创建连接工厂,按照给定的url地址采用默认的用户名和密码
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
// 通过连接工厂,获取Connection并启动
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
// 创建Session
Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
// 创建目的地(目的地有两个子接口,分别是Queue和Topic)
Queue queue = session.createQueue(QUEUE_NAME);
// 创建消费者,指明从queue取消息
MessageConsumer messageConsumer = session.createConsumer(queue);
while (true) {
// 因为向队列中存放的是TextMessage的实例,所以取出来的时候,也要用TextMessage的实例来接收
// 这里的receive()方法表示一直等待,如果给它传一个long类型的毫秒数,表示consumer等待超时时间
TextMessage textMessage = (TextMessage) messageConsumer.receive(1000L);
if (textMessage != null) {
System.out.println("消费者消费:"+textMessage.getText());
} else {
break;
}
}
// 上面开启了事务,正常情况下,必须执行commit();这里模拟出问题的情况,注释掉commit();
// session.commit();
// 按照资源打开的相反顺序关闭资源
messageConsumer.close();
session.close();
connection.close();
}
}
运行Producer,运行Consumer。如果程序正确的情况下,这一次消息的发送和消费就完成了。因为consumer里没有执行commit,对于broker来说,它没有收到consumer的反馈,它认为consumer并没有消费掉消息。
此时,再启动consumer,会触发重发机制,consumer依旧会消费到消息,但是不告诉broker。再启动consumer6次,发现第6次收不到消息了,意味着broker已经把消息放到死信队列里了。也就是在第7次重试的时候,收不到消息了。
此时,打开管理页面的queue标签,会发现多了一个DLQ(死信队列)。
如果希望修改重发策略,也就是用自定义值覆盖默认值,使用RedeliveryPolicy这个类的实例,修改实例的属性值,最后再将实例设置到ActiveMQConnectionFactory实例的RedeliveryPolicy属性上即可,这里以修改重试次数为例,其他属性的修改方法类似,具体看代码。在消费者中添加
// 手动修改RedeliveryPolicy(重发策略)
RedeliveryPolicy redeliveryPolicy = new RedeliveryPolicy();
redeliveryPolicy.setMaximumRedeliveries(3);// 修改重发次数为3次
activeMQConnectionFactory.setRedeliveryPolicy(redeliveryPolicy);// 将重发策略设置到ConnectionFactory中
如果项目是和Spring整合的,可以将这些属性值设置到Spring的配置文件中。
<!--定义RedeliveryPolicy(重发机制)-->
<bean id="activeMQRedeliveryPolicy" class="org.apache.activemq.RedeliveryPolicy">
<property name="useCollisionAvoidance" value="false"/>
<property name="useExponentialBackOff" value="true"/>
<property name="maximumRedeliveries" value="3"/>
<property name="initialRedeliveryDelay" value="1000"/>
<property name="backOffMultiplier" value="2"/>
<property name="maximumRedeliveryDelay" value="1000"/>
</bean>
<!--创建连接工厂-->
<bean id="connectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
<property name="brokerURL" value="tcp://localhost:61616"/>
<!--引用自定义重发机制-->
<property name="redeliveryPolicy" ref="activeMQRedeliveryPolicy"/>
</bean>
五、高级特性之死信队列
ActiveMQ对死信队列的介绍:http://activemq.apache.org/message-redelivery-and-dlq-handling
ActiveMQ中引入了死信队列的概念,一条消息被重复发了多次(默认重发6次redeliveryCounter=6),这条消息将会被ActiveMQ加入死信队列。开发人员可以在死信队列中查看出错的消息,进行人工干预。
通常生产环境中,使用MQ会设计两个队列,一个是业务队列,一个是死信队列。
假如第三方物流系统故障,仓储系统每次消费一条订单消息,尝试通知发货和配送,都会遇到对方接口报错,此时仓储系统就可以把这条消息标记为处理失败。一旦标记为处理失败后,MQ会把这条消息转入提前设置好的死信队列。在物流系统故障期间,所有订单全部处理失败,全部会转入死信队列。仓储系统有一个后台线程,监控第三方系统是否正常,当后台线程发现物流系统恢复正常后,后台线程就从死信队列里拿出失败的订单继续处理,重新执行发货和配送的逻辑。
SharedDealLetterStrategy:共享死信队列策略
所有的DeadLetter保存在一个共享的队列中,这是ActiveMQ broker端的默认策略。默认为ActiveMQ.DLQ,可以通过deadLetterQueue属性来设置。
<dealLetterStrategy>
<sharedDeadLetterStrategy deadLetterQueue="MyDeadLetterQueue">
</dealLetterStrategy>
IndividualDeadLetterStrategy:独特死信队列策略
对于Queue而言,死信通道前缀默认为“ActiveMQ.DLQ.Queue”。
对于Topic而言,死信通道的前缀默认为“ActiveMQ.DLQ.Topic”。
我们使用默认前缀+队列或者主题的名称来指定死信队列。比如:ActiveMQ.DLQ.Queue.MyQueue和ActiveMQ.DLQ.Topic.MyTopic。
默认情况下,无论是Topic还是Queue,broker默认都会使用Queue来保存DeadLetter,即死信队列通常为Queue,开发者也可以指定为Topic。
<policyEntry queue="order">
<deadLetterStrategy>
<individualDeadLetterStrategy queuePrefix="DLQ." useQueueForQueueMessages="true"/>
</deadLetterStrategy>
</policyEntry>
如果需要直接删除过期的消息,而不需要发送到死信队列,那么设置processExpired参数为false,这个参数的默认值是true的。
<deadLetterStrategy>
<sharedDeadLetterStrategy processExpired="false"/>
</deadLetterStrategy>
默认情况下,ActiveMQ不会把非持久的死信存放到死信队列,如果希望存放非持久的死信,那么需要修改processNonPersistent参数为true,这个参数默认值是fasle。
<deadLetterStrategy>
<sharedDeadLetterStrategy processNonPersistent="true"/>
</deadLetterStrategy>
六、高级特性之防止重复调用
网络延迟传输中,会进行MQ的重试,可能会出现重复消费。
如果一条消息是做数据库的插入操作,那么给这个消息做一个唯一主键,如果出现了重复消息,从数据库主键唯一性层面,导致主键冲突,可以避免重复插入数据。
可以使用第三方服务来做消费记录,以redis为例,给消息分配一个全局id,只要消息消费过,将<id,message>写入redis,当消费者开始消费之前,先去redis里查看有没有消费记录。
如果有消费记录,那么这条消息已经被处理过,否则,执行消费逻辑,并将<id,message>存到redis里。
参考
https://blog.csdn.net/qq_36059561/article/details/104264405
https://blog.csdn.net/qq_36059561/article/details/104264991
https://blog.csdn.net/qq_36059561/article/details/104266874
https://blog.csdn.net/qq_36059561/article/details/104270551