4、消费者策略:Dispatch Async
讨论完了消息生产者的关键性能点,我们再将目光转向消息消费者(接收者端);就像本小节开始时描述的那样,比起消息生产者来说消息消费者的性能更能影响ActiveMQ系统的整体性能,因为要成功完成一条消息的处理,它的工作要远远多于消息生产者。
首先,在默认情况下ActiveMQ服务端采用异步方式向客户端推送消息。也就是说ActiveMQ服务端在向某个消费者会话推送消息后,不会等待消费者的响应信息,直到消费者处理完消息后,主动向服务端返回处理结果。如果您对自己的消费者性能足够满意,也可以将这个过程设置为“同步”:
......
// 设置为同步
connectionFactory.setDispatchAsync(false);
......
- 1
- 2
- 3
- 4
5、消费者策略:Prefetch
消费者关键策略中,需要重点讨论的是消费者“预取数量”——prefetchSize。可以想象,如果消费者端的工作策略是按照某个周期(例如1秒),主动到服务器端一条一条请求新的消息,那么消费者的工作效率一定是极低的;所以ActiveMQ系统中,默认的策略是ActiveMQ服务端一旦有消息,就主动按照设置的规则推送给当前活动的消费者。其中每次推送都有一定的数量限制,这个限制值就是prefetchSize。
针对Queue工作模型的队列和Topic工作模型的队列,ActiveMQ有不同的默认“预取数量”;针对NON_PERSISTENT Message和PERSISTENT Message,ActiveMQ也有不同的默认“预取数量”:
- PERSISTENT Message—Queue:prefetchSize=1000
- NON_PERSISTENT Message—Queue:prefetchSize=1000
- PERSISTENT Message—Topic:prefetchSize=100
- NON_PERSISTENT Message—Topic:prefetchSize=32766
ActiveMQ中设置的各种默认预取数量一般情况下不需要进行改变。如果您使用默认的异步方式从服务器端推送消息到消费者端,且您对消费者端的性能有足够的信心,可以加大预取数量的限制。但是非必要情况下,请不要设置prefetchSize=1,因为这样就是一条一条的取数据;也不要设置为prefetchSize=0,因为这将导致关闭服务器端的推送机制,改为客户端主动请求。
- 可以通过ActiveMQPrefetchPolicy策略对象更改预取数量
......
// 预取策略对象
ActiveMQPrefetchPolicy prefetchPolicy = connectionFactory.getPrefetchPolicy();
// 设置Queue的预取数量为50
prefetchPolicy.setQueuePrefetch(50);
connectionFactory.setPrefetchPolicy(prefetchPolicy);
//进行连接
connection = connectionFactory.createQueueConnection();
connection.start();
......
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 也可以通过Properties属性更改(当然还可以加入其他属性)预取数量:
......
Properties props = new Properties();
props.setProperty("prefetchPolicy.queuePrefetch", "1000");
props.setProperty("prefetchPolicy.topicPrefetch", "1000");
//设置属性
connectionFactory.setProperties(props);
//进行连接
connection = connectionFactory.createQueueConnection();
connection.start();
......
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
6、消费者策略:事务和死信
6.1、消费者端事务
JMS规范除了为消息生产者端提供事务支持以外,还为消费服务端准备了事务的支持。您可以通过在消费者端操作事务的commit和rollback方法,向服务器告知一组消息是否处理完成。采用事务的意义在于,一组消息要么被全部处理并确认成功,要么全部被回滚并重新处理。
......
//建立会话(采用commit方式确认一批消息处理完毕)
session = connection.createSession(true, Session.SESSION_TRANSACTED);
//建立Queue(当然如果有了就不会重复建立)
sendQueue = session.createQueue("/test");
//建立消息发送者对象
MessageConsumer consumer = session.createConsumer(sendQueue);
consumer.setMessageListener(new MyMessageListener(session));
......
class MyMessageListener implements MessageListener {
private int number = 0;
/**
* 会话
*/
private Session session;
public MyMessageListener(Session session) {
this.session = session;
}
@Override
public void onMessage(Message message) {
// 打印这条消息
System.out.println("Message = " + message);
// 如果条件成立,就向服务器确认这批消息处理成功
// 服务器将从队列中删除这些消息
if(number++ % 3 == 0) {
try {
this.session.commit();
} catch (JMSException e) {
e.printStackTrace(System.out);
}
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
以上代码演示的是消费者通过事务commit的方式,向服务器确认一批消息正常处理完成的方式。请注意代码示例中的“session = connection.createSession(true, Session.SESSION_TRANSACTED);”语句。第一个参数表示连接会话启用事务支持;第二个参数表示使用commit或者rollback的方式进行向服务器应答。
这是调用commit的情况,那么如果调用rollback方法又会发生什么情况呢?调用rollback方法时,在rollback之前已处理过的消息(注意,并不是所有预取的消息)将重新发送一次到消费者端(发送给同一个连接会话)。并且消息中redeliveryCounter(重发计数器)属性将会加1。请看如下所示的代码片段和运行结果:
@Override
public void onMessage(Message message) {
// 打印这条消息
System.out.println("Message = " + message);
// rollback这条消息
this.session.rollback();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
以上代码片段中,我们不停的回滚正在处理的这条消息,通过打印出来的信息可以看到,这条消息被不停的重发:
Message = ActiveMQTextMessage {...... redeliveryCounter = 0, text = 这是发送的消息内容-------------------20}
Message = ActiveMQTextMessage {...... redeliveryCounter = 1, text = 这是发送的消息内容-------------------20}
Message = ActiveMQTextMessage {...... redeliveryCounter = 2, text = 这是发送的消息内容-------------------20}
Message = ActiveMQTextMessage {...... redeliveryCounter = 3, text = 这是发送的消息内容-------------------20}
Message = ActiveMQTextMessage {...... redeliveryCounter = 4, text = 这是发送的消息内容-------------------20}
- 1
- 2
- 3
- 4
- 5
可以看到同一条记录被重复的处理,并且其中的redeliveryCounter属性不断累加。
6.2、重发和死信队列
但是消息处理失败后,不断的重发消息肯定不是一个最好的处理办法:如果一条消息被不断的处理失败,那么最可能的情况就是这条消息承载的业务内容本身就有问题。那么无论重发多少次,这条消息还是会处理失败。
为了解决这个问题,ActiveMQ中引入了“死信队列”(Dead Letter Queue)的概念。即一条消息再被重发了多次后(默认为重发6次redeliveryCounter==6),将会被ActiveMQ移入“死信队列”。开发人员可以在这个Queue中查看处理出错的消息,进行人工干预。
默认情况下“死信队列”只接受PERSISTENT Message,如果NON_PERSISTENT Message超过了重发上限,将直接被删除。以下配置信息可以让NON_PERSISTENT Message在超过重发上限后,也移入“死信队列”:
<policyEntry queue=">">
<deadLetterStrategy>
<sharedDeadLetterStrategy processNonPersistent="true" />
</deadLetterStrategy>
</policyEntry>
- 1
- 2
- 3
- 4
- 5
另外,上文提到的默认重发次数redeliveryCounter的上限也是可以进行设置的,为了保证消息异常情况下尽可能小的影响消费者端的处理效率,实际工作中建议将这个上限值设置为3。原因上文已经说过,如果消息本身的业务内容就存在问题,那么重发多少次也没有用。
RedeliveryPolicy redeliveryPolicy = connectionFactory.getRedeliveryPolicy();
// 设置最大重发次数
redeliveryPolicy.setMaximumRedeliveries(3);
- 1
- 2
- 3
实际上ActiveMQ的重发机制还有包括以上提到的rollback方式在内的多种方式:
- 在支持事务的消费者连接会话中调用rollback方法;
- 在支持事务的消费者连接会话中,使用commit方法明确告知服务器端消息已处理成功前,会话连接就终止了(最可能是异常终止);
- 在需要使用ACK模式的会话中,使用消息的acknowledge方式明确告知服务器端消息已处理成功前,会话连接就终止了(最可能是异常终止)。
但是以上几种重发机制有一些小小的差异,主要体现在redeliveryCounter属性的作用区域。简而言之,第一种方法redeliveryCounter属性的作用区域是本次连接会话,而后两种redeliveryCounter属性的作用区域是在整个ActiveMQ系统范围。
7、消费者策略:ACK
消费者端,除了可以使用事务方式来告知ActiveMQ服务端一批消息已经成功处理外,还可以通过JMS规范中定义的acknowledge模式来实现同样功能。事实上acknowledge模式更为常用。
7.1、基本使用
如果选择使用acknowledge模式,那么你至少有4种方式使用它,且这四种方式的性能区别很大:
AUTO_ACKNOWLEDGE方式:这种方式下,当消费者端通过receive方法或者MessageListener监听方式从服务端得到消息后(无论是pul方式还是push方式),消费者连接会话会自动认为消费者端对消息的处理是成功的。但请注意,这种方式下消费者端不一定是向服务端一条一条ACK消息;
CLIENT_ACKNOWLEDGE方式:这种方式下,当消费者端通过receive方法或者MessageListener监听方式从服务端得到消息后(无论是pul方式还是push方式),必须显示调用消息中的acknowledge方法。如果不这样做,ActiveMQ服务器端将不会认为这条消息处理成功:
public void onMessage(Message message) {
//====================
//这里进行您的业务处理
//====================
try {
// 显示调用ack方法
message.acknowledge();
} catch (JMSException e) {
e.printStackTrace();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
DUPS_OK_ACKNOWLEDGE方式:批量确认方式。消费者端会按照一定的策略向服务器端间隔发送一个ack标示,表示某一批消息已经处理完成。DUPS_OK_ACKNOWLEDGE方式和 AUTO_ACKNOWLEDGE方式在某些情况下是一致的,这个在后文会讲到;
INDIVIDUAL_ACKNOWLEDGE方式:单条确认方式。这种方式是ActiveMQ单独提供的一种方式,其常量定义的位置都不在javax.jms.Session规范接口中,而是在org.apache.activemq.ActiveMQSession这个类中。这种方式消费者端将会逐条向ActiveMQ服务端发送ACK信息。所以这种ACK方式的性能很差,除非您有特别的业务要求,否则不建议使用。
7.2、工作方式和性能
笔者建议首先考虑使用AUTO_ACKNOWLEDGE方式确认消息,如果您这样做,那么一定请使用optimizeACK优化选项,并且重新设置prefetchSize数量为一个较小值(因为1000条的默认值在这样的情况下就显得比较大了):
......
//ack优化选项(实际上默认情况下是开启的)
connectionFactory.setOptimizeAcknowledge(true);
//ack信息最大发送周期(毫秒)
connectionFactory.setOptimizeAcknowledgeTimeOut(5000);
connection = connectionFactory.createQueueConnection();
connection.start();
......
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
AUTO_ACKNOWLEDGE方式的根本意义是“延迟确认”,消费者端在处理消息后暂时不会发送ACK标示,而是把它缓存在连接会话的一个pending 区域,等到这些消息的条数达到一定的值(或者等待时间超过设置的值),再通过一个ACK指令告知服务端这一批消息已经处理完成;而optimizeACK选项(指明AUTO_ACKNOWLEDGE采用“延迟确认”方式)只有当消费者端使用AUTO_ACKNOWLEDGE方式时才会起效:
“延迟确认”的数量阀值:prefetch * 0.65
“延迟确认”的时间阀值:> optimizeAcknowledgeTimeOut
DUPS_OK_ACKNOWLEDGE方式也是一种“延迟确认”策略,如果目标队列是Queue模式,那么它的工作策略与AUTO_ACKNOWLEDGE方式是一样的。也就是说,如果这时prefetchSize =1 或者没有开启optimizeACK,也会逐条消息发送ACK标示;如果目标队列是Topic模式,那么无论optimizeACK是否开启,都会在消费的消息个数>=prefetch * 0.5时,批量确认这些消息。