• 欢迎访问 winrains 的个人网站!
  • 本网站主要从互联网整理和收集了与Java、网络安全、Linux等技术相关的文章,供学习和研究使用。如有侵权,请留言告知,谢谢!

ActiveMQ发送消息原理解析

消息队列 winrains 来源:匠丶 1年前 (2019-08-31) 36次浏览
本文将对ActiveMQ发送消息的源码进行解析,并分析ActiveMQ持久化消息和非持久化消息的发送策略和消息的存储策略。

消息的发送原理

消息同步发送和异步发送

ActiveMQ支持同步、异步两种发送模式将消息发送到broker上。
同步发送过程中,发送者发送一条消息会阻塞直到broker反馈一个确认消息,表示消息已经被broker处理。这个机制提供了消息的安全性保障,但是由于是阻塞的操作,会影响到客户端消息发送的性能。
异步发送的过程中,发送者不需要等待broker提供反馈,所以性能相对较高。但是可能会出现消息丢失的情况。所以使用异步发送的前提是在某些情况下允许出现数据丢失的情况。
默认情况下,非持久化消息是异步发送的,持久化消息并且是在非事务模式下是同步发送的。
但是在开启事务的情况下,消息都是异步发送。由于异步发送的效率会比同步发送性能更高。所以在发送持久化消息的时候,尽量去开启事务会话。
除了持久化消息和非持久化消息的同步和异步特性以外,我们还可以通过以下几种方式来设置异步发送:

ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://192.168.11.153:61616?jms.useAsyncSend=true");
((ActiveMQConnectionFactory) connectionFactory).setUseAsyncSend(true);
((ActiveMQConnection)connection).setUseAsyncSend(true);

消息发送源码分析

消息发送的流程图如下:

ActiveMQMessageProducer.send为入口:

public void send(Destination destination, Message message, int deliveryMode, int priority, long timeToLive,
        AsyncCallback onComplete) throws JMSException {
    checkClosed(); // 检查session的状态,如果session以关闭则抛异常
    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;
    if (destination.equals(info.getDestination())) {// 检查destination的类型,如果符合要求,就转变为ActiveMQDestination
        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);
    }
    synchronized (sendMutex) { // 互斥锁,如果一个session的多个producer发送消息到这里,会保证消息发送的有序性
        // tell the Broker we are about to start a new transaction
        doStartTransaction();// 告诉broker开始一个新事务,只有事务型会话中才会开启
        TransactionId txid = transactionContext.getTransactionId();// 从事务上下文中获取事务id
        long sequenceNumber = producer.getMessageSequence();
        // Set the "JMS" header fields on the original message, see 1.1 spec
        // section 3.4.11
        message.setJMSDeliveryMode(deliveryMode); // 在JMS协议头中设置是否持久化标识
        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.
        if (msg != message) {// 如果消息是经过转化的,则更新原来的消息id和目的地
            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.
                int size = msg.getSize(); // 异步发送的情况下,需要设置producerWindow的大小
                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);
    }
}

transport
这里调用了transport的方法,下面分析一下transport的实例化过程。在ActiveMQConnectionFactory. createActiveMQConnection这个方法中:

protected ActiveMQConnection createActiveMQConnection(String userName, String password) throws JMSException {
    if (brokerURL == null) {
        throw new ConfigurationException("brokerURL not set.");
    }
    ActiveMQConnection connection = null;
    try {
        Transport transport = createTransport();
        connection = createActiveMQConnection(transport, factoryStats);
        connection.setUserName(userName);
        connection.setPassword(password);
        // 省略后面的代码
    } catch (Exception e) {
        e.printStackTrace();
    }
}

createTransport,调用ActiveMQConnectionFactory.createTransport方法,去创建一个transport对象。

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);
    }
}
  1. 构建一个URI(默认使用的是tcp的协议)
  2. 根据URI去创建一个连接TransportFactory.connect
public static Transport connect(URI location) throws Exception {
    TransportFactory tf = findTransportFactory(location);
    return tf.doConnect(location);
}

TransportFactory. findTransportFactory会从TRANSPORT_FACTORYS这个Map集合中,根据scheme去获得一个TransportFactory指定的实例对象,如果Map集合中不存在,则通过TRANSPORT_FACTORY_FINDER去找一个并且构建实例。
这个地方又有点类似于Java SPI的思想。他会从META-INF/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 transport = createTransport(location, wf); // 创建一个Transport,创建一个socket连接
                                                             // -> 终于找到真相了
        Transport rc = configure(transport, wf, options);// 配置configure,这个里面是对Transport做链路包装
        // 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(transport, wf, options)

public Transport configure(Transport transport, WireFormat wf, Map options) throws Exception {
    // 组装一个复合的transport,这里会包装两层,一个是IactivityMonitor.另一个是WireFormatNegotiator
    transport = compositeConfigure(transport, wf, options);
    transport = new MutexTransport(transport); // 再做一层包装,MutexTransport
    transport = new ResponseCorrelator(transport); // 包装ResponseCorrelator
    return transport;
}

到目前为止,这个transport实际上就是一个调用链了,它的链结构为:ResponseCorrelator(MutexTransport(WireFormatNegotiator(IactivityMonitor(TcpTransport()))每一层包装的作用:
ResponseCorrelator 用于实现异步请求。
MutexTransport 实现写锁,表示同一时间只允许发送一个请求。
WireFormatNegotiator 实现了客户端连接broker的时候先发送数据解析相关的协议信息,比如解析版本号,是否使用缓存等。
InactivityMonitor 用于实现连接成功成功后的心跳检查机制,客户端每10s发送一次心跳信息。服务端每30s读取一次心跳信息。
同步发送和异步发送的区别
ResponseCorrelator的request方法中,通过response.getResult去获得broker的结果,如果没有获取到则阻塞。

public Object request(Object command, int timeout) throws IOException {
    FutureResponse response = asyncRequest(command, null);
    return response.getResult(timeout); // 从future方法阻塞等待返回
}

持久化消息和非持久化消息的存储原理

正常情况下,非持久化消息是存储在内存中的,持久化消息是存储在文件中的。能够存储的最大消息数据在
${ActiveMQ_HOME}/conf/activemq.xml文件中的systemUsage节点配置。

<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。这个子标记就是为了设置这个tempstore区域的“可用磁盘空间限制”。-->
<tempUsage limit="50 gb"/>
</tempUsage>
</systemUsage>
</systemUsage>

从上面的配置可以知道,当非持久化消息堆积到一定程度的时候,也就是内存超过指定的设置阀值时,ActiveMQ会将内存中的非持久化消息写入到临时文件,以便腾出内存。但是它和持久化消息的区别是,重启之后,持久化消息会从文件中恢复,非持久化的临时文件会直接删除。

消息的持久化策略分析

消息持久性对于可靠消息传递来说是一种比较好的方法,即时发送者和接受者不是同时在线或者消息中心在发送者发送消息后宕机了,在消息中心重启后仍然可以将消息发送出去。消息持久性的原理很简单,就是在发送消息出去后,消息中心首先将消息存储在本地文件、内存或者远程数据库,然后把消息发送给接受者,发送成功后再把消息从存储中删除,失败则继续尝试。
ActiveMQ支持多种不同的持久化方式,主要有以下几种:

  • KahaDB存储(默认存储方式)
  • JDBC存储
  • Memory存储
  • LevelDB存储
  • JDBC With ActiveMQ Journal

KahaDB存储
KahaDB是目前默认的存储方式,可用于任何场景,提高了性能和恢复能力。消息存储使用一个事务日志和仅仅用一个索引文件来存储它所有的地址。KahaDB是一个专门针对消息持久化的解决方案,它对典型的消息使用模式进行了优化。在Kaha中,数据被追加到data logs中。当不再需要log文件中的数据的时候,log文件会被丢弃。
KahaDB的存储原理:
在data/kahadb这个目录下,会生成四个文件:
Ø db.data 它是消息的索引文件,本质上是B-Tree(B树),使用B-Tree作为索引指向db-.log里面存储的消息
Ø db.redo 用来进行消息恢复
Ø db-
.log 存储消息内容。新的数据以APPEND的方式追加到日志文件末尾。属于顺序写入,因此消息存储是比较快的。默认是32M,达到阀值会自动递增
Ø lock文件 锁,表示当前获得kahadb读写权限的broker
JDBC存储
使用JDBC持久化方式,数据库会创建3个表:activemq_msgs,activemq_acks和activemq_lock。
ACTIVEMQ_MSGS 消息表,queue和topic都存在这个表中
ACTIVEMQ_ACKS 存储持久订阅的信息和最后一个持久订阅接收的消息ID
ACTIVEMQ_LOCKS 锁表,用来确保某一时刻,只能有一个ActiveMQ broker实例来访问数据库
LevelDB存储
LevelDB持久化性能高于KahaDB,虽然目前默认的持久化方式仍然是KahaDB。并且,在ActiveMQ 5.9版本提供了基于LevelDB和Zookeeper的数据复制方式,用于Master-slave方式的首选数据复制方案。不过,据ActiveMQ官网对LevelDB的表述:LevelDB官方建议使用以及不再支持,推荐使用的是KahaDB。
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。
如果消费者的消费速度很慢,这个时候journal文件可以使消息以批量方式写到DB。

作者:匠丶

来源:https://www.jianshu.com/p/3a29b032827a


版权声明:文末如注明作者和来源,则表示本文系转载,版权为原作者所有 | 本文如有侵权,请及时联系,承诺在收到消息后第一时间删除 | 如转载本文,请注明原文链接。
喜欢 (1)