一、异步投递
ActiveMQ支持两种消息投递方式:同步投递和异步投递。消息投递指的是消息生产者端将消息发送到消息服务器(即Broker)的过程,若采用同步投递的方式,则生产者端每次向消息服务器发送消息时都需要同步地等待消息服务器给予消息发送成功与否的回执,这一定程度上可能会引起消息生产者的阻塞,影响消息发送的效率;采用异步投递的方式则不会有这个问题,因此ActiveMQ默认采用异步投递的方式。
使用异步投递的方式怎么确定消息投递成功与否呢?ActiveMQ为我们提供了回调机制,在我们向消息服务器发送消息时可以同时传递一个异步回调方法,无论消息是否投递成功消息服务器都会调用这个回调方法用于通知消息生产者这个消息投递的状态。
示例:
public class MQProducer {
private static final String BROKER_URL = "tcp://192.168.2.107:61616";
private static final String QUEUE_NAME = "queue-async";
public static void main(String args[]) throws JMSException {
//注意此处ConnectionFactory的类型必须是ActiveMQConnectionFactory
ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(BROKER_URL);
factory.setUseAsyncSend(true);//设置为异步投递,默认即为true
Connection connection = factory.createConnection();
connection.start();
//创建session
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//创建目的地
Queue queue = session.createQueue(QUEUE_NAME);
//创建消息生产者
ActiveMQMessageProducer producer = (ActiveMQMessageProducer) session.createProducer(queue);
try {
for (int i = 0; i < 3; i++) {
TextMessage textMessage = session.createTextMessage("msg" + i);
//发送消息时传入回调
producer.send(textMessage, new AsyncCallback() {
@Override
public void onSuccess() {
try {
System.out.println(textMessage.getJMSMessageID() + " 发送成功!");
} catch (JMSException e) {
e.printStackTrace();
}
}
@Override
public void onException(JMSException e) {
try {
System.out.println(textMessage.getJMSMessageID() + " 发送失败!");
} catch (JMSException je) {
je.printStackTrace();
}
}
});
}
//提交事务
//session.commit();
} catch (Exception e) {
//回滚事务
//session.rollback();
} finally {
producer.close();
session.close();
connection.close();
}
}
}
Tip
:
1️⃣有三种显示设置消息投递模式的方式,如下所示:
//1、在brokerUri后跟jms.useAsyncSend参数
cf = new ActiveMQConnectionFactory("tcp://locahost:61616?jms.useAsyncSend=true");
//2、设置connectionFactory的useAsyncSend属性,此时要注意ConnectionFactory的类型必须是ActiveMQConnectionFactory
((ActiveMQConnectionFactory)connectionFactory).setUseAsyncSend(true);
//3、设置Connection的useAsyncSend属性,此时要注意Connection的类型必须是ActiveMQConnection
((ActiveMQConnection)connection).setUseAsyncSend(true);
2️⃣回调方法是由消息服务器调用的,用以通知消息生产者消息投递的状态。因此在消息服务器宕机时肯定不会调用回调方法(服务器已经宕机了,是无法工作的),反过来说,如果某个消息发送后的回调没有被调用则说明可能该消息发送时消息服务器宕机了,该消息就很可能发送失败。那回调方法在什么情况下会被调用呢?一个是消息投递成功;一个是消息投递因非消息服务器宕机这个原因导致的投递失败时(比如超时)
那什么情况下ActiveMQ会使用同步投递的方式呢?一是明确指定使用同步投递的方式;二是在未使用事务时发送持久化的消息,此时消息生产者会阻塞直到Broker返回一个确认,表明消息已经被安全的持久化到磁盘。
异步投递可以显著的提高消息发送的性能,但是也带来了额外的问题。比如会消耗较多的Client端内存同时也会导致Broker端性能消耗增加,此外它不能有效的确保消息发送成功,客户端需要容忍消息丢失的可能。
二、延迟投递和定时投递
ActiveMQ还支持消息的延迟投递和定时投递,这也是针对于消息生产者的。ActiveMQ为我们提供的 ScheduledMessage 类描述了延迟投递和定时投递的四种属性:
属性名 | 数据类型 | 描述 |
---|---|---|
AMQ_SCHEDULED_DELAY | long | 延迟投递的时间,单位为毫秒 |
AMQ_SCHEDULED_PERIOD | long | 重复投递的时间间隔,单位为毫秒 |
AMQ_SCHEDULED_REPEAT | int | 重复投递的次数 |
AMQ_SCHEDULED_CRON | String | 使用CRON表达式设置定时投递规则 |
ActiveMQ默认是不支持延迟投递和定时投递的,需要我们更改activemq.xml配置文件,使其支持延迟投递和定时投递:给<broker>节点添加schedulerSuppoort属性,并将属性值置为true即可
<broker xmlns="http://activemq.apache.org/schema/core" brokerName="localhost" dataDirectory="${activemq.data}" schedulerSupport="true">
...
</broker>
示例1:延迟投递
public class MQProducer {
private static final String BROKER_URL = "tcp://192.168.2.107:61616";
private static final String QUEUE_NAME = "queue-delay";
public static void main(String args[]) throws JMSException {
ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(BROKER_URL);
Connection connection = factory.createConnection();
connection.start();
//创建session
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//创建目的地
Queue queue = session.createQueue(QUEUE_NAME);
//创建消息生产者
ActiveMQMessageProducer producer = (ActiveMQMessageProducer) session.createProducer(queue);
try {
TextMessage textMessage = session.createTextMessage("delayMsg");
//设置和延迟投递相关的消息属性
textMessage.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY, 5000);//延迟5秒投递
textMessage.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_PERIOD, 2000);//每隔2秒投递1次这个消息
textMessage.setIntProperty(ScheduledMessage.AMQ_SCHEDULED_REPEAT, 3);//重复投递3次,加上正常投递的1次,共4次
//发送消息
producer.send(textMessage);
//提交事务
//session.commit();
} catch (Exception e) {
//回滚事务
//session.rollback();
} finally {
producer.close();
session.close();
connection.close();
}
}
}
示例2:定时投递,使用CRON
public class MQProducer {
private static final String BROKER_URL = "tcp://192.168.2.107:61616";
private static final String QUEUE_NAME = "queue-delay";
public static void main(String args[]) throws JMSException {
ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(BROKER_URL);
Connection connection = factory.createConnection();
connection.start();
//创建session
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//创建目的地
Queue queue = session.createQueue(QUEUE_NAME);
//创建消息生产者
ActiveMQMessageProducer producer = (ActiveMQMessageProducer) session.createProducer(queue);
try {
TextMessage textMessage = session.createTextMessage("delayMsgcron");
//textMessage.setStringProperty(ScheduledMessage.AMQ_SCHEDULED_ID, UUID.randomUUID().toString());
//设置和定时投递的CRON
textMessage.setStringProperty(ScheduledMessage.AMQ_SCHEDULED_CRON,"* * * * *");
//发送消息
producer.send(textMessage);
//提交事务
//session.commit();
} catch (Exception e) {
//回滚事务
//session.rollback();
} finally {
producer.close();
session.close();
connection.close();
}
}
}
Tip
1️⃣设置消息属性时要注意属性的数据类型要和表格中一致
2️⃣注意重复投递的次数+1才是总共投递的次数
3️⃣CRON表达式的优先级高于另外三个参数,如果在设置了CRON的同时,也有repeat和period参数,则会在每次CRON执行的时候,重复投递repeat次,每次间隔为period,就是说设置是叠加的效果
4️⃣无论是延迟投递还是定时投递,实际上都是消息生产者给消息服务器安排了一个消息发送的任务(注意并不是消息生产者自己在做消息发送的作业,而是消息生产者会将消息发送的作业交给消息服务器去完成,在将任务交给消息服务器后,消息生产者就可以断开与消息服务器的连接了,而不是一直阻塞着等待所有的消息发送完成),这个(或者说这些)任务可以从管理控制台的 Scheduled 项中看到。在消息服务器将这个任务完成之后会删除该任务,也就是说那些有限次数的任务最终是会被删除的,而那些无限次数的任务则会一直保留在 Scheduled 中
5️⃣此处的CRON表达式并非Quartz框架中的CRON表达式,而是Linux中的Crontab表达式,规则请参考:Linux之crontab定时任务
三、消费重试机制
在消息的消费过程中,如果消息未被签收或者签收失败,是会导致消息重复消费的,但如果消息一直签收失败,那是不是就会被无限次的消费呢?答案是否定的。一条消息签收不成功,消息服务器就会认为该消费者没有消费过这条消息,就会再次将这条消息传送给该消费者供它消费。至于会传送几次取决于我们定义的消费重试机制。很显然消费重试机制是针对消费者端的。
哪些情况下会发生消息的重复消费呢?其实就是客户端消息签收失败的情况下,这包括但不限于以下情况:
1️⃣消费者端开启事务,但最终事务回滚而未提交,或者在提交之前关闭了连接而提交失败
2️⃣需要手动签收(CLENT_ACKNOWLEDGE)的消息,消费者端在签收之后又调用了 session.recover();
在默认情况下,当消息签收失败时ActiveMQ消息服务器会继续每隔1秒钟向消费者端发送一次这个签收失败的消息,默认会尝试6次(加上正常的1次共7次),如果这7次消费者端全部签收失败,则会给ActiveMQ服务器发送一个“poison ack”,表示这个消息不正常(“有毒”),这时消息服务器不会继续传送这个消息给这个消费者,而是将这个消息放入死信队列(DLQ,即Dead Letter Queue)。
消费重试机制的默认相关配置如下:
配置说明:
1、collisionAvoidanceFactor:设置防止冲突范围的正负百分比,只有启用useCollisionAvoidance参数时才生效,也就是在延迟时间上再加一个时间波动范围,默认值为0.15
2、maximumRedeliveries:最大重传次数,达到最大重传次数后抛出异常。值为-1时不限制次数,为0时不重传,默认值为6
3、maximumRedeliveryDelay:最大重连时间间隔,只在useExponentialBackOff为true是有效。假设首次重连间隔为10ms,倍数为2,那么第2次重连的时间间隔为20ms,第3次重连的时间间隔为40ms,当重连时间间隔大于最大重连时间间隔时,以后每次重连的时间间隔都是设置的最大重连时间间隔。默认值为-1,表示没有最大重连时间间隔
4、initialRedeliveryDelay:初始的重发时间间隔,即正常发送签收失败后间隔多长时间进行重发,默认值为1000L
5、redeliveryDelay:重发延迟时间,当initialRedeliveryDelay=0是有效,默认1000L
6、useCollisionAvoidance:启用防止冲突功能,默认false
7、useExponentialBackOff:启用指数倍数递增的方式增加重发延迟时间,默认false
8、backOffMultiplier:重连时间间隔的递增倍数,只有值大于1和启用useExponentialBackOff参数时生效,默认为5
自定义消费重试示例:只需要在消费者端设置重试策略
ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(BROKER_URL);
//自定义消费重试机制
RedeliveryPolicy policy = new RedeliveryPolicy();
policy.setMaximumRedeliveries(3);
factory.setRedeliveryPolicy(policy);
Connection connection = factory.createConnection();
connection.start();
...
Tip
:测试时,持久化方式采用jdbcPersistenceAdapter时重试策略不起作用,改为kahaDB时生效,其他方式没有测试,不知道这是不是一个缺陷。
消费重试失败的消息会被放入到死信队列中:
其他方式的重试策略设置可以参考官方文档:
与Spring的整合:
<bean id="activeMQRedeliveryPolicy" class="org.apache.activemq.RedeliveryPolicy">
<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中引入了“死信队列”(Dead Letter Queue)的概念,在一条消息被重复发送给消息消费者端多次(默认为6次)后,若一直签收不成功,则ActiveMQ会将这条消息移入到“死信队列”。开发时可以开启一个后台线程监听这个队列(默认死信队列的名称为ActiveMQ.DLQ)中的消息,进行人工干预,也就是说死信队列的作用主要是处理签收失败的消息。
关于死信队列的配置主要有两种:SharedDeadLetterStrategy和IndividualDeadLetterStrategy
1️⃣SharedDeadLetterStrategy:共享的死信队列配置策略,将所有的DeadLetter保存在一个共享的队列中,这是ActiveMQ Broker端的默认策略。共享队列的名称默认为“ActiveMQ.DLQ”,可以通过“deadLetterQueue”属性来设定:在activemq.xml中的<policyEntries>节点中配置
<deadLetterStrategy>
<sharedDeadLetterStrategy deadLetterQueue="DLQ-QUEUE"/>
</deadLetterStrategy>
2️⃣IndividualDeadLetterStrategy:单独的死信队列配置策略,把DeadLetter放入各自的死信通道中。对于Queue而言,死信通道的前缀默认为“ActiveMQ.DLQ.Queue”;对于Topic而言,死信通道的前缀默认为“ActiveMQ.DLQ.Topic”。比如队列Order,那么它对应的死信通道为“ActiveMQ.DLQ.Queue.Order”。我们可以使用queuePrefix和topicPrefix来指定上述前缀:
<!-- 仅对与order队列起作用 -->
<policyEntry queue="order">
<deadLetterStrategy>
<!-- useQueueForQueueMessage属性的作用:是否将名为order的Topic中的DeadLetter也保存在该队列中,默认为true -->
<individualDeadLetterStrategy queuePrefix="DLQ." useQueueForQueueMessage="false"/>
</deadLetterStrategy>
</policyEntry>
注意
:默认情况下,无论是Topic还是Queue,Broker都使用Queue来保存DeadLetter,即死信通道通常为Queue,不过开发时也可以指定为Topic
配置案例1:自动删除过期消息,此时对于过期的消息将不会被放入到死信队列,而是自动删除,>表示对所有队列起作用,processExpired表示是否将过期消息放入死信队列,默认为true
<!-- >表示对所有队列起作用 -->
<policyEntry queue=">">
<deadLetterStrategy>
<sharedDeadLetterStrategy processExpired="false"/>
</deadLetterStrategy>
</policyEntry>
配置案例2:将签收失败的非持久消息也放入到死信队列,默认情况下,ActiveMQ不会把非持久化的死消息放入死信队列,processNonPersistent表示是否将非持久化消息放入死信队列,默认为false
<!-- >表示对所有队列起作用 -->
<policyEntry queue=">">
<deadLetterStrategy>
<sharedDeadLetterStrategy processNonPersistent="true"/>
</deadLetterStrategy>
</policyEntry>
五、防止重复调用引发的问题
ActiveMQ中的消息有时是会被重复消费的,而我们消费消息时大都会在拿到消息后去调用其他的方法,比如说将消息的内容解析为一个对象保存到数据库中。一旦发生消息的重复消费时就会重复保存,这是有问题的,因此我们需要考虑如何防止重复调用。其实我们是没有办法防止重复调用的,只能在重复调用时进行消息是否重复消费的校验,当然对于幂等性接口也可以不进行校验。
那如何进行校验呢?有很多种方式,比如说我们将消费过的消息的messageId保存到数据库,每次消费消息前先到数据库中查一下该消息是否已被消费。在分布式系统中,也可以将消费过的消息放入redis中,以messageId作为key,message对象作为value(其实value不重要,当然也要看需求本身),在消费消息时先从redis中查找该消息是否已被消费。