消息中间件之ActiveMQ

 

一、为什么需要MQ?

主要原因是由于在高并发环境下,由于来不及同步处理,请求往往会发生堵塞,比如说,大量的insert,update之类的请求同时到达MySQL,直接导致无数的行锁表锁,甚至最后请求会堆积过多,从而触发too many connections错误。通过使用消息队列,我们可以异步处理请求,从而缓解系统的压力。

RPC和消息中间件的不同很大程度上就是“依赖性”和“同步性”。RPC方式是典型的同步方式,让远程调用像本地调用。消息中间件方式属于异步方式。消息队列是系统级、模块级的通信。RPC是对象级、函数级通信。

消息中间件常常用于:异步处理、应用解耦、流量削峰、日志处理、消息通讯

1.MQ对比与选型

  • 中小型项目用于解耦和异步操作,可考虑ActiveMQ,简单易用,对队列数较多的情况支持不好。
  • RabbitMQ,erlang开发,性能较稳定,社区活跃度高,但是不利于做二次开发和维护。ActiveMQ和RabbitMQ都适用于中小型公司,技术挑战不是特别高。
  • 大公司,基础架构研发实力较强,用RocketMQ不错,支持海量消息,但并没有实现JMS规范,使用起来很简单。
  • 大数据领域、日志采集等场景,Kafka是标准,其社区活跃度也很高。

Apache ActiveMQ下载:http://activemq.apache.org/download-archives.html
运行后在浏览器中访问http://127.0.0.1:8161/admin,出现管理界面时,可采用如下用户名和密码进行登录:admin/admin。

2.JMS规范:

JMS即Java消息服务(Java Message Service)应用程序接口是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。Java消息服务是一个与具体平台无关的API,绝大多数MOM提供商都对JMS提供支持。

JMS是一种与厂商无关的 API,用来访问消息收发系统消息。它类似于JDBC:这里,JDBC 是可以用来访问许多不同关系数据库的 API,而 JMS 则提供同样与厂商无关的访问方法,以访问消息收发服务。JMS 使您能够通过消息收发服务(有时称为消息中介程序或路由器)从一个 JMS 客户机向另一个 JMS客户机发送消息。消息是 JMS 中的一种类型对象,由两部分组成:报头和消息主体。报头由路由信息以及有关该消息的元数据组成。消息主体则携带着应用程序的数据或有效负载。根据有效负载的类型来划分,可以将消息分为几种类型,它们分别携带:简单文本(TextMessage)、可序列化的对象 (ObjectMessage)、属性集合 (MapMessage)、字节流 (BytesMessage)、原始值流 (StreamMessage),还有无有效负载的消息 (Message)。

  • 连接工厂。连接工厂(ConnectionFactory)是由管理员创建,并绑定到JNDI树中。客户端使用JNDI查找连接工厂,然后利用连接工厂创建一个JMS连接。

  • JMS连接。JMS连接(Connection)表示JMS客户端和服务器端之间的一个活动的连接,是由客户端通过调用连接工厂的方法建立的。

  • JMS会话。JMS会话(Session)表示JMS客户与JMS服务器之间的会话状态。JMS会话建立在JMS连接上,表示客户与服务器之间的一个会话线程。

  • JMS目的。JMS目的(Destination),又称为消息队列,是实际的消息源。

  • JMS生产者和消费者。生产者(Message Producer)和消费者(Message Consumer)对象由Session对象创建,用于发送和接收消息。

3.JMS消息通常有两种类型

点对点(Point-to-Point)。在点对点的消息系统中,消息分发给一个单独的使用者。点对点消息往往与队列(javax.jms.Queue)相关联。如果希望发送的每个消息都会被成功处理的话,那么需要P2P模式

点对点

 

  • 每个消息只有一个消费者(Consumer)(即一旦被消费,消息就不再在消息队列中)
  • 发送者和接收者之间在时间上没有依赖性,也就是说当发送者发送了消息之后,不管接收者有没有正在运行,它不会影响到消息被发送到队列
  • 接收者在成功接收消息之后需向队列应答成功

发布/订阅(Publish/Subscribe)。发布/订阅消息系统支持一个事件驱动模型,消息生产者和消费者都参与消息的传递。生产者发布事件,而使用者订阅感兴趣的事件,并使用事件。该类型消息一般与特定的主题(javax.jms.Topic)关联。如果希望发送的消息可以不被做任何处理、或者只被一个消息者处理、或者可以被多个消费者处理的话,那么可以采用Pub/Sub模型。

发布订阅模式

 

  • 每个消息可以有多个消费者
  • 发布者和订阅者之间有时间上的依赖性。针对某个主题(Topic)的订阅者,它必须创建一个订阅者之后,才能消费发布者的消息。
  • 为了消费消息,订阅者必须保持运行的状态。

在JMS中,消息的产生和消费都是异步的。对于消费来说,JMS的消息者可以通过两种方式来消费消息。

  • (1)同步:订阅者或接收者通过receive方法来接收消息,receive方法在接收到消息之前(或超时之前)将一直阻塞;

 

/* 创建消息消费者*/
messageConsumer = session.createConsumer(destination);
Message message;
while ((message = messageConsumer.receive()) != null) {
      System.out.println(((TextMessage) message).getText());
}
  • (2)异步:订阅者或接收者可以注册为一个消息监听器。当消息到达之后,系统自动调用监听器的onMessage方法。

 

/* 创建消息消费者*/
messageConsumer = session.createConsumer(destination);
/* 设置消费者监听器,监听消息*/
messageConsumer.setMessageListener(new MessageListener() {
     public void onMessage(Message message) {
         try {
              System.out.println(((TextMessage) message).getText());
         } catch (JMSException e) {
              e.printStackTrace();
         }
     }
});

二、ActiveMQ的使用

使用Active中间件只需要在pom文件中添加如下配置项即可,非常简便:

maven依赖

创建会话的参数:

 

/* 创建session*/
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);

第一个参数是否使用事务:当消息发送者向消息提供者(即消息代理)发送消息时,消息发送者等待消息代理的确认,没有回应则抛出异常,消息发送程序负责处理这个错误。

第二个参数消息的确认模式

  • AUTO_ACKNOWLEDGE: 指定消息接收者在每次收到消息时自动发送确认。消息只向目标发送一次,但传输过程中可能因为错误而丢失消息。
  • CLIENT_ACKNOWLEDGE: 由消息接收者确认收到消息,通过调用消息的acknowledge()方法(会通知消息提供者收到了消息)
  • DUPS_OK_ACKNOWLEDGE: 指定消息提供者在消息接收者没有确认发送时重新发送消息(这种确认模式不在乎接收者收到重复的消息)。

集成Spring

1、Maven中添加依赖:

 

<dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jms</artifactId>
      <version>4.3.11.RELEASE</version>
</dependency>

2、消息生产者模式:
生产者在Spring的配置文件中增加ActiveMQ相关配置,包括命名空间、连接工厂、连接池配置

 

    <amq:connectionFactory id="amqConnectionFactory" 
     brokerURL="tcp://127.0.0.1:61616" userName="" password=""/>

    <!-- Spring用于管理真正的ConnectionFactory的ConnectionFactory -->
    <bean id="connectionFactory"
          class="org.springframework.jms.connection.CachingConnectionFactory">
        <property name="targetConnectionFactory" ref="amqConnectionFactory"></property>
        <property name="sessionCacheSize" value="100"></property>
    </bean>

定义生产者模式:

 

    <!-- 定义JmsTemplate的Queue类型 -->
    <bean id="jmsQueueTemplate" class="org.springframework.jms.core.JmsTemplate">
        <constructor-arg ref="connectionFactory"></constructor-arg>
        <!-- 队列模式-->
        <property name="pubSubDomain" value="false"></property>
    </bean>

    <!-- 定义JmsTemplate的Topic类型 -->
    <bean id="jmsTopicTemplate" class="org.springframework.jms.core.JmsTemplate">
        <constructor-arg ref="connectionFactory"></constructor-arg>
        <!-- 发布订阅模式-->
        <property name="pubSubDomain" value="true"></property>
    </bean>

在Java代码里封装生产者:

 

@Component("topicSender")
public class TopicSender {

    @Autowired
    @Qualifier("jmsTopicTemplate")
    private JmsTemplate jmsTemplate;

    public void send(String queueName, final String message) {
        jmsTemplate.send(queueName, new MessageCreator() {
            @Override
            public Message createMessage(Session session) throws JMSException {
                TextMessage textMessage = session.createTextMessage(message);
                return textMessage;
            }
        });
    }
}

定义好上述bean对象以后,提供出相应的send()方法,可以再Spring框架的Service层中进行方法的调用。

3、消息消费者模式
同样的需要定义好Jms的连接工厂、连接池配置,这部分同生产者的配置文件保持一致,不同的是需要定义好消费者的消费模板:

 

<!-- 定义Topic监听器 -->
    <jms:listener-container destination-type="topic" container-type="default"
                            connection-factory="connectionFactory" acknowledge="auto">
        <jms:listener destination="topicDemo1" ref="topicReceiver1"></jms:listener>
        <jms:listener destination="topicDemo2" ref="topicReceiver2"></jms:listener>
    </jms:listener-container>

    <!-- 定义Queue监听器 -->
    <jms:listener-container destination-type="queue" container-type="default"
                            connection-factory="connectionFactory" acknowledge="auto">
        <jms:listener destination="queueDemo1" ref="queueReceiver1"></jms:listener>
        <jms:listener destination="queueDemo2" ref="queueReceiver2"></jms:listener>
    </jms:listener-container>

在Java代码里封装消费者:

 

@Component
public class QueueReceiver1 implements MessageListener {
    @Override
    public void onMessage(Message message) {
        try {
            String messgeStr= ((TextMessage) message).getText();
            // ....业务逻辑...
        } catch (JMSException e) {
            e.printStackTrace();
        }

    }
}

4、扩展的P2P模式——请求应答
Request-Response的通信方式很常见,但不是默认提供的。在前面的两种模式中都是一方负责发送消息而另外一方负责处理。实际中的很多应用可能需要一应一答,需要双方都能给对方发送消息。请求-应答方式并不是JMS规范系统默认提供的一种通信方式,而是通过在现有通信方式的基础上稍微运用一点技巧实现的。下图是典型的请求-应答方式的交互过程:

请求应答

首先在生产者端配置了特定的监听器(同消费者配置方式一致),监听来自消费者的消息,此处注意目的地tempqueue和引用对象ref的配置:

 

    <!--接收消费者应答的监听器-->
    <jms:listener-container destination-type="queue" container-type="default"
                            connection-factory="connectionFactory" acknowledge="auto">
        <jms:listener destination="tempqueue" ref="getResponse"></jms:listener>
    </jms:listener-container>

实现该监听器(即上述配置文件里对应的ref),并将该Bean对象声明给Spring容器托管

 

@Component
public class GetResponse implements MessageListener {
    @Override
    public void onMessage(Message message) {
        String textMsg = null;
        try {
            textMsg = ((TextMessage) message).getText();
            System.out.println("GetResponse accept msg : " + textMsg);
        } catch (JMSException e) {
            e.printStackTrace();
        }
    }
}

在生产者发送方法send()的代码里配置应答的代码:

 

//配置,告诉消费者如何应答
Destination tempDst = session.createTemporaryQueue();
MessageConsumer responseConsumer = session.createConsumer(tempDst);
responseConsumer.setMessageListener(getResponse);
msg.setJMSReplyTo(tempDst);

同理在消费者这一方需要配置消息生产的模板,方便收到消息后发送应答通知给消息生产方,在Spring配置文件中加入同样的消息发送配置:

 

    <bean id="jmsConsumerQueueTemplate" class="org.springframework.jms.core.JmsTemplate">
        <constructor-arg ref="connectionFactory"></constructor-arg>
        <!-- 队列模式-->
        <property name="pubSubDomain" value="false"></property>
    </bean>

实现应答发送的方法,然后将该Bean对象交给Spring容器管理,此处需要注意在send()方法中声明的两个参数,参数一对应的是发送的消息内容,参数二封装的时候消息生产者的对象(方便从中获取应答的对象信息)。

 

@Component
public class ReplyTo {

    @Autowired
    @Qualifier("jmsConsumerQueueTemplate")
    private JmsTemplate jmsTemplate;

    public void send(final String consumerMsg, Message producerMessage)
            throws JMSException {
        jmsTemplate.send(producerMessage.getJMSReplyTo(),
                new MessageCreator() {
                    @Override
                    public Message createMessage(Session session)
                            throws JMSException {
                        Message msg
                                = session.createTextMessage("ReplyTo " + consumerMsg);
                        return msg;
                    }
                });
    }
}

于是在需要应答的消息处理时引入该Bean对象,即可对收到的消息进行应答处理:

 

    @Autowired
    private ReplyTo replyTo;

    @Override
    public void onMessage(Message message) {
        try {
            String textMsg = ((TextMessage) message).getText();
            // do business work;
            replyTo.send(textMsg,message);
        } catch (JMSException e) {
            e.printStackTrace();
        }
    }

上面步骤就完成了一个扩展的P2P请求-应答(Request-Response)模式,只是在原先的消息生产者加入监听、消息的消费方加入了针对消息的应答处理逻辑实现。

三、ActiveMQ高阶应用

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

消息的持久化机制

1、JDBC持久化(推荐)
使用JDBC持久化方式,数据库会创建3个表:activemq_msgs,activemq_acksactivemq_lock
activemq_msgs用于存储消息,QueueTopic都存储在这个表中。配置持久化的方式,都是修改安装目录下conf/acticvemq.xml文件,首先定义一个mysql-ds的MySQL数据源,然后在persistenceAdapter节点中配置jdbcPersistenceAdapter并且引用刚才定义的数据源。

 

<beans>
    <broker brokerName="test-broker" persistent="true" xmlns="http://activemq.apache.org/schema/core">
        <persistenceAdapter>
            <jdbcPersistenceAdapter dataSource="#mysql-ds" createTablesOnStartup="false"/>
        </persistenceAdapter>
    </broker>

    <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://localhost/activemq?relaxAutoCommit=true"/>
        <property name="username" value="activemq"/>
        <property name="password" value="activemq"/>
        <property name="maxActive" value="200"/>
        <property name="poolPreparedStatements" value="true"/>
    </bean>
</beans>

2. AMQ方式(不推荐)
性能高于JDBC,写入消息时,会将消息写入日志文件,由于是顺序追加写,性能很高。为了提升性能,创建消息主键索引,并且提供缓存机制,进一步提升性能。每个日志文件的大小都是有限制的(默认32m,可自行配置)。
虽然AMQ性能略高于下面的Kaha DB方式,但是由于其重建索引时间过长,而且索引文件占用磁盘空间过大,所以已经不推荐使用。

3.KahaDB方式
KahaDB是从ActiveMQ 5.4开始默认的持久化插件,KahaDb恢复时间远远小于其前身AMQ并且使用更少的数据文件,所以可以完全代替AMQ。kahaDB的持久化机制同样是基于日志文件,索引和缓存。

4.LevelDB方式
从ActiveMQ 5.6版本之后,又推出了LevelDB的持久化引擎。目前默认的持久化方式仍然是KahaDB,不过LevelDB持久化性能高于KahaDB,可能是以后的趋势。在ActiveMQ 5.9版本提供了基于LevelDB和Zookeeper的数据复制方式,用于Master-slave方式的首选数据复制方案。

《ActiveMQ的几种消息持久化机制》

消息的持久化订阅

在上述持久化机制中默认是对P2P模式开启了,但是主题订阅模式下如果需要持久化订阅则还需要做一些额外的工作,主要是在消费端这边进行一些特殊处理:
1、设置客户端id:connection.setClientID("clientID");

 

connection.setClientID("Mark");

2、消息的destination变为 Topic (原先是Destination)

 

Topic destination = session.createTopic("DurableTopic");

消费者类型变为TopicSubscriber

 

//任意名字,代表订阅名
messageConsumer = session.createDurableSubscriber(destination,"durableSubscriber");

运行一次消费者,将消费者在ActiveMQ上进行一次注册。在ActiveMQ的管理控制台subscribers页面可看见消费者。生产者端这边是不需要做特殊处理,但是需要注意的是生产者可以对消息是否持久化的处理,而这个配置就会影响到下游的消费者是否能进行持久化订阅,配置是取的枚举值而来:

 

public interface DeliveryMode {
    int NON_PERSISTENT = 1;
    int PERSISTENT = 2;
}

消息的可靠性保证

对于某些重要的涉及资金和交易业务的消息传输需要有可靠保证,除了上述提到的消息持久化,还包括两个方面,一是生产者发送的消息可以被ActiveMQ收到,二是消费者收到了ActiveMQ发送的消息,这需要在两端都进行配置。

生产端,创建会话的时候:

  • 未开启事务,调用send()方法会以同步方式进行消息的发送,send()方法阻塞直到 broker(消息中间件的实例) 收到消息并返回确认消息给发送者。
  • 开启事务,异步发送,send()方法不被阻塞。但是commit()方法会被阻塞,直到收到来自 broker 的确认消息。

 

/* 创建消息会话*/
Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
/* 创建消息生产者*/
messageProducer = session.createProducer(destination);
/* 异步发送*/
messageProducer.send(textMessage);
/* commit()阻塞直到确认方能提交*/
session.commit();

消费端:消息的四种确认机制,同样也是在会话创建之时进行配置定义

ACK机制描述
AUTO_ACKNOWLEDGE = 1自动确认
CLIENT_ACKNOWLEDGE = 2客户端手动确认
DUPS_OK_ACKNOWLEDGE = 3自动批量确认
SESSION_TRANSACTED = 0事务提交并确认
  • 当消费方配置为true事务时,默认为SESSION_TRANSACTED = 0。事务中的消息可以确认时,调用commit方法将让当前所有消息被确认。在事务开始的任何时机调用rollback(),意味着当前事务的结束,事务中所有的消息都将被重发。当然在commit之前抛出异常,也会导致事务的rollback。

  • AUTO_ACKNOWLEDGE客户端自动确认模式,这里分两种情况来看,如果采用“同步”messageConsumer.receive()方法,返回message给消息时会立即确认。而在"异步"(messageListener)方式中,如果onMessage方法正常结束,消息将会正常确认。如果方法异常,将导致消费者要求ActiveMQ重发消息。
    消息的重发有次数限制,消息中内置属性“redeliveryCounter”计数器,记录一条消息的重发次数,一旦达到阀值,消息将会被删除或者迁移到死信通道中。最好在方法中用try catch处理,可记录异常信息。同时也需要注意onMessage方法中逻辑是否能够兼容对重复消息的判断。

  • CLIENT_ACKNOWLEDGE客户端手动确认,需要在代码里择机确认,调用 message.acknowledge()方法。可以逐条确认消息或者批量处理完后再确认消息,自行权衡。

  • DUPS_OK_ACKNOWLEDGE自动批量确认机制,具有“延迟”确认的特点,由ActiveMQ决定批量自动进行确认。

死信队列

DLQ-死信队列(Dead Letter Queue)用来保存处理失败或者过期的消息。出现以下情况时,消息会被重发:

  • A transacted session is used and rollback() is called(使用一个事务session,并且调用了rollback()方法).
  • A transacted session is closed before commit is called(一个事务session,关闭之前调用了commit).
  • A session is using CLIENT_ACKNOWLEDGE and Session.recover() is called(在session中使用CLIENT_ACKNOWLEDGE签收模式,并且调用了Session.recover()方法)。

当一个消息被重发次数超过maximumRedeliveries(缺省为6次)次数时,会给broker发送一个"Poison ack",这个消息被认为是a poison pill(毒丸),这时broker会将这个消息发送到DLQ,以便后续处理。在业务中可以单独使用死信消费者处理这些死信,其处理方式和普通的消息消费者是一样的。综合来看,死信队列的消息处理一般有如下解决方案:

  • 重发:对于安全性要求比较高的系统,那需要将发送失败的消息进行重试发送,甚至在消息一直不能到达的情况下给予相关的邮件、短信等必要的告警措施以保证消息的正确投递。
  • 丢弃:在消息不是很重要以及有其他通知手段的情况下,那么对消息做丢弃处理也不失为一种好办法,毕竟如果大量不可抵达的消息存在于消息系统中会对我们的系统造成非常大的负荷,所以也会采用丢弃的方式进行处理。

虚拟Destinations实现消费者分组与简单路由

ActiveMQ支持的虚拟Destinations分为有两种,分别是

  • 虚拟主题(Virtual Topics)
  • 组合 Destinations(CompositeDestinations)

这两种虚拟Destinations可以看做对简单的topic和queue用法的补充,基于它们可以实现一些简单有用的EIP功能,虚拟主题类似于1对多的分支功能+消费端的cluster+failover,组合Destinations类似于简单的destinations直接的路由功能。

虚拟主题(Virtual Topics)
ActiveMQ中,topic只有在持久订阅(durablesubscription)下是持久化的。存在持久订阅时,每个持久订阅者,都相当于一个持久化的queue的客户端,它会收取所有消息。这种情况下存在两个问题:

  1. 同一应用内consumer端负载均衡的问题:同一个应用上的一个持久订阅不能使用多个consumer来共同承担消息处理功能,因为每个都会获取所有消息。queue模式可以解决这个问题,broker端又不能将消息发送到多个应用端。所以,既要发布订阅,又要让消费者分组,这个功能jms规范本身是没有的。

  2. 同一应用内consumer端failover的问题:由于只能使用单个的持久订阅者,如果这个订阅者出错,则应用就无法处理消息了,系统的健壮性不高。

为了解决这两个问题,ActiveMQ中实现了虚拟Topic的功能。使用起来非常简单。对于消息发布者来说,就是一个正常的Topic,名称以VirtualTopic.开头。例如VirtualTopic.TEST

对于消息接收端来说,是个队列,不同应用里使用不同的前缀作为队列的名称,即可表明自己的身份即可实现消费端应用分组。 例如Consumer.A.VirtualTopic.TEST,说明它是名称为A的消费端,同理Consumer.B.VirtualTopic.TEST说明是一个名称为B的客户端。可以在同一个应用里使用多个consumer消费此queue,则可以实现上面两个功能。又因为不同应用使用的queue名称不同(前缀不同),所以不同的应用中都可以接收到全部的消息。每个客户端相当于一个持久订阅者,而且这个客户端可以使用多个消费者共同来承担消费任务。



作者:YitaiCloud
链接:https://www.jianshu.com/p/cd8e037e11ff
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值