本章知识点:
1. 持久化消息和非持久化消息的发送策略
2. 消息的持久化方案及实践
3. 消费端消费消息的原理
持久化消息与非持久化消息的发送策略
消息同步发送和异步发送
- 同步发送过程中,发送者发送一条消息会阻塞直到broker反馈一个确认消息,表示消息已经被broker处理,这个机制提供了消息地安全机制,但是由于是阻塞地,会影响到客户端发送消息地性能。
- 异步发送过程中,发送者不需要等待broker提供反馈,所以性能相对较高,但是可能会出现消息丢失的情况,所以使用异步发送前提是允许出现消息丢失的情况。
- 默认情况下,非持久化消息是异步发送的,持久化消息并且是在非事务模式下是同步发送的。
- 但在开启事务的情况下,消息都是异步发送的,由于异步发送效率性能比同步发送高,所以在发送持久化消息的时候,尽量去开启事务会话。
除了持久化消息和非持久化消息的同步和异步特性以外,我们还可以通过以下几种方式来设置异步发送
1.ConnectionFactory connectionFactory=new ActiveMQConnectionFactory("tcp://192.168.11.153:61616? jms.useAsyncSend=true");
2.((ActiveMQConnectionFactory) connectionFactory).setUseAsyncSend(true);
3.((ActiveMQConnection)connection).setUseAsyncSend(true);
消息的发送原理分析图解
消息发送的流程图
ProducerWindowSize的含义
producer每发送一个消息,统计一下发送的字节数,当字节数达到ProducerWindowSize值时,需要等待broker的确认,才能继续发送。
主要是用来约束在异步发送时produce端允许积压的还未ACK的消息大小,且只对异步发送有意义,每次发送消息之后都会导致memoryUsage大小增加(+message.size)。当broker返回producer ACK时,memoryUsage尺寸减少(producerAck.size)此size表示先前发送消息的大小。
可以通过如下2种方式设置:
- 在brokerUrl中设置: "tcp://localhost:61616?jms.producerWindowSize=1048576",这种设置将会对所有的 producer生效。
- 在destinationUri中设置: "test-queue?producer.windowSize=1048576",此参数只会对使用此Destination实例 的producer失效,将会覆盖brokerUrl中的producerWindowSize值。 注意:此值越大,意味着消耗Client端的内存就越大。
消息发送的源码分析
以producer.send为入口
public void send(Destination destination, Message message, int deliveryMode, int priority, long timeToLive, AsyncCallback onComplete) throws JMSException {
//检查session状态,session关闭就会抛出异常
checkClosed();
if (destination == null) {
if (info.getDestination() == null) {
throw new UnsupportedOperationException("A destination must be specified.");
}
throw new InvalidDestinationException("Don't understand null destinations");
}
ActiveMQDestination dest;
//检查destination的类型
if (destination.equals(info.getDestination())) {
dest = (ActiveMQDestination)destination;
} else if (info.getDestination() == null) {
dest = ActiveMQDestination.transform(destination);
} else {
throw new UnsupportedOperationException("This producer can only send messages to: " + this.info.getDestination().getPhysicalName());
}
if (dest == null) {
throw new JMSException("No destination specified");
}
if (transformer != null) {
Message transformedMessage = transformer.producerTransform(session, this, message);
if (transformedMessage != null) {
message = transformedMessage;
}
}
//如果发送窗口大小不为空,则判断发送窗口大小决定是否阻塞
if (producerWindow != null) {
try {
producerWindow.waitForSpace();
} catch (InterruptedException e) {
throw new JMSException("Send aborted due to thread interrupt.");
}
}
//发送消息到broker的topic上
this.session.send(this, dest, message, deliveryMode, priority, timeToLive, producerWindow, sendTimeout, onComplete);
stats.onMessage();
}
ActiveMQSession的send方法
protected void send(ActiveMQMessageProducer producer, ActiveMQDestination destination, Message message, int deliveryMode, int priority, long timeToLive,
MemoryUsage producerWindow, int sendTimeout, AsyncCallback onComplete) throws JMSException {
checkClosed();
if (destination.isTemporary() && connection.isDeleted(destination)) {
throw new InvalidDestinationException("Cannot publish to a deleted Destination: " + destination);
}
//互斥锁,如果一个session的多个producer发送消息到这里,会保证消息发送的有序性
synchronized (sendMutex) {
// tell the Broker we are about to start a new transaction
//告诉broker开启一个新事务,只有事务型会话中才会开启
doStartTransaction();
//从事务上下文中获取事务id
TransactionId txid = transactionContext.getTransactionId();
long sequenceNumber = producer.getMessageSequence();
//Set the "JMS" header fields on the original message, see 1.1 spec section 3.4.11
//在Jms协议头中设置是否持久化标
message.setJMSDeliveryMode(deliveryMode);
//计算消息过期时间
long expiration = 0L;
if (!producer.getDisableMessageTimestamp()) {
long timeStamp = System.currentTimeMillis();
message.setJMSTimestamp(timeStamp);
if (timeToLive > 0) {
expiration = timeToLive + timeStamp;
}
}
//设置消息过期时间
message.setJMSExpiration(expiration);
//设置消息的优先级
message.setJMSPriority(priority);
//设置消息为非重发
message.setJMSRedelivered(false);
// transform to our own message format here
//将不同的格式统一转化为ActiveMQMessage
ActiveMQMessage msg = ActiveMQMessageTransformation.transformMessage(message, connection);
//设置目的地
msg.setDestination(destination);
//生成并设置消息id
msg.setMessageId(new MessageId(producer.getProducerInfo().getProducerId(), sequenceNumber));
// Set the message id.
//如果消息经过转化,则更新原来的消息id和目的地
if (msg != message) {
message.setJMSMessageID(msg.getMessageId().toString());
// Make sure the JMS destination is set on the foreign messages too.
message.setJMSDestination(destination);
}
//clear the brokerPath in case we are re-sending this message
msg.setBrokerPath(null);
msg.setTransactionId(txid);
if (connection.isCopyMessageOnSend()) {
msg = (ActiveMQMessage)msg.copy();
}
msg.setConnection(connection);
//把消息属性和消息体都设置为只读、防止篡改
msg.onSend();
msg.setProducerId(msg.getMessageId().getProducerId());
if (LOG.isTraceEnabled()) {
LOG.trace(getSessionId() + " sending message: " + msg);
}
//如果onComplete没有设置,且发送超时时间小于0,且消息不需要反馈,且连接器不是同步发送模式,且消息非持久化,或者连接器是异步发送模式,或者存在事务id的情况下,走异步发送,否则走同步发送
if (onComplete==null && sendTimeout <= 0 && !msg.isResponseRequired() && !connection.isAlwaysSyncSend() && (!msg.isPersistent() || connection.isUseAsyncSend() || txid != null)) {
this.connection.asyncSendPacket(msg);
if (producerWindow != null) {
// Since we defer lots of the marshaling till we hit the
// wire, this might not
// provide and accurate size. We may change over to doing
// more aggressive marshaling,
// to get more accurate sizes.. this is more important once
// users start using producer window
// flow control.
//异步发送情况下,需要设置producerWindow的大小
int size = msg.getSize();
producerWindow.increaseUsage(size);
}
} else {
if (sendTimeout > 0 && onComplete==null) {
//带超时时间的同步发送
this.connection.syncSendPacket(msg,sendTimeout);
}else {
//带回调的同步发送
this.connection.syncSendPacket(msg, onComplete);
}
}
}
}
ActiveMQConnection.doAsyncSendPacket
private void doAsyncSendPacket(Command command) throws JMSException {
try {
this.transport.oneway(command);
} catch (IOException e) {
throw JMSExceptionSupport.create(e);
}
}
这个地方问题来了,this.transport是什么东西?在哪里实例化的?按照以前看源码的惯例来看,它肯定不是一个 单纯的对象。
按照以往看源码的经验来看,一定是在创建连接的过程中初始化的。所以我们定位到代码 。
transport的实例化过程
从connection=connectionFactory.createConnection();这行代码作为入口,一直跟踪到 ActiveMQConnectionFactory. createActiveMQConnection这个方法中。代码如下
protected ActiveMQConnection createActiveMQConnection(String userName, String password) throws JMSException {
if (brokerURL == null) {
throw new ConfigurationException("brokerURL not set.");
}
ActiveMQConnection connection = null;
try {
//去创建creatTransport对象
Transport transport = createTransport();
connection = createActiveMQConnection(transport, factoryStats);
connection.setUserName(userName);
connection.setPassword(password);
configureConnection(connection);
transport.start();
if (clientID != null) {
connection.setDefaultClientID(clientID);
}
return connection;
} catch (JMSException e) {
// Clean up!
try {
connection.close();
} catch (Throwable ignore) {
}
throw e;
} catch (Exception e) {
// Clean up!
try {
connection.close();
} catch (Throwable ignore) {
}
throw JMSExceptionSupport.create("Could not connect to broker URL: " + brokerURL + ". Reason: " + e, e);
}
}
createTransport
调用ActiveMQConnectionFactory.createTransport方法,去创建一个transport对象
- 创建一个URI
- 根据URI去创建一个连接TransportFactory.connect
- 默认使用的是TCP协议
protected Transport createTransport() throws JMSException {
try {
URI connectBrokerUL = brokerURL;
String scheme = brokerURL.getScheme();
if (scheme == null) {
throw new IOException("Transport not scheme specified: [" + brokerURL + "]");
}
if (scheme.equals("auto")) {
connectBrokerUL = new URI(brokerURL.toString().replace("auto", "tcp"));
} else if (scheme.equals("auto+ssl")) {
connectBrokerUL = new URI(brokerURL.toString().replace("auto+ssl", "ssl"));
} else if (scheme.equals("auto+nio")) {
connectBrokerUL = new URI(brokerURL.toString().replace("auto+nio", "nio"));
} else if (scheme.equals("auto+nio+ssl")) {
connectBrokerUL = new URI(brokerURL.toString().replace("auto+nio+ssl", "nio+ssl"));
}
return TransportFactory.connect(connectBrokerUL);
} catch (Exception e) {
throw JMSExceptionSupport.create("Could not create Transport. Reason: " + e, e);
}
}
TransportFactory.findTransportFactory
- 从TRANSPORT_FACTORYS这个Map集合中,根据scheme去获得一个TransportFactory指定的实例对象
- 如果Map集合中不存在,则通过TRANSPORT_FACTORY_FINDER去找一个并且构建实例
- 他会从METAINF/services/org/apache/activemq/transport/ 这个路径下,根据URI组装的scheme去找到匹配的class对象并且 实例化,所以根据tcp为key去对应的路径下可以找到TcpTransportFactory
public static TransportFactory findTransportFactory(URI location) throws IOException {
String scheme = location.getScheme();
if (scheme == null) {
throw new IOException("Transport not scheme specified: [" + location + "]");
}
TransportFactory tf = TRANSPORT_FACTORYS.get(scheme);
if (tf == null) {
// Try to load if from a META-INF property.
try {
tf = (TransportFactory)TRANSPORT_FACTORY_FINDER.newInstance(scheme);
TRANSPORT_FACTORYS.put(scheme, tf);
} catch (Throwable e) {
throw IOExceptionSupport.create("Transport scheme NOT recognized: [" + scheme + "]", e);
}
}
return tf;
}
调用TransportFactory.doConnect去构建一个连接
public Transport doConnect(URI location) throws Exception {
try {
Map<String, String> options = new HashMap<String, String>(URISupport.parseParameters(location));
if( !options.containsKey("wireFormat.host") ) {
options.put("wireFormat.host", location.getHost());
}
WireFormat wf = createWireFormat(options);
//创建一个Transport,创建一个socket连接
Transport transport = createTransport(location, wf);
//配置configure,这里面是对Transport做链路包装
Transport rc = configure(transport, wf, options);
//remove auto
IntrospectionSupport.extractProperties(options, "auto.");
if (!options.isEmpty()) {
throw new IllegalArgumentException("Invalid connect parameters: " + options);
}
return rc;
} catch (URISyntaxException e) {
throw IOExceptionSupport.create(e);
}
}
Configure
public Transport configure(Transport transport, WireFormat wf, Map options) throws Exception {
//组装一个复合的transport,这里会包装两层,一个是IactivityMonitor.另一个是WireFormatNegotiator
transport = compositeConfigure(transport, wf, options);
//再做一层包装,MutexTransport
transport = new MutexTransport(transport);
//包装ResponseCorrelator
transport = new ResponseCorrelator(transport);
return transport;
}
public Transport compositeConfigure(Transport transport, WireFormat format, Map options) {
TcpTransport tcpTransport = transport.narrow(TcpTransport.class);
IntrospectionSupport.setProperties(tcpTransport, options);
Map<String, Object> socketOptions = IntrospectionSupport.extractProperties(options, "socket.");
tcpTransport.setSocketOptions(socketOptions);
if (tcpTransport.isTrace()) {
try {
transport = TransportLoggerSupport.createTransportLogger(transport, tcpTransport.getLogWriterName(), tcpTransport.isDynamicManagement(), tcpTransport.isStartLogging(), tcpTransport.getJmxPort());
} catch (Throwable e) {
LOG.error("Could not create TransportLogger object for: " + tcpTransport.getLogWriterName() + ", reason: " + e, e);
}
}
boolean useInactivityMonitor = "true".equals(getOption(options, "useInactivityMonitor", "true"));
if (useInactivityMonitor && isUseInactivityMonitor(transport)) {
//进入这里又一层包装
transport = createInactivityMonitor(transport, format);
/**
* return new InactivityMonitor(transport, format);
*/
IntrospectionSupport.setProperties(transport, options);
}
// Only need the WireFormatNegotiator if using openwire
if (format instanceof OpenWireFormat) {
//这里包装了一层
transport = new WireFormatNegotiator(transport, (OpenWireFormat)format, tcpTransport.getMinmumWireFormatVersion());
}
return super.compositeConfigure(transport, format, options);
}
到目前为止,这个transport实际上就是一个调用链了,它的结构为
ResponseCorrelator(MutexTransport(WireFormatNegotiator(IactivityMonitor(TcpTransport()))
每一层包装表示什么意思呢?
- ResponseCorrelator 用于实现异步请求。
- MutexTransport 实现写锁,表示同一时间只允许发送一个请求
- WireFormatNegotiator 实现了客户端连接broker的时候先发送数据解析相关的协议信息,比如解析版本号,是否使用缓存等
- InactivityMonitor 用于实现连接成功后的心跳检查机制,客户端每10s发送一次心跳信息。服务端每30s读取 一次心跳信息。
同步发送和异步发送的区别
public Object request(Object command, int timeout) throws IOException {
FutureResponse response = asyncRequest(command, null);
// 从future方法阻塞等待返回
return response.getResult(timeout);
}
在ResponseCorrelator的request方法中,需要通过response.getResult去获得broker的反馈,否则会阻塞 。
持久化消息和非持久化消息存储原理
正常情况下,非持久化消息是存储在内存中的,持久化消息是存储在存储文件中的。能够存储的最大消息数据在 ${ActiveMQ_HOME}/conf/activemq.xml文件中的systemUsage节点 。
SystemUsage配置设置了一些系统内存和硬盘容量
<?xml version="1.0" encoding="utf-8"?>
<systemUsage>
<systemUsage>
<memoryUsage>
//该子标记设置整个ActiveMQ节点的“可用内存限制”。这个值不能超过ActiveMQ本身设置的最大内存大小。其中的
percentOfJvmHeap属性表示百分比。占用70%的堆内存
<memoryUsage percentOfJvmHeap="70"/>
</memoryUsage>
<storeUsage>
//该标记设置整个ActiveMQ节点,用于存储“持久化消息”的“可用磁盘空间”。该子标记的limit属性必须要进行设置
<storeUsage limit="100 gb"/>
</storeUsage>
<tempUsage>
//一旦ActiveMQ服务节点存储的消息达到了memoryUsage的限制,非持久化消息就会被转储到 temp store区域,虽然 我
们说过非持久化消息不进行持久化存储,但是ActiveMQ为了防止“数据洪峰”出现时非持久化消息大量堆积致使内存耗尽的情
况出现,还是会将非持久化消息写入到磁盘的临时区域——temp store。这个子标记就是为了设置这个temp store区域
的可用磁盘空间限制”
<tempUsage limit="50 gb"/>
</tempUsage>
</systemUsage>
</systemUsage>
从上面的配置文件中,可以get到一个结论,当非持久化消息堆积到一定程度的时候,也就是内存超过指定的设置阈值时,ActiveMQ会将内存中的非持久化消息写入到临时文件,以便腾出内存。但是他和持久化消息的区别是,重启之后,持久化消息会从文件中恢复,非持久化的临时文件会直接删除。
消息的持久化策略分析
消息持久性对于可靠消息传递来说是一种比较好的方法,发送者和接收者不是同时在线或者消息中间件直接宕机了,消息中间件重启之后还可以继续将消息发送出去,消息持久性原理很简单,就是在发送消息出去之后,消息中心首先将消息存储在本地文件、内存或许远程数据库,然后把消息发送给接收者,发送成功之后,再把消息从存储中删除,失败则继续尝试。
持久化存储支持类型
- KahaDB存储(默认存储方式)
- JDBC存储
- Memory存储
- LevelDB存储
- JDBC With ActiveMQ Journal
KahaDB存储
KahaDB是目前默认的存储方式,可用于任何场景,提高了性能和恢复能力,消息存储使用一个事务日志和仅用一个索引文件来存储它所有的地址。
KahaDB是一个专门针对消息持久化的解决方案,它对典型的消息使用模式进行了优化,在KahaDB中,数据被追加到data logs中,当不再需要log文件中的数据的时候log文件会被丢弃。
KahaDB的配置方式
<persistenceAdapter>
<kahaDB directory="${activemq.data}/kahadb"/>
</persistenceAdapter>
KahaDB的存储原理
在data/kahadb这个目录下,会生成四个文件
- db.data 它是消息的索引文件,本质上是B-Tree(B树),使用B-Tree作为索引指向db-*.log里面存储的消息 。
- db.redo 用来进行消息恢复。
- db-*.log 存储消息内容。新的数据以APPEND方式追加到日志文件末尾。属于顺序写入,因此消息存储是比较 快的。默认是32M,达到阀值会自动递增
- lock文件 锁,表示当前获得kahadb读写权限的broker。
JDBC存储
使用JDBC持久化方式,数据库会创建三个表:activemq_msgs,activemq_acks,activemq_locks。
- ACTIVEMQ_MSGS 消息表,queue和topic都存在这个表中
- ACTIVEMQ_ACKS 存储持久订阅的信息和最后一个持久订阅接收的消息ID
- ACTIVEMQ_LOCKS 锁表,用来确保某一时刻,只能有一个ActiveMQ broker实例来访问数据库
JDBC存储实践
<persistenceAdapter>
<jdbcPersistenceAdapter dataSource="# MySQL-DS " createTablesOnStartup="true" />
</persistenceAdapter>
dataSource指定持久化数据库的bean,createTablesOnStartup是否在启动的时候创建数据表,默认值是true,这 样每次启动都会去创建数据表了,一般是第一次启动的时候设置为true,之后改成false 。
Mysql持久化Bean配置
<bean id="Mysql-DS" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://192.168.11.156:3306/activemq? relaxAutoCommit=true"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</bean>
添加jar包依赖
LevelDB存储
LevelDB持久化性能高于KahaDB,虽然目前默认的持久化方式仍然是KahaDB。并且,在ActiveMQ 5.9版本提供 了基于LevelDB和Zookeeper的数据复制方式,用于Master-slave方式的首选数据复制方案。 不过,据ActiveMQ官网对LevelDB的表述:LevelDB官方不建议使用以及不再支持,推荐使用的是KahaDB
<persistenceAdapter>
<levelDBdirectory="activemq-data"/>
</persistenceAdapter>
Memory消息存储
基于内存的消息存储,内存消息存储主要是存储所有持久化的消息在内存中,persistent="false",表示不支持持久化存储,直接存储在内存。
<beans>
<broker brokerName="test-broker" persistent="false"
xmlns="http://activemq.apache.org/schema/core">
<transportConnectors>
<transportConnector uri="tcp://localhost:61635"/>
</transportConnectors>
</broker>
</beans>
JDBC Message store with ActiveMQ Journal
这种方式可否了JDBC Store的不足,jdbc每次消息过来,都需要去写库和读库。
ActiveMQ Journal使用高效缓存写入技术,大大提高了性能。
当消费者的消费速度能及时跟上生产者消息的生产速度时,journal文件能够大大减少需要写入到DB中的消息。
举个例子:生产者生产了1000个消息,这1000条消息会保存在journal文件,如果消费者的消费速度很快的情况下,在journal文件还没有同步到DB之前,消费者已经消费了90%的数据,这个时候只需要同步剩余的10%数据到DB数据库。
如果消费者的消费速度很慢,这个时候jonrnal文件可以批量方式写入DB
<persistenceFactory>
<journalPersistenceAdapterFactory dataSource="#Mysql-DS" dataDirectory="activemqdata"/>
</persistenceFactory>
在服务端循环发送消息,可以看到数据是延迟同步到数据库的。
消费端消费消息的原理
根据上篇文章的讲解,可以大致了解有两种方式可以接收消息:
一种是使用同步阻塞的MessageConsumer #receive方法;另外一种是使用消息监听器MessageListener。
需要注意的是,在同一个session下,这两者不能同时工作,也就是说不能针对不同的消息采用不同的接收方式,否则会抛出异常。
至于为什么这么做,最大的原因还是在事务性会话中,两种消费模式的事务不好管控 。
消费端消费消息源码分析
原理图:
ActiveMQMessageConsumer.reserve
消费端同步接收消息的源码入口
public Message receive() throws JMSException {
checkClosed();
//检查reserve和MessageListener是否同时配置在当前的会话中
checkMessageListener();
//如果PrefetchSizeSize为0,并且unconsumedMessages为空,则发起pull命令
sendPullCommand(0);
//从unconsumedMessages队列获取到消息
MessageDispatch md = dequeue(-1);
if (md == null) {
return null;
}
beforeMessageIsConsumed(md);
//发送ACK给到broker
afterMessageIsConsumed(md, false);
//获取消息并返回
return createActiveMQMessage(md);
}
sendPullCommand
发送pull命令从broker上获取消息,前提是prefetchSize=0并且unconsumedMessages为空
unconsumedMessages表示未消费的消息,这里面预读取的大小为prefetchSize的值
protected void sendPullCommand(long timeout) throws JMSException {
clearDeliveredList();
if (info.getCurrentPrefetchSize() == 0 && unconsumedMessages.isEmpty()) {
MessagePull messagePull = new MessagePull();
messagePull.configure(info);
messagePull.setTimeout(timeout);
//向服务器异步发送messagePull命令
session.asyncSendPacket(messagePull);
}
}
clearDeliveredList
在上面的sendPullCommand方法中,会先调用clearDeliveredList方法,主要用来清理已经分发的消息链表 deliveredMessages deliveredMessages,存储分发给消费者但还为应答的消息链表。
- 如果session是事务的,则会遍历deliveredMessage中的消息放入到previouslyDeliveredMessage中来做重发
- 如果session是非事务的,根据ACK的模式来选择不同的应答操作
private void registerSync() throws JMSException {
session.doStartTransaction();
if (!synchronizationRegistered) {
synchronizationRegistered = true;
session.getTransactionContext().addSynchronization(new Synchronization() {
@Override
public void beforeEnd() throws Exception {
if (transactedIndividualAck) {
clearDeliveredList();
waitForRedeliveries();
synchronized(deliveredMessages) {
rollbackOnFailedRecoveryRedelivery();
}
} else {
acknowledge();
}
synchronizationRegistered = false;
}
@Override
public void afterCommit() throws Exception {
commit();
synchronizationRegistered = false;
}
@Override
public void afterRollback() throws Exception {
rollback();
synchronizationRegistered = false;
}
});
}
}
dequeue
从unconsumedMessage中取出一个消息,在创建一个消费者时,就会为这个消费者创建一个未消费的消息通道。
这个通道分为两种:
- 一种是简单优先级队列分发通道SimplePriorityMessageDispatchChannel ;
- 另一种是先进先出的分发通道FifoMessageDispatchChannel
至于为什么要存在这样一个消息分发通道,大家可以想象一下,如果消费者每次去消费完一个消息以后再去broker 拿一个消息,效率是比较低的。所以通过这样的设计可以允许session能够一次性将多条消息分发给一个消费者。 默认情况下对于queue来说,prefetchSize的值是1000。
beforeMessagelsConsumed
这里主要是做消息消费之前的一些准备工作,如果ACK类型不是DUPS_OK_ACKNOWLEDGE或者队列模式(除Topic和DUPS_ACK这两种情况),所有的消息先放在deliveredMessage链表的开头,并且如果当前是事务类型的会话,,则判断transactedIndividualAck,如果为true,表示单条消息直接返回ack。 否则,调用ackLater,批量应答, client端在消费消息后暂且不发送ACK,而是把它缓存下来(pendingACK),等到这 些消息的条数达到一定阀值时,只需要通过一个ACK指令把它们全部确认;这比对每条消息都逐个确认,在性能上 要提高很多。
private void beforeMessageIsConsumed(MessageDispatch md) throws JMSException {
md.setDeliverySequenceId(session.getNextDeliveryId());
lastDeliveredSequenceId = md.getMessage().getMessageId().getBrokerSequenceId();
if (!isAutoAcknowledgeBatch()) {
synchronized(deliveredMessages) {
deliveredMessages.addFirst(md);
}
if (session.getTransacted()) {
if (transactedIndividualAck) {
immediateIndividualTransactedAck(md);
} else {
ackLater(md, MessageAck.DELIVERED_ACK_TYPE);
}
}
}
}
afterMessagelsConsumed
这个方法的主要作用是执行应答操作:
- 如果消息过期,则返回消息过期的ACK
- 如果是事务类型的会话,则不做任何处理
- 如果是AUTOACK或者(DUPS_OK_ACK且是队列),并且是优化ack操作,则走批量确认ack
- 如果是DUCP_OK_ACK,则走ackLater逻辑
- 如果是CLIENT_ACK则执行ackLater
private void afterMessageIsConsumed(MessageDispatch md, boolean messageExpired) throws JMSException {
if (unconsumedMessages.isClosed()) {
return;
}
if (messageExpired) {
acknowledge(md, MessageAck.EXPIRED_ACK_TYPE);
stats.getExpiredMessageCount().increment();
} else {
stats.onMessage();
if (session.getTransacted()) {
// Do nothing.
} else if (isAutoAcknowledgeEach()) {
if (deliveryingAcknowledgements.compareAndSet(false, true)) {
synchronized (deliveredMessages) {
if (!deliveredMessages.isEmpty()) {
if (optimizeAcknowledge) {
ackCounter++;
// AMQ-3956 evaluate both expired and normal msgs as
// otherwise consumer may get stalled
if (ackCounter + deliveredCounter >= (info.getPrefetchSize() * .65) || (optimizeAcknowledgeTimeOut > 0 && System.currentTimeMillis() >= (optimizeAckTimestamp + optimizeAcknowledgeTimeOut))) {
MessageAck ack = makeAckForAllDeliveredMessages(MessageAck.STANDARD_ACK_TYPE);
if (ack != null) {
deliveredMessages.clear();
ackCounter = 0;
session.sendAck(ack);
optimizeAckTimestamp = System.currentTimeMillis();
}
// AMQ-3956 - as further optimization send
// ack for expired msgs when there are any.
// This resets the deliveredCounter to 0 so that
// we won't sent standard acks with every msg just
// because the deliveredCounter just below
// 0.5 * prefetch as used in ackLater()
if (pendingAck != null && deliveredCounter > 0) {
session.sendAck(pendingAck);
pendingAck = null;
deliveredCounter = 0;
}
}
} else {
MessageAck ack = makeAckForAllDeliveredMessages(MessageAck.STANDARD_ACK_TYPE);
if (ack!=null) {
deliveredMessages.clear();
session.sendAck(ack);
}
}
}
}
deliveryingAcknowledgements.set(false);
}
} else if (isAutoAcknowledgeBatch()) {
ackLater(md, MessageAck.STANDARD_ACK_TYPE);
} else if (session.isClientAcknowledge()||session.isIndividualAcknowledge()) {
boolean messageUnackedByConsumer = false;
synchronized (deliveredMessages) {
messageUnackedByConsumer = deliveredMessages.contains(md);
}
if (messageUnackedByConsumer) {
ackLater(md, MessageAck.DELIVERED_ACK_TYPE);
}
}
else {
throw new IllegalStateException("Invalid session state.");
}
}
}