activeMQ 入门指南

1.简介

1.1. ActiveMQ 由Apache出品的开源消息总线。是一个完全支持JMS1.1和J2EE 1.4规范的 JMS Provider实现。


1.2 它的优点是:
支持多种语言编写客户端;
对spring的支持,很容易和spring整合 ;
支持多种传输协议:TCP,SSL,NIO,UDP等 ;
支持AJAX ;
非常成熟,功能强大,在早些年业内大量的公司以及项目中都有应用 ;
单机吞吐量:万级
时效性:ms级
可用性:高,基于主从架构实现高可用性
消息可靠性:有较低的概率丢失数据
功能支持:MQ领域的功能极其完备


1.3 它的缺点是
偶尔会有较低概率丢失消息;
现在社区以及国内应用都越来越少,官方社区现在对ActiveMQ 5.x维护越来越少,几个月才发布一个版本;


1.4 使用场景
主要是基于解耦和异步来用的,较少在大规模吞吐的场景中使用

2.安装

2.1 下载安装包

到官网下载:https://activemq.apache.org/components/classic/

2.2 启动activeMQ

解压缩,进入bin目录,启动/停止/查看状态:./activemq [start|stop|status]

​ 启动信息如下


INFO | Apache ActiveMQ 5.15.9 (localhost, ID:LAPTOP-BRJ9A33I-60490-1563860323722-0:1) is starting

INFO | Listening for connections at: tcp://LAPTOP-BRJ9A33I:61616?maximumConnections=1000&wireFormat.maxFrameSize=104857600

INFO | Connector openwire started

INFO | Listening for connections at: amqp://LAPTOP-BRJ9A33I:5672?maximumConnections=1000&wireFormat.maxFrameSize=104857600

INFO | Connector amqp started

INFO | Listening for connections at: stomp://LAPTOP-BRJ9A33I:61613?maximumConnections=1000&wireFormat.maxFrameSize=104857600

INFO | Connector stomp started

INFO | Listening for connections at: mqtt://LAPTOP-BRJ9A33I:1883?maximumConnections=1000&wireFormat.maxFrameSize=104857600

INFO | Connector mqtt started

INFO | Starting Jetty server

INFO | Creating Jetty connector

WARN | [email protected]@f107c50{/,null,STARTING} has uncovered http methods for path: /

INFO | Listening for connections at ws://LAPTOP-BRJ9A33I:61614?maximumConnections=1000&wireFormat.maxFrameSize=104857600

INFO | Connector ws started

INFO | Apache ActiveMQ 5.15.9 (localhost, ID:LAPTOP-BRJ9A33I-60490-1563860323722-0:1) started

INFO | For help or more information please see: http://activemq.apache.org

INFO | No Spring WebApplicationInitializer types detected on classpath

INFO | ActiveMQ WebConsole available at http://0.0.0.0:8161/

INFO | ActiveMQ Jolokia REST API available at http://0.0.0.0:8161/api/jolokia/

INFO | Initializing Spring FrameworkServlet 'dispatcher'

2.3 查看控制台

进入管理后台查看消息:http://ip:8161/admin 用户名:admin 密码:admin

3.编码实现

3.1 发送端编码


            org.apache.activemq
            activemq-all
            5.15.3
        


package com.qhs.mq.activemq;

import org.apache.activemq.ActiveMQConnectionFactory;

import javax.jms.*;

public class MsgProducer {

    public static void main(String[] args) {

        testQueue();

        testTopic();

    }

    private static void testQueue() {

        ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory("tcp://192.168.137.1:61616");

        Connection connection = null;

        Session session = null;

        MessageProducer producer = null;

        try {

            connection = factory.createConnection();

            connection.start();

            session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);

            Queue queue = session.createQueue("qhs-queue");

            producer = session.createProducer(queue);

            TextMessage textMessage = null;

            for (int i = 0; i < 50; i++) {

                textMessage = session.createTextMessage("字符串queue消息:" + i);

                producer.send(textMessage);

            }

        } catch (JMSException e) {

            e.printStackTrace();

        } finally {

            try {

                producer.close();

            } catch (JMSException e) {

                e.printStackTrace();

            }

            try {

                session.close();

            } catch (JMSException e) {

                e.printStackTrace();

            }

            try {

                connection.close();

            } catch (JMSException e) {

                e.printStackTrace();

            }

        }

    }

    private static void testTopic() {

        ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory("tcp://192.168.137.1:61616");

        Connection connection = null;

        Session session = null;

        MessageProducer producer = null;

        try {

            connection = factory.createConnection();

            connection.start();

            session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);

            Topic topic = session.createTopic("qhs-topic");

            producer = session.createProducer(topic);

            TextMessage textMessage = null;

            for (int i = 0; i < 50; i++) {

                textMessage = session.createTextMessage("字符串topic消息:" + i);

                producer.send(textMessage);

            }

        } catch (JMSException e) {

            e.printStackTrace();

        } finally {

            try {

                producer.close();

            } catch (JMSException e) {

                e.printStackTrace();

            }

            try {

                session.close();

            } catch (JMSException e) {

                e.printStackTrace();

            }

            try {

                connection.close();

            } catch (JMSException e) {

                e.printStackTrace();

            }

        }

    }

}

3.2 接收端编码


package com.qhs.mq.activemq;

import org.apache.activemq.ActiveMQConnectionFactory;

import javax.jms.*;

public class MsgConsumer {

    public static void main(String[] args) {

        testQueue();

        testTopic();

    }

    public static void testQueue() {

        ConnectionFactory factory = new ActiveMQConnectionFactory("tcp://192.168.137.1:61616");

        Connection connection = null;

        Session session = null;

        MessageConsumer queueConsumer = null;

        MessageConsumer topicConsumer = null;

        try {

            connection = factory.createConnection();

            connection.start();

            session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);

            Queue queue = session.createQueue("qhs-queue");

            queueConsumer = session.createConsumer(queue);

            queueConsumer.setMessageListener(new MessageListener() {

                @Override

                public void onMessage(Message message) {

                    try {

                        System.out.println(((TextMessage)message).getText());

                    } catch (JMSException e) {

                        e.printStackTrace();

                    }

                }

            });

            Topic topic = session.createTopic("qhs-topic");

            topicConsumer = session.createConsumer(topic);

            topicConsumer.setMessageListener(new MessageListener() {

                @Override

                public void onMessage(Message message) {

                    try {

                        System.out.println(((TextMessage)message).getText());

                    } catch (JMSException e) {

                        e.printStackTrace();

                    }

                }

            });

            System.in.read();

        } catch (Exception e) {

            e.printStackTrace();

        }finally {

            try {

                queueConsumer.close();

            } catch (JMSException e) {

                e.printStackTrace();

            }

            try {

                topicConsumer.close();

            } catch (JMSException e) {

                e.printStackTrace();

            }

            try {

                session.close();

            } catch (JMSException e) {

                e.printStackTrace();

            }

            try {

                connection.close();

            } catch (JMSException e) {

                e.printStackTrace();

            }

        }

    }

    public static void testTopic() {

        ConnectionFactory factory = new ActiveMQConnectionFactory("tcp://192.168.137.1:61616");

        Connection connection = null;

        Session session = null;

        MessageConsumer consumer = null;

        try {

            connection = factory.createConnection();

            connection.start();

            session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);

            System.in.read();

        } catch (Exception e) {

            e.printStackTrace();

        }finally {

            try {

                consumer.close();

            } catch (JMSException e) {

                e.printStackTrace();

            }

            try {

                session.close();

            } catch (JMSException e) {

                e.printStackTrace();

            }

            try {

                connection.close();

            } catch (JMSException e) {

                e.printStackTrace();

            }

        }

    }

}

运行程序,先运行接收端,再运行发送端即可(topic消息默认不持久化)。
通过web控制台可以查看到对应的消息。

4.一些特征

  • 点对点模式不会造成重复消费,一个生产者的消息只能给一个消费者消费;
  • 发布订阅模型和点对点模型不一样,若发布消息之前未启动订阅者,则订阅者启动时,订阅者也无法接收到消息(未持久化状态)。另外,发布订阅模型,消费者是可以重新消费的;
  • 消息保序方式是broker维持了一个consumer列表,对于同一session的消息取列表中的第一个来发送消息;

5.实现机制

5.1 可靠性保证

  1. 自动签收,生产者向队列发送消息后,只要消费者监听了消息队列,消费者将立刻获得消息,不管消费者是否成功取得消息,过程是否抛出异常导致消费者无法获得消息,都不会触发重试机制。--没有事务机制,没有补偿机制
  2. 事务签收,对于生产者而言,生产者要想向消息队列发送消息,必须提交事务。对于消费者而言,如果消费没有提交事务,则默认表示没有消费,会触发重试机制。-- 双方事务提交
  3. 手动签收,需要消费者手动签收,如果消费者没有进行签收,则默认消息没有被消费。--单方事务提交

代码范例:


//场景1 

//生产者不开启事务,客户端必须有手动签收模式

Session session = createConnection.createSession(Boolean.FALSE, Session.CLIENT_ACKNOWLEDGE);

//消费者不开启事务,客户端必须有手动签收模式

TextMessage textMessage = (TextMessage) createConsumer.receive();

//接受消息

textMessage.acknowledge();

//场景2

//生产者不开启事务,客户端自动签收模式

Session session = createConnection.createSession(Boolean.FALSE, Session.AUTO_ACKNOWLEDGE);

//消费者不开启事务,自动签收消息

Session session = createConnection.createSession(Boolean.FALSE, Session.AUTO_ACKNOWLEDGE);

//场景3

//在支持事务的session中,producer发送message时在message中带有transactionID。broker收到message后判断是否有transactionID,如果有就把message保存在transaction store中,等待commit或者rollback消息

//事务消息 生产者以事务形式,必须要将消息提交事务,才可以提交到队列中。

Session session = createConnection.createSession(Boolean.TRUE, Session.AUTO_ACKNOWLEDGE);

session.commit();

//消费者 消费完后必须提交,不提交生产者不知道消费者消费了

Session session = createConnection.createSession(Boolean.TRUE, Session.AUTO_ACKNOWLEDGE);

session.commit();

5.2 事务机制

  • 消息事务是在生产者producer到broker或broker到consumer过程中同一个session中发生的,保证几条消息在发送过程中的原子性。(Broker:消息队列核心,相当于一个控制中心,负责路由消息、保存订阅和连接、消息确认和控制事务)
  • 消息生产者-异步发送
    消息生产者使用持久(persistent)传递模式发送消息的时候,Producer.send() 方法会被阻塞,直到 broker 发送一个确认消息给生产者(ProducerAck),这个确认消息暗示broker已经成功接收到消息并把消息保存到二级存储中。这个过程通常称为同步发送。
    如果应用程序能够容忍一些消息的丢失,那么可以使用异步发送。异步发送不会在受到 broker 的确认之前一直阻塞 Producer.send 方法。但有一个例外,当发送方法在一个事务上下文中时,被阻塞的是 commit 方法而不是 send 方法。commit 方法成功返回意味着所有的持久消息都以被写到二级存储中。
    想要使用异步,在brokerURL中增加 jms.alwaysSyncSend=false&jms.useAsyncSend=true
    如果设置了alwaysSyncSend=true系统将会忽略useAsyncSend设置的值都采用同步
  1. 当alwaysSyncSend=false时,“NON_PERSISTENT”(非持久化)、事务中的消息将使用“异步发送”;
  2. 当alwaysSyncSend=false时,如果指定了useAsyncSend=true,“PERSISTENT”类型的消息使用异步发送。如果useAsyncSend=false,“PERSISTENT”类型的消息使用同步发送。

总结:

  • 默认情况(alwaysSyncSend=false,useAsyncSend=false),非持久化消息、事务内的消息均采用异步发送;对于持久化消息采用同步发送。
    jms.sendTimeout:发送超时时间,默认等于0,如果jms.sendTimeout>0将会忽略(alwaysSyncSend、useAsyncSend、消息是否持久化)所有的消息都是用同步发送!
  • 即使使用异步发送,也可以通过producerWindowSize来控制发送端无节制的向broker发送消息:
    producerWindowSize:窗口尺寸,用来约束在异步发送时producer端允许积压的(尚未ACK)的消息的尺寸,且只对异步发送有意义。每次发送消息之后,都将会导致memoryUsage尺寸增加(+message.size),当broker返回producerAck时,如果达到了producerWindowSize上限,即使是异步调用也会被阻塞,防止不停向broker发送消息。
    通过jms.producerWindowSize=。。。来设置

5.3 持久化机制

ActiveMQ的消息持久化机制有JDBC,AMQ,KahaDB和LevelDB,无论使用哪种持久化方式,消息的存储逻辑都是一致的。在发送者将消息发送出去后,消息中心首先将消息存储到本地数据文件、内存数据库或者远程数据库等,然后试图将消息发送给接收者,发送成功则将消息从存储中删除,失败则继续尝试。消息中心启动以后首先要检查指定的存储位置,如果有未发送成功的消息,则需要把消息发送出去。

5.4 消息幂等性

消费者保证消息幂等性,不被重复消费
网络延迟传输中,会造成进行MQ重试中,在重试过程中,可能会造成重复消费
使用全局MessageID判断消费方使用同一个,解决幂等性
activeMQ提供了textMessageId,使用业务ID(订单号)也可以

String jmsMessageId = textMessage.getJMSMessageID();

网络延迟环境下,第二次请求过来,应该使用全局ID判断该消息是否被使用过

if(jmsMessageId == redis内的id){

    //把消息签收掉,否则将继续重试,有些人觉得难理解,我解释一下:

    textMessage.acknowledge();  //避免第三次重试

   // 消息可以重发,但是消息不能做重复操作,如重复向数据库中做插入操作,造成幂等性问题,

    //所以当第二次发送过来的时候,就可能造成重复提交问题,我们使用手动提交(业务中一般也是使用手动提交多)

    //手动提交可以把重复发送的消息从队列中移除,那么接下来就不会触发重试了。

}

    //将拿到的消息做业务处理,如插入修改操作等...

   // 消费成功,把jmsMessageId放入redis

5.5 队列消息过滤收取

不同消息可以放到不同的队列,也可以放到一个队列中,通过增加消息的属性,在发送message的时候,可以设置它的消息属性值,然后在消费端建立消费队列时,传入过滤条件即可。

发送端:


MapMessage message1 = session.createMapMessage();

          message1.setString("name","张1");//设置了name值

          message1.setIntProperty("age",23);//设置了age属性

接受端:

MessageConsumer consumer = session.createConsumer(queue,"age > 30");

这样,这个consumer就只消费这个queue中age>30的消息。

5.6 消息保序性

利用Activemq的高级特性:consumer之独有消费者(exclusive consumer)

  queue = new ActiveMQQueue("TEST.QUEUE?consumer.exclusive=true");

这样会确保consumer列表中,始终取一个主消费者来消费。但是缺点在于将会始终发往这个消费者,导致无法负载均衡。比如,订单消息,希望一个订单内的消息是保序的,但各个订单之间可以并发,这就需要用到下文消息组 messageGroup 的概念。

Message Groups特性是一种负载均衡的机制。在一个消息被分发到consumer之前,broker首先检查消息JMSXGroupID属性。如果存在,那么broker会检查是否有某个consumer拥有这个message group。如果没有,那么broker会选择一个consumer,并将它关联到这个message group。此后,这个consumer会接收这个message group的所有消息,直到:

  • Consumer被关闭
  • Message group被关闭,通过发送一个消息,并设置这个消息的JMSXGroupSeq为-1

例如:

message.setStringProperty("JMSXGroupID", "constact-20100000002");
.....
message.setIntProperty("JMSXGroupSeq", -1);

5.6 死信队列

在使用Message Queue的过程中,总会由于种种原因而导致消息失败。一个经典的场景是一个生成者向Queue中发消息,里面包含了一组邮件地址和邮件内容。而消费者从Queue中将消息一条条读出来,向指定邮件地址发送邮件。消费者在发送消息的过程中由于种种原因会导致失败,比如网络超时、当前邮件服务器不可用等。这样我们就希望建立一种机制,对于未发送成功的邮件再重新发送,也就是重新处理。重新处理超过一定次数还不成功,就放弃对该消息的处理,记录下来,继续对剩余消息进行处理。

ActiveMQ为我们实现了这一功能,叫做ReDelivery(重新投递)。当消费者在处理消息时有异常发生,会将消息重新放回Queue里,进行下一次处理。当超过重试次数时,会给broker发送一个"Poison ack",这个消息被认为是a poison pill(毒丸),这时broker会将这个消息发送到DLQ。

在以下四种情况中,ActiveMQ消息会被重发给客户端/消费者:

  • 在一个事务session中,并且调用了session.rollback()方法。
  • 在一个事务session中,session.commit()之前调用了commit.close()。
  • 在session中使用CLIENT_ACKNOWLEDGE签收模式,并且调用了session.recover()方法。
  • 在session中使用AUTO_ACKNOWLEDGE签收模式,在异步(messageListener)消费消息情况下,如果onMessage方法异常且没有被catch,此消息会被redelivery。

缺省情况下:持久消息过期,会被送到DLQ,非持久消息不会送到DLQ(不会redelivery)。
可以在connectionFactory中注入自定义的redeliveryPolicy来改变缺省参数。
activeMQ默认是发送六次,每次间隔1秒。

6.常见问题

6.1 消息堆积导致服务不可用

  • ActiveMQ分为持久化和非持久化。非持久化消息是存储在内存中的,持久化消息是存储在文件中的,它们的最大限制在配置文件的节点中配置。
  • 非持久化消息堆积到内存告急时,ActiveMQ会将内存中的非持久化消息写入临时文件中,以腾出内存。(重启后持久化消息会从文件中恢复,非持久化的临时文件会直接删除)
  • 当文件增大到达了配置中的最大限制的时:
  • 持久化情况,到达文件限额时,生产者阻塞,消费者可连接可消费,消费后生产者继续运行:
    设置2G左右的持久化文件限制,大量生产持久化消息直到文件达到最大限制,此时生产者阻塞,但消费者可正常连接并消费消息,等消息消费掉一部分,文件删除又腾出空间之后,生产者又可继续发送消息,服务自动恢复正常。
  • 非持久化情况,到达文件限额时,生产者阻塞,消费者可连接但不能消费,系统崩溃:
    设置2G左右的临时文件限制,大量生产非持久化消息并写入临时文件,在达到最大限制时,生产者阻塞,消费者可正常连接但不能消费消息,或者原本慢速消费的消费者,消费突然停止。整个系统可连接,但是无法提供服务,就这样挂了。
    解决方案:尽量不要用非持久化消息,非要用的话,将临时文件限制尽可能的调大

6.2 消息丢失

6.3 消息持久化慢

默认情况非持久化的消息是异步发送的,持久化的消息是同步发送的,遇到慢一点的硬盘,发送消息的速度很慢。

但是在开启事务的情况下,消息都是异步发送的,效率会有2个数量级的提升。所以在发送持久化消息时,请务必开启事务模式。发送非持久化消息时也建议开启事务,因为根本不会影响性能;

解决方案:消息发送启用事务;

6.4 消息不均匀消费(存在无法处理消息,prefech机制)

ActiveMQ的一个主要的设计目标是:提供一个高性能的消息中间件。它使用了SEDA(Staged Event Driven Architecture)架构及异步传输。为了提供更高的性能,很重要的一点是 尽快地将消息传送给消费者,这样消费者利用消息缓冲区等待处理,而不是等待消息。
   然后,这样也有很大风险:不断地向 消费者 传送消息可能使得其消息缓冲溢出,因为传送的速度比消费者真正“消费”消息更快,这是很可能。
  因此,ActiveMQ使用了 消息”预取限制“(prefetch limit):表示在某个时间段内,可能向消费者传输的最大消息量,如果达到该上限,那么停止发送,直到ActiveMQ收到消费者的acknowledgements(确认,表示已经处理了该消息)。prefetch limit可以针对每个不同的consumer来设置。
  为了获取更高的性能,prefetch limit当然是越大越好,只要consumer有足够大的消息缓冲区(messagevolume)。如果消息的总量非常少,而且每个消息的处理时间非常的长,那么,可以将prefetch设置为1,这样,每次向consumer发送一个消息,等其确认已经处理完毕后,再发送第二个。
  特别地,如果prefetch设置为0,表示consumer每次 主动向activeMQ要求传输最大的数据量,而不是被动地接收消息

原因在于ActiveMQ的prefetch机制:每个消费者获取消息时,是批量获取的,相当于预定。因为activeMQ提倡是尽量消费理念,所以会采取消费者批量获取信息的方式来提高消费效率。prefech的值默认是1000,即表示一个消费者在取消息时,会一次取1000条到自己的消息缓冲区中(不溢出),然后再一条一条的处理,每处理完一条,mq就会删除这一条,没处理的在mq中依然可见。

在发送一些消息之后,开启2个消费者去处理消息。会发现一个消费者处理了所有的消息,另一个消费者根本没收到消息。原因在于ActiveMQ的prefetch机制。当消费者去获取消息时,不会一条一条去获取,而是一次性获取一批,默认是1000条。这些预获取的消息,在还没确认消费之前,在管理控制台还是可以看见这些消息的,但是不会再分配给其他消费者,此时这些消息的状态应该算作“已分配未消费”,如果消息最后被消费,则会在服务器端被删除,如果消费者崩溃,则这些消息会被重新分配给新的消费者。但是如果消费者既不消费确认,又不崩溃,那这些消息就永远躺在消费者的缓存区里无法处理。更通常的情况是,消费这些消息非常耗时,你开了10个消费者去处理,结果发现只有一台机器吭哧吭哧处理,另外9台啥事不干。

解决方案:将prefetch设为1,每次处理1条消息,处理完再去取,这样也慢不了多少.

另外,当连接来自一个连接池中,消费消息可能出现一些由于“prefetch”而产生的问题:预取的消息(还未被处理)当连接关闭时会被释放(即,可以在activeMQ中再次读取到该消息)。而连接池中的连接只有在连接池关闭后才真正的销毁。这使得 预取的消息直到连接被重用时才会被处理(或者等连接池关闭,可再次从activeMQ中读取)。这样导致了消息可能丢失,或者当连接池中有多个连接时,消息乱序(out-of-sequence)!

  • 消费者则不放入连接池!当然,这样在消费者的性能上,会受到影响(当有多个线程快速的消费消息时,对象被不断的创建销毁)。
  • 将消费者的连接池中数量设为1。
  • 在使用连接池的情况下,将prefetch设为1或者0。当使用Spring JMS和MessageDrivenPojo时,只能将prefetch设为1,而不能为0;

你可能感兴趣的:(activeMQ 入门指南)