JMS规范及ActiveMQ的使用

1. 初步认识消息中间件

1.1什么是消息中间件?

消息中间件是值利用高效可靠的消息传递机制进行平台无关的数据交流,并基于数据通信来进行分布式系统的集成。通过提供消息传递和消息排队模型,可以在分布式架构下扩展进程之间的通信。

1.2消息中间件能做什么?

消息中间件主要解决的就是分布式系统之间消息传递的问题,它能够屏蔽各种平台以及协议之间的特性,实现应用程序之间的协同。

1.3应用场景

1.3.1异步处理

如下一个电商平台的注册功,需要完成一系类操作,,如果4部分操作串行执行每一个操作,都需要消耗 1s,那么整个注册过程就需要耗时 4s 才能响应给用户

但是我们从注册这个服务可以看到,每一个子操作都是相对独立的,没有数据的强一致性要求所以我们可以对这些子操作进行来实现异步化执行,类似于多线程并行处理的概念。

如何实现异步化呢?

分布式消息队列就是一个非常好的解决办法,引入分布式消息队列以后,架构图就变成这样了(下图是异步消息队列的场景)。通过引入分布式队列,就能够大大提升程序的处理效率,并且还解决了各个模块之间的耦合问题

 

1.3.2(秒杀场景)通过分布式消息队列来实现流量整形

在电商平台的秒杀场景下,流量会非常大。通过消息队列的方式可以很好的缓解高流量的问题。

用户提交过来的请求,先写入到消息队列。消息队列是有长度的,如果消息队列长度超过指定长度,直接抛弃,秒杀的具体核心处理业务,接收消息队列中消息进行处理,这里的消息处理能力取决于消费端本身的吞吐量

1.3.3分布式事物

消在弱一致性事务模型中,可以采用分布式消息队列的实现最大能力通知做方式来实现数据的最终一致性等等

2JMS

2.1定义

Java 消息服务(Java Message Service)是 java 平台中关 于面向消息中间件的 API,用于在两个应用程序之间,或者分布式系统中发送消息,进行异步通信。

2.2 MOM

JMS 是一个与具体平台无关的 API,绝大多数 MOM (Message Oriented Middleware)(面向消息中间件)提供商都对 JMS 提供了支持。

 

2.2.1什么是 MOM

MOM 是面向消息的中间件,使用消息传送提供者来协调 消息传送操作。MOM 需要提供 API 和管理工具。客户端 使用 api 调用,把消息发送到由提供者管理的目的地。在发送消息之后,客户端会继续执行其他工作,并且在接收方收到这个消息确认之前,提供者一直保留该消息。

2.2.2MOM 的特点

1. 消息异步接收,发送者不需要等待消息接受者响应

2. 消息可靠接收,确保消息在中间件可靠保存。只有接收 方收到后才删除消息 Java 消息传送服务规范最初的开发目的是为了使 Java 应用程序能够访问现有 MOM 系统。引入该规范之后,它已被许多现有的 MOM 供应商采用并且已经凭借自身的 功能实现为异步消息传送系统。

2.3 JMS 规范

我们已经知道了 JMS 规范的目的是为了使得 Java 应用程序能够访问现有 MOM (消息中间件)系统,形成一套统一 的标准规范,解决不同消息中间件之间的协作问题。

在创建 JMS 规范时,设计者希望能够结合现有的消息传送的精髓,比如说

1. 不同的消息传送模式或域,例如点对点消息传送和发布 /订阅消息传送

2. 提供于接收同步和异步消息的工具

3. 对可靠消息传送的支持

4. 常见消息格式,例如流、文本和字节

2.4JMS 的体系结构

 

2.5JMS 提供的具体标准

2.5.1消息传递域

JMS 规范中定义了两种消息传递域:点对点(point-topoint )消息传递域发布 / 消息传递域 (publish/subscribe)

2.5.1.1点对点消息传递域

1. 每个消息只能有一个消费者

2. 消息的生产者和消费者之间没有时间上的相关性。无论 消费者在生产者发送消息的时候是否处于运行状态,都可以提取消息

2.5.1.2发布订阅消息传递域

1. 每个消息可以有多个消费者。

2. 生产者和消费者之间有时间上的相关性。订阅一个主题 的消费者只能消费自它订阅之后发布的消息。JMS 规范 允许客户创建持久订阅,这在一定程度上降低了时间上 的相关性要求。持久订阅允许消费者消费它在未处于激活状态时发送的消息。

2.6消息结构组成

JMS 消息由及部分组成:消息头、属性、消息体

2.6.1消息头

消息头(Header) - 消息头包含消息的识别信息和路由信息, 消息头包含一些标准的属性如:

JMSDestination 消息发送的目的地,queue 或者 topic)

JMSDeliveryMode 传送模式。持久模式和非持久模式

JMSPriority 消息优先级(优先级分为 10 个级别, 从 0(最低)到 9(最高). 如果不设定优先级,默认级别是 4。 需要注意的是,JMS provider 并不一定保证按照优先级的顺序提交消息)

JMSMessageID 唯一识别每个消息的标识

 

2.6.2属性

我们可以给消息设置自定义属性,这些属性主要是提供给应用程序的。对于实现消息过滤功能,消息属性非常有用,JMS API定义了一些标准属性,JMS服务提供者可以选择性的提供部分标准属性。

2.6.3消息体

就是我们需要传递的消息内容,JMS API 定义了 5 中消息体格式,可以使用不同形式发送接收数据,并可以兼容现有的消息格式,其中包括

 

TextMessage

java.lang.String 对象,如 xml 文件内 容

MapMessage

名/值对的集合,名是 String 对象,值 类型可以是 Java 任何基本类型

BytesMessage

字节流

StreamMessage

Java 中的输入输出流

ObjectMessage

Java 中的可序列化对象

Message

没有消息体,只有消息头和属性。

 

3.ActiveMQ

3.1简介

ActiveMQ 是完全基于 JMS 规范实现的一个消息中间件产品。是Apache 开源基金会研发的消息中间件。ActiveMQ 主要应用在分布式系统架构中,帮助构建高可用、高性能、 可伸缩的企业级面向消息服务的系统

3.2ActiveMQ 特性

1. 多语言和协议编写客户端 语言:java/C/C++/C#/Ruby/Perl/Python/PHP 应用协议 : openwire/stomp/REST/ws/notification/XMPP/AMQP

2. 完全支持 jms1.1 和 J2ee1.4 规范

3. 对 spring 的支持,ActiveMQ 可以很容易内嵌到 spring 模块中

 

3.3ActiveMQ 安装

1. 登录到 http://activemq.apache.org/activemq-5150- release.html,找到 ActiveMQ 的下载地址

2. 直 接 copy 到 服 务 器 上 通 过 tar -zxvf apacheactiveMQ.tar.gz

3. 启动运行

a) 普通启动:到 bin 目录下, sh activemq start

b) 启 动 并 指 定 日 志 文 件 sh activemq start > /tmp/activemqlog

4. 检查是否已启动 ActiveMQ默认采用 61616 端口提供 JMS服务,使用 8161 端口提供管理控制台服务,执行以下命令可以检查是否 成功启动 ActiveMQ 服务 netstat -an|grep 61616

5. 通过 http://ip:8161 访问 activeMQ 管理页面 ,默认帐号密码 admin/admin

6. 关闭 ActiveMQ; sh activemq stop

 

3.4 ActiveMQ的使用

由于ActiveMQ是完全基于JMS规范的中间件产品所以具备JMS的特性

 

3.4.1点对点通信

生产者

public class P2PActiveMqProducer {

    public static void main(String[] args) {
        ConnectionFactory connectionFactory =
                    new ActiveMQConnectionFactory("tcp://192.168.255.131:61616");
        Connection connection =null;

        try{
            //创建连接
            connection =connectionFactory.createConnection();
            connection.start();
            //创建会话
            Session session =connection.createSession
                    (Boolean.TRUE,Session.AUTO_ACKNOWLEDGE);
            //创建目的地
            Destination destination =session.createQueue("myQueue");
            //创建发送者
            MessageProducer producer =session.createProducer(destination);
            //创建需要发送的消息
            TextMessage message = session.createTextMessage("Hellow 1111");
            //发送消息
            producer.send(message);
            //向消息队列提交消息
            session.commit();
            //关闭会话
            session.close();
        } catch (JMSException e) {
            e.printStackTrace();
        }finally {
            if (connection!=null){
                try {
                    connection.close();
                } catch (JMSException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

消费者

public class ActiveMQConsumer {

    public static void main(String[] args) {

        ConnectionFactory connectionFactory =

                new ActiveMQConnectionFactory("tcp://192.168.255.131:61616");

        Connection connection =null;

        try{

            //创建连接

            connection =connectionFactory.createConnection();

            connection.start();

            Session session =connection.createSession

                    (Boolean.TRUE,Session.AUTO_ACKNOWLEDGE);

            //监听队列

            Destination destination =session.createQueue("myQueue");

            //创建接受者

            MessageConsumer consumer = session.createConsumer(destination);

            //接受消息

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

                System.out.println(textMessage.getText());

                session.commit();

                session.close();

        } catch (JMSException e) {

            e.printStackTrace();

        }finally {

            if (connection!=null){

                try {

                    connection.close();

                } catch (JMSException e) {

                    e.printStackTrace();

                }

            }

        }

    }

}

通过MessageListener来监听消息队列的消息

public class ActiveMqListenerConsume {

    public static void main(String[] args) {
        {
            ConnectionFactory connectionFactory =
                    new ActiveMQConnectionFactory("tcp://192.168.255.131:61616");
            Connection connection =null;
            try{
                //创建连接
                connection =connectionFactory.createConnection();
                connection.start(); 
                Session session =connection.createSession
                        (Boolean.TRUE,Session.AUTO_ACKNOWLEDGE);
                //监听队列
                Destination destination =session.createQueue("myQueue");
                //创建接受者
                MessageConsumer consumer = session.createConsumer(destination);
                //常创建监听队列
                MessageListener messageListener =new MessageListener() {
                    @Override
                    public void onMessage(Message message) {
                        try {
                            System.out.println(((TextMessage)message).getText());
                        } catch (JMSException e) {
                            e.printStackTrace();
                        }
                    }
                };
                //通过监听消息中间件输出消息
                while (true){
                    consumer.setMessageListener(messageListener);
                    session.commit();
                }
            } catch (JMSException e) {
                e.printStackTrace();
            }finally {
                if (connection!=null){
                    try {
                        connection.close();
                    } catch (JMSException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

3.4.2发布订阅

生产者

public class TopicActiveMqProducer {

    public static void main(String[] args) {
        ConnectionFactory connectionFactory =
                new ActiveMQConnectionFactory("tcp://192.168.255.131:61616");
        Connection connection =null;

        try{
            //创建连接
            connection =connectionFactory.createConnection();
            connection.start();
            //创建会话
            // Boolean.TRUE,指事务会话(可以commit和rollback)
            Session session =connection.createSession
                    (Boolean.TRUE,Session.AUTO_ACKNOWLEDGE);
            //创建目的地(Topic类型发布订阅)
            Destination destination =session.createTopic("MuTopic");
            //创建发送者
            MessageProducer producer =session.createProducer(destination);

            //持久化消息需要设置(消息发送到中间件中消息不会丢失)
            producer.setDeliveryMode(DeliveryMode.PERSISTENT);
            //创建需要发送的消息
            TextMessage message = session.createTextMessage("欢迎大家订阅消息");

            producer.send(message);
            //发送到Mq中 session.rollback();事务回滚
            session.commit();//消息被提交
            session.close();
        } catch (JMSException e) {
            e.printStackTrace();
        }finally {
            if (connection!=null){
                try {
                    connection.close();
                } catch (JMSException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

消费者

public class TopicActiveMQConsumer {
    public static void main(String[] args) {
        ConnectionFactory connectionFactory =
                new ActiveMQConnectionFactory("tcp://192.168.255.131:61616");
        Connection connection =null;

        try{
            //创建连接
            connection =connectionFactory.createConnection();
            connection.start();

            Session session =connection.createSession
                    (Boolean.TRUE,Session.AUTO_ACKNOWLEDGE);
            //监听队列
            Destination destination =session.createTopic("MuTopic");
            //创建接受者
            MessageConsumer consumer = session.createConsumer(destination);
            //接受消息
                TextMessage textMessage =(TextMessage)consumer.receive();
                System.out.println(textMessage.getText());
                session.commit();
                session.close();


        } catch (JMSException e) {
            e.printStackTrace();
        }finally {
            if (connection!=null){
                try {
                    connection.close();
                } catch (JMSException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

持久化订阅

public class TopicActiveMQPersistentConsumer {
    public static void main(String[] args) {
        ConnectionFactory connectionFactory =
                new ActiveMQConnectionFactory("tcp://192.168.255.131:61616");
        Connection connection =null;

        try{
            //创建连接
            connection =connectionFactory.createConnection();
            //设置持久化订阅ID
            connection.setClientID("001");
            connection.start();

            Session session =connection.createSession
                    (Boolean.TRUE,Session.AUTO_ACKNOWLEDGE);
            //监听队列
            Topic destination =session.createTopic("MuTopic");
            //创建接受者(持久化订阅)
            MessageConsumer consumer = session.createDurableSubscriber(destination,"001");
            //接受消息
                TextMessage textMessage =(TextMessage)consumer.receive();
                System.out.println(textMessage.getText());
                session.commit();
                session.close();
        } catch (JMSException e) {
            e.printStackTrace();
        }finally {
            if (connection!=null){
                try {
                    connection.close();
                } catch (JMSException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

3.4具体参数解析

3.4.1持久订阅

持久订阅有两个特点:

1. 持久订阅者和非持久订阅者针对的 Domain 是 Pub/Sub, 而不是 P2P

2. 当 Broker 发送消息给订阅者时,如果订阅者处于未激活状态状态:持久订阅者可以收到消息,而非持久订阅者则收不到消息。

当然这种方式也有一定的影响:当持久订阅者处于未激活状态时,Broker 需要为持久订阅者保存消息;如果持久订阅者订阅的消息太多则会溢出。

消费端改动如上 加入

​
  connection.setClientID("001");//设置持久订阅ID

  Topic destination =session.createTopic("MuTopic");
  //创建接受者(持久化订阅)
  MessageConsumer consumer = session.createDurableSubscriber(destination,"001");

​

3.修改三处地方,然后先启动消费端去注册一个持久订阅。

客户端向 JMS 服务器注册一个自己身份的 ID, 当这个客户端处于离线时,JMS Provider 会为这个ID保存所有发送到主题的消息,当客户再次连接到 JMS Provider 时,会根据自己的 ID 得到所有当自己处于离线时发送到主题的消息。 这个身份ID,在代码中的体现就是 connection的 ClientID,

注意:连接的 clientId 必须是唯一的,订阅者的名称在同一个连接内必须唯一。这样才能唯一的确定连接和订阅者。

3.4.2JMS 消息的可靠性机制

消息的消费通常包含 3 个阶段:客户接收消息、客户处理消息、消息被确认

3.4.2.1JMS会话(事务性会话和非事务性会话的概念)

事务性会话

JMS Session 接口提供了 commit 和 rollback 方法。

  1. 事务提交意味着生产的所有消息被发送,消费的所有消息被确认;
  2. 事务回滚意味着生产的所有消息被销毁,消费的所有消息 被恢复并重新提交,除非它们已经过期。
  3. 事务性的会话总是牵涉到事务处理中,commit 或 rollback 方法一旦被调用,一个事务就结束了,而另一个事务被开始。
  4. 关闭事务性会话将回滚其中的事务,在事务型会话中在事务状态下进行发送操作,消息并未真正投递到中间件, 而只有进行 session.commit 操作之后,消息才会发送到中间件,再转发到适当的消费者进行处理。如果是调用 rollback 操作,则表明,当前事务期间内所发送的消息都取消掉。

通过在创建 session 的时候使用 true or false 来决定当前的会话是事务性还是非事务性

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

在事务性会话中,消息的确认是自动进行,也就是通过 session.commit()以后,消息会自动确认。

注意: 必须保证发送端和接收端都是事务性会话

在非事务型会话中

消息何时被确认取决于创建会话时的应答模式 (acknowledgement mode).有三个可选项

  1. Session.AUTO_ACKNOWLEDGE 当客户成功的从 receive 方法返回的时候,或者从 MessageListenner.onMessage 方法成功返回的时候,会话自动确认客户收到消息。
  2. Session.CLIENT_ACKNOWLEDGE 客户通过调用消息的 acknowledge 方法确认消息
  3. CLIENT_ACKNOWLEDGE 特性:在这种模式中,确认是在会话层上进行,确认一个被消费 的消息将自动确认所有已被会话消费的消息。

列如,如果 一个消息消费者消费了 10 个消息,然后确认了第 5 个消 息,那么 0~5 消息都会被确认

演示如下:

发送端发送 10 个消息,接收端接收 10 个消息, 但是在 i==5 的时候,调用 message.acknowledge()进行 确认,会发现 0~4 的消息都会被确认

  1. Session.DUPS_ACKNOWLEDGE 消息延迟确认。指定消息提供者在消息接收者没有确认发 送时重新发送消息,这种模式不在乎接受者收到重复的消息。

3.4.3消息的持久化存储

消息的持久化存储也是保证可靠性最重要的机制之一,也就是消息发送到 Broker 上以后,如果 broker 出现故障宕 机了,那么存储在 broker 上的消息不应该丢失。

可以通过 下面的代码来设置消息发送端的持久化和非持久化特性

producer.setDeliveryMode(DeliveryMode.PERSISTENT);

 

  1. 对于非持久的消息,JMS provider 不会将它存到文件/数据库等稳定的存储介质中。也就是说非持久消息驻留在 内存中,如果 jms provider 宕机,那么内存中的非持久 消息会丢失
  2. 对于持久消息,消息提供者会使用存储-转发机制,先将消息存储到稳定介质中,等消息发送成功后再删除。如果 jms provider 挂掉了,那么这些未送达的消息不会丢 失;jms provider 恢复正常后,会重新读取这些消息, 并传送给对应的消费者。

4.ActiveMQ消息发送分析

4.1消息同步发送和异步发送

ActiveMQ支持同步、异步两种发送模式将消息发送到broker上

1)同步发送

同步发送过程中,发送者发送一条消息会阻塞直到broker反馈一个确认消息,表示消息已经被broker处理。

这个机 制提供了消息的安全性保障,但是由于是阻塞的操作,会影响到客户端消息发送的性能

2)异步发送

异步发送的过程中,发送者不需要等待broker提供反馈,所以性能相对较高。但是可能会出现消息丢失的情况。

所以使用异步发送的前提是在某些情况下允许出现数据丢失的情况。

默认情况下,非持久化消息是异步发送的,持久化消息并且是在非事务模式下是同步发送的。

但是在开启事务的情况下,消息都是异步发送。

由于异步发送的效率会比同步发送性能更高。所以在发送持久化消 息的时候,尽量去开启事务会话。

3)设置异步发送

除了持久化消息和非持久化消息的同步和异步特性以外,我们还可以通过以下几种方式来设置异步发送消息的发送

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



2.((ActiveMQConnectionFactory) connectionFactory).setUseAsyncSend(true);



3.((ActiveMQConnection)connection).setUseAsyncSend(true);

4.2消息发送的流程图

 

1)ProducerWindowSize的含义

producer每发送一个消息,统计一下发送的字节数,当字节数达到ProducerWindowSize值时,需要等待broker的确认,才能继续发送。

主要用来约束在异步发送时producer端允许积压的(尚未ACK)的消息的大小,且只对异步发送有意义。

每次发送消息之后,都将会导致memoryUsage大小增加(+message.size),

当broker返回producerAck时,memoryUsage尺 寸减少(producerAck.size,此size表示先前发送消息的大小)。

简单理解:ProducerWindowSize就是表示broker节点能够容纳消息数量的空间,当空间不足时阻塞消息发送,当消息被消费时,空间大小减少

可以通过如下2种方式设置:

  1. 在brokerUrl中设置: "tcp://localhost:61616?jms.producerWindowSize=1048576",这种设置将会对所有的 producer生效。
  2. 在destinationUri中设置: "test-queue?producer.windowSize=1048576",此参数只会对使用此Destination实例 的producer失效,将会覆盖brokerUrl中的producerWindowSize值。

 注意:此值越大,意味着消耗Client端的内存就越大。

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

正常情况下,非持久化消息是存储在内存中的,持久化消息是存储在文件中的。

能够存储的最大消息数据在 ${ActiveMQ_HOME}/conf/activemq.xml文件中的systemUsage节点

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。这个子标记就是为了设置这个temp store区域的“可用磁盘空间限制”

                    <tempUsage limit="50 gb"/>

                </tempUsage>

            </systemUsage>

        </systemUsage>

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

4.3.1消息的持久化策略分析

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

4.3.2持久化存储支持类型

ActiveMQ支持多种不同的持久化方式,主要有以下几种,不过,无论使用哪种持久化方式,消息的存储逻辑都是 一致的。

  1. KahaDB存储(默认存储方式)
  2. JDBC存储
  3. Memory存储
  4. LevelDB存储

1)KahaDB存储

KahaDB是目前默认的存储方式,可用于任何场景,提高了性能和恢复能力。消息存储使用一个事务日志和仅仅用一个 索引文件来存储它所有的地址。

KahaDB是一个专门针对消息持久化的解决方案,它对典型的消息使用模式进行了优化。在Kaha中,数据被追加到 data logs中。当不再需要log文件中的数据的时候,log文件会被丢弃

activemq.xml

KahaDB的存储原理

在data/kahadb这个目录下,会生成四个文件

  1. db.data 它是消息的索引文件,本质上是B-Tree(B树),使用B-Tree作为索引指向db-*.log里面存储的消息
  2. db.redo 用来进行消息恢复
  3. db-*.log 存储消息内容。新的数据以APPEND的方式追加到日志文件末尾。属于顺序写入,因此消息存储是比较 快的。默认是32M,达到阀值会自动递增 Ø lock文件 锁,表示当前获得kahadb读写权限的broker

2)JDBC存储

使用JDBC持久化方式,数据库会创建3个表:activemq_msgs,activemq_acks和activemq_lock。

  • ACTIVEMQ_MSGS 消息表,queue和topic都存在这个表中
  • ACTIVEMQ_ACKS 存储持久订阅的信息和最后一个持久订阅接收的消息
  • ID ACTIVEMQ_LOCKS 锁表,用来确保某一时刻,只能有一个ActiveMQ broker实例来访问数据库

3)LevelDB存储

LevelDB持久化性能高于KahaDB,虽然目前默认的持久化方式仍然是KahaDB。并且,在ActiveMQ 5.9版本提供了基于LevelDB和Zookeeper的数据复制方式,用于Master-slave方式的首选数据复制方案。 不过,据ActiveMQ官网对LevelDB的表述:LevelDB官方建议使用以及不再支持,推荐使用的是KahaDB

4)Memory 消息存储

基于内存的消息存储,内存消息存储主要是存储所有的持久化的消息在内存中。persistent=”false”,表示不设置持 久化存储,直接存储到内存中

5)JDBC Message store with ActiveMQ Journal

这种方式克服了JDBC Store的不足,JDBC每次消息过来,都需要去写库和读库。

ActiveMQ Journal,使用高速缓存写入技术,大大提高了性能。 当消费者的消费速度能够及时跟上生产者消息的生产速度时,journal文件能够大大减少需要写入到DB中的消息。

 举个例子,生产者生产了1000条消息,这1000条消息会保存到journal文件,如果消费者的消费速度很快的情况 下,在journal文件还没有同步到DB之前,消费者已经消费了90%的以上的消息,那么这个时候只需要同步剩余的 10%的消息到DB。 如果消费者的消费速度很慢,这个时候journal文件可以使消息以批量方式写到DB。

5.ActiveMQ消息接受分析

5.1消息接受流程图

5.2prefetchSize

activemq 的 consumer 端也有窗口机制,通过 prefetchSize 就可以设置窗口大小。(不同的类型的队列,prefetchSize 的默认值也是不一样的)

  1. 持久化队列和非持久化队列的默认值为 1000
  2. 持久化 topic 默认值为 100
  3. 非持久化队列的默认值为 Short.MAX_VALUE-1

消费端会根据 prefetchSize 的大小批量获取数据,比如默认值是 1000,那么消费端会预先加 1000 条数据到本地的内存中。

prefetchSize 的设置方法

在 createQueue 中添加 consumer.prefetchSize,就可以看到效果

Destination destination=session.createQueue("myQueue?consumer.prefetchSize=10");

5.3optimizeAcknowledge

既然有批量加载,那么一定有批量确认,这样才算是彻底的优化

optimizeAcknowledge ActiveMQ 提供了 optimizeAcknowledge 来优化确认,它表示是否开启“优化 ACK”,只有在为 true 的情况下,prefetchSize 以及 optimizeAcknowledgeTimeout 参数才会有意义

优化确认一方面可以减轻 client 负担(不需要频繁的确认消息)、减少通信开销,另一方面由于延迟了确认(默认 ack 了 0.65*prefetchSize 个消息才确 认),broker 再次发送消息时又可以批量发送 如果只是开启了 prefetchSize,每条消息都去确认的话,broker 在收到确认后 也只是发送一条消息,并不是批量发布,当然也可以通过设置 DUPS_OK_ACK 来手动延迟确认, 我们需要在 brokerUrl 指定 optimizeACK 选项


ConnectionFactory connectionFactory= new ActiveMQConnectionFactory ("tcp://192.168.11.153:61616?jms.optimizeAcknowledge=true&jms.optimizeAcknowledgeTimeOut=10000");

注意,如果 optimizeAcknowledge true,那么 prefetchSize 必须大于 0。当 prefetchSize=0 的时候,表示 consumer 通过 PULL 方式从 broker 获取消息

5.4总结

我们知道了 optimizeAcknowledge 和 prefetchSize 的作用,两者协同工作,通过批量获取消息、并延迟批量确认,来达到一个高效的消息消费模型。

它比仅减少了客户端在获取消息时的阻塞次数,还能减少每次获取消息时的网络通信开销

注意

如果消费端的消费速度比较高,通过这两者组合是能大大提升 consumer 的性能。如果 consumer 的消费性能本身就比较慢,设置比较大的 prefetchSize 反而不能有效的达到提升消费性能的目的。因为过大的 prefetchSize 不利于 consumer 端消息的负载均衡。因为通常情况下,我们都会部署多个 consumer 节点来提升消费端的消费性能。 这个优化方案还会存在另外一个潜在风险,当消息被消费之后还没有来得及确认时,client 端发生故障,那么这些消息就有可能会被重新发送给其他 consumer,那么这种风险就需要 client 端能够容忍“重复”消息。

 

5.5消息的确认过程

消息确认有四种 ACK_MODE

  1. AUTO_ACKNOWLEDGE = 1 自动确认
  2. CLIENT_ACKNOWLEDGE = 2 客户端手动确认
  3. DUPS_OK_ACKNOWLEDGE = 3 自动批量确认
  4. SESSION_TRANSACTED = 0 事务提交并确认

虽然 Client 端指定了 ACK 模式,但是在 Client 与 broker 在交换 ACK 指令的时候,还需要告知 ACK_TYPE,ACK_TYPE 表示此确认指令的类型,不同的 ACK_TYPE 将传递着消息的状态,broker 可以根据不同的 ACK_TYPE 对消息进 行不同的操作。

ACK_TYPE

  1. DELIVERED_ACK_TYPE = 0 消息"已接收",但尚未处理结束
  2. STANDARD_ACK_TYPE = 2 "标准"类型,通常表示为消息"处理成功",broker 端 可以删除消息了
  3. POSION_ACK_TYPE = 1 消息"错误",通常表示"抛弃"此消息,比如消息重发多 次后,都无法正确处理时,消息将会被删除或者 DLQ(死信队列)
  4. REDELIVERED_ACK_TYPE = 3 消息需"重发",比如 consumer 处理消息时抛出 了异常,broker 稍后会重新发送此消息
  5. INDIVIDUAL_ACK_TYPE = 4 表示只确认"单条消息",无论在任何 ACK_MODE 下
  6. UNMATCHED_ACK_TYPE = 5 在 Topic 中,如果一条消息在转发给“订阅者” 时,发现此消息不符合 Selector 过滤条件,那么此消息将 不会转发给订阅 者,消息将会被存储引擎删除(相当于在 Broker 上确认了消息)。

Client 端在不同的 ACK 模式时,将意味着在不同的时机发送 ACK 指令,每个 ACK Command 中会包含 ACK_TYPE,那么 broker 端就可以根据 ACK_TYPE 来决定 此消息的后续操作 消息的重发机制原

5.6消息的重发机制

5.6.1消息重发的情况

 在正常情况下,有几中情况会导致消息重新发送

在事务性会话中,没有调用 session.commit 确认消息或者调用 session.rollback 方法回滚消息

在非事务性会话中,ACK 模式为 CLIENT_ACKNOWLEDGE 的情况下,没有 调用 acknowledge 或者调用了 recover 方法;

一个消息被 redelivedred 超过默认的最大重发次数(默认 6 次)时,消费端会给 broker 发送一个”poison ack”,表示这个消息有毒,告诉 broker 不要再发了。这个时候 broker 会 把这个消息放到 DLQ(死信队列)。

5.6.2死信队列

ActiveMQ 中默认的死信队列是 ActiveMQ.DLQ,如果没有特别的配置,有毒的消息都会被发送到这个队列。默认情况下,如果持久消息过期以后,也会被送到 DLQ 中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值