ActiveMQ

消息中间件之ActiveMQ

一、概述

1.1、MQ的定义

面向消息的中间件(message-oriented middleware)MOM能够很好的解决以上问题。是指利用高效可靠的消息传递机制与平台无关的数据交流,并基于数据通信来进行分布式系统的集成。通过提供消息传递和消息排队模型在分布式环境下提供应用解耦,弹性伸缩,冗余存储、流量削峰,异步通信,数据同步等功能。

大致的过程是这样的:发送者把消息发送给消息服务器,消息服务器将消息存放在若干队列/主题topic中,在合适的时候,消息服务器回将消息转发给接受者。在这个过程中,发送和接收是异步的,也就是发送无需等待,而且发送者和接受者的生命周期也没有必然的关系;尤其在发布pub/订阅sub模式下,也可以完成一对多的通信,即让一个消息有多个接受者。

1.2、MQ的产生背景

系统之间直接调用存在的问题

  • 系统之间接口耦合比较严重
  • 面对大流量并发时,容易被冲垮
  • 等待同步存在性能问题

根据上述的几个问题,在设计系统时可以明确要达到的目标

  • 要做到系统解耦,当新的模块接进来时,可以做到代码改动最小;能够解耦
  • 设置流量缓冲池,可以让后端系统按照自身吞吐能力进行消费,不被冲垮;能削峰
  • 强弱依赖梳理能将非关键调用链路的操作异步化并提升整体系统的吞吐能力;能够异步

1.3、MQ的主要作用

  • 异步
    调用者无需等待。
  • 解耦
    解决了系统之间耦合调用的问题。
  • 消峰
    抵御洪峰流量,保护了主业务。

1.4、有 Broker 的 MQ

这个流派通常有⼀台服务器作为 Broker,所有的消息都通过它中转。生产者把消息发送给它就结束自己的任务了,Broker 则把消息主动推送给消费者(或者消费者主动轮询)

1.4.1、重Topic

Kafka、ActiveMQ就属于这个流派,⽣产者会发送 key 和数据到 Broker,由Broker比较 key 之后决定给哪个消费者。这种模式是我们最常见的模式,是我们对 MQ 最多的印象。在这种模式下⼀个 topic 往往是⼀个比较大的概念,甚至⼀个系统中就可能只有⼀个 topic,topic 某种意义上就是 queue,⽣产者发送 key 相当于说:“hi,把数据放到 key 的队列中”

虽然架构⼀样但是 kafka 的性能要比JMS的性能不知道⾼到多少倍,所以基本这种类型的 MQ 只有 kafka ⼀种备选方案。如果你需要⼀条暴力的数据流(在乎性能而非灵活性)那么 kafka 是最好的选择。

1.4.2、轻Topic

这种的代表是 RabbitMQ(或者说是 AMQP)。⽣产者发送 key 和数据,消费者定义订阅的队列,Broker 收到数据之后会通过⼀定的逻辑计算出 key 对应的队列,然后把数据交给队列。

这种模式下解耦了 key 和 queue,在这种架构中 queue 是⾮常轻量级的(在RabbitMQ中它的上限取决于你的内存),消费者关心的只是自己的 queue;生产者不必关心数据最终给谁只要指定 key 就行了,中间的那层映射在 AMQP 中exchange(交换机)。
AMQP 中有四种 exchange

  • Direct exchange:key 就等于 queue
  • Fanout exchange:无视 key,给所有的 queue 都来⼀份
  • Topic exchange:key 可以用“宽字符”模糊匹配 queue
  • Headers exchange:无视 key,通过查看消息的头部元数据来决定发给那个

queue(AMQP 头部元数据⾮常丰富⽽且可以⾃定义)
这种结构的架构给通讯带来了很⼤的灵活性,我们能想到的通讯方式都可以用这四种 exchange 表达出来。如果你需要⼀个企业数据总线(在乎灵活性)那么RabbitMQ 绝对的值得⼀用。

1.5、无Broker的MQ

无Broker 的 MQ 的代表是 ZeroMQ。 ZeroMQ 被设计成了⼀个“库”而不是⼀个中间件,这种实现也可以达到——没有 Broker 的目的节点之间通讯的消息都是发送到彼此的队列中,每个节点都既是⽣产者⼜是消费者。ZeroMQ 做的事情就是封装出⼀套类似于 Socket 的 API 可以完成发送数据,读取数据

ZeroMQ 其实就是⼀个跨语言的、重量级的 Actor 模型邮箱库。你可以把自己的程序想象成⼀个 Actor,ZeroMQ 就是提供邮箱功能的库;ZeroMQ 可以实现同⼀台机器的 RPC 通讯也可以实现不同机器的 TCP、UDP 通讯。

1.6、MQ的产品种类和对比

MQ就是消息中间件,MQ是一种理念,ActiveMQ是MQ的落地产品。不管是哪款消息中间件,都有如下一些技术维度:

市场上常见的消息队列有如下:

  • ActiveMQ:基于JMS实现, 比较均衡, 不是最快的, 也不是最稳定的。
  • RabbitMQ:基于AMQP协议,erlang语言开发,稳定性好, 数据基本上不会丢失。
  • RocketMQ:基于JMS,阿里巴巴产品, 目前已经捐献给apahce, 还在孵化器中孵化。
  • Kafka:类似MQ的产品;分布式消息系统,高吞吐量, 目前最快的消息服务器, 不保证数据完整性。

在这里插入图片描述

二、JMS框架

2.1、JMS介绍

JMS即Java Message Service(Java消息服务),是Java专门为消息服务定义的⼀套规范。Activemq充分遵循了JMS规范。在JMS中定义了⼀系列的成员来描述这套规范,比如Connection Factory、Connection、Session、Producer、Consumer等等。JMS约定了使用消息服务的多方,进行消息通信的步骤。

2.2、JMS开发的基本步骤

在这里插入图片描述

  • Producer:消息的生产者,发送消息的⼀方。
  • Consumer:消息的消费者,订阅消息的⼀方。
  • Connection:描述连接的对象,生产者/消费者连接消息队列时需要通过Connection对象。
  • ConnectionFactory:生产连接对象的工厂
  • Session:连接后的⼀次会话,生产者/消费者与消息队列之间的会话。
  • Message:描述消息的内容
  • Destination:目的地,泛指生产者消息发送的⽬的地,和消费者订阅消息的来源地。在activemq中,destination有两者具体的实现:
    • queue:队列,某条消息只能被某⼀个消费者订阅
    • topic:主题,某条消息可以同时被多个消费者订阅

在JMS约定的消息服务中,需要由以上几个部分相互配合、协作,完成消息服务中消息的发送和接收。

三、JMS消息服务的实现

3.1、导入依赖

<dependencies>
    <!--activemq的jar包-->
    <dependency>
        <groupId>org.apache.activemq</groupId>
        <artifactId>activemq-all</artifactId>
        <version>5.15.14</version>
    </dependency>
    <!--activemq和spring的整合包 -->
    <dependency>
        <groupId>org.apache.xbean</groupId>
        <artifactId>xbean-spring</artifactId>
        <version>3.16</version>
    </dependency>
</dependencies>

3.2、消息生产者

/**
 * 消息生产者
 */
public class JmsProducer {
    private static final String ACTIVE_URL = "tcp://192.168.0.101:61616";
    private static final String QUEUE_NAME = "queue_1";

    public static void main(String[] args) throws JMSException, IOException {
        //1.创建连接工厂
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVE_URL);

        //2.通过连接工厂 获得连接 并启动
        Connection connection = activeMQConnectionFactory.createConnection();
        connection.start();

        //3.创建会话session
        //两个参数 第一个事务 第二个签收
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);

        //4.创建目的地(具体是队列还是主题)
        Queue queue = session.createQueue(QUEUE_NAME);

        //5.创建消息的生产者
        MessageProducer producer = session.createProducer(queue);

        //6.创建消息对象
        TextMessage textMessage = session.createTextMessage("hello activemq");

        //7.发送消息
        producer.send(textMessage);
        System.out.println("消息已发送");

        //8.关闭连接
        producer.close();
        session.close();
        connection.close();
    }
}

3.3、消息消费者

/**
 * 消息消费者
 */
public class JmsConsumer {
    private static final String ACTIVE_URL = "tcp://192.168.0.101:61616";
    private static final String QUEUE_NAME = "queue_1";

    public static void main(String[] args) throws JMSException, IOException {
        //1.创建连接工厂
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVE_URL);

        //2.通过连接工厂 获得连接 并启动
        Connection connection = activeMQConnectionFactory.createConnection();
        connection.start();

        //3.创建会话session
        //两个参数 第一个事务 第二个签收
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);

        //4.创建目的地(具体是队列还是主题)
        Queue queue = session.createQueue(QUEUE_NAME);

        //5.创建消息的消费者
        MessageConsumer consumer = session.createConsumer(queue);

        //方法1: 同步阻塞方法 使用receive()方法来接收消息, receive方法在能够接收到 消息之前(或超时之前)将一直阻塞
        /*while (true) {
            //TextMessage textMessage = (TextMessage)consumer.receive();
            Message message = consumer.receive(3000L);
            if (message instanceof TextMessage && message != null) {
                TextMessage textMessage = (TextMessage) message;
                String messageText = textMessage.getText();
                System.out.println("接收消息: " + messageText);
            }
        }*/

        //方法2: 监听方式来消费消息
        consumer.setMessageListener(new MessageListener() {
            @Override
            public void onMessage(Message message) {
                if (message instanceof TextMessage && message != null) {
                    try {
                        TextMessage textMessage = (TextMessage) message;
                        String textMessageText = textMessage.getText();
                        System.out.println("接收消息: " + textMessageText);
                    } catch (JMSException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        //保证控制台不灭 暂时不灭
        System.in.read();
    }
}

3.4、ActiveMQ控制台之队列

运行上面代码,控制台显示如下:
在这里插入图片描述

  • Number Of Pending Messages:
    等待消费的消息,这个是未出队列的数量,公式=总接收数-总出队列数。
  • Number Of Consumers:
    消费者数量,消费者端的消费者数量。
  • Messages Enqueued:
    进队消息数,进队列的总消息量,包括出队列的。这个数只增不减。
  • Messages Dequeued:
    出队消息数,可以理解为是消费者消费掉的数量。

当有一个消息进入这个队列时,等待消费的消息是1,进入队列的消息是1。
当消息消费后,等待消费的消息是0,进入队列的消息是1,出队列的消息是1。
当再来一条消息时,等待消费的消息是1,进入队列的消息就是2。

3.5、队列消息(Queue)总结

两种消费方式

  • 同步阻塞方式(receive)
    订阅者或接收者抵用MessageConsumer的receive()方法来接收消息,receive方法在能接收到消息之前(或超时之前)将一直阻塞。

  • 异步非阻塞方式(监听器onMessage())
    订阅者或接收者通过MessageConsumer的setMessageListener(MessageListener listener)注册一个消息监听器,当消息到达之后,系统会自动调用监听器MessageListener的onMessage(Message message)方法。

队列模式的特点

  • 每个消息只能有一个消费者,类似1对1的关系,好比个人快递自己领取自己的。
  • 消息的生产者和消费者之间没有时间上的相关性。无论消费者在生产者发送消息的时候是否处于运行状态,消费者都可以提取消息。好比我们的发送短信,发送者发送后不见得接收者会即收即看。
  • 消息被消费后队列中不会再存储,所以消费者不会消费到已经被消费掉的消息。

四、Topic模式

4.1、Topic介绍

在发布订阅消息传递域中,目的地被称为主题(topic)

发布/订阅消息传递域的特点如下:

  • 生产者将消息发布到topic中,每个消息可以有多个消费者,属于1:N的关系;
  • 生产者和消费者之间有时间上的相关性。订阅某一个主题的消费者只能消费自它订阅之后发布的消息。
  • 生产者生产时,topic不保存消息它是无状态的不落地,假如无人订阅就去生产,那就是一条废消息,所以,一般先启动消费者再启动生产者

默认情况下如上所述,但是JMS规范允许客户创建持久订阅,这在一定程度上放松了时间上的相关性要求。持久订阅允许消费者消费它在未处于激活状态时发送的消息。一句话,好比我们的微信公众号订阅。

4.2、队列生产者

/**
 * 队列生产者
 */
public class JmsTopicProducer {
    private static final String ACTIVE_URL = "tcp://192.168.0.101:61616";
    private static final String TOPIC_NAME = "topic_1";

    public static void main(String[] args) throws JMSException, IOException {
        //1.创建连接工厂
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVE_URL);

        //2.通过连接工厂 获得连接 并启动
        Connection connection = activeMQConnectionFactory.createConnection();
        connection.start();

        //3.创建会话session
        //两个参数 第一个事务 第二个签收
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);

        //4.创建目的地(具体是队列还是主题)
        Topic topic = session.createTopic(TOPIC_NAME);

        //5.创建消息的生产者
        MessageProducer producer = session.createProducer(topic);

        //6.创建消息对象
        TextMessage textMessage = session.createTextMessage("hello activemq, this is topic message.");

        //7.发送消息
        producer.send(textMessage);
        System.out.println("消息已发送");

        //8.关闭连接
        producer.close();
        session.close();
        connection.close();
    }
}

4.3、队列消费者

/**
 * 队列消费者
 */
public class JmsTopicConsumer {
    private static final String ACTIVE_URL = "tcp://192.168.0.101:61616";
    private static final String TOPIC_NAME = "topic_1";

    public static void main(String[] args) throws JMSException, IOException {
        //1.创建连接工厂
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVE_URL);

        //2.通过连接工厂 获得连接 并启动
        Connection connection = activeMQConnectionFactory.createConnection();
        connection.start();

        //3.创建会话session
        //两个参数 第一个事务 第二个签收
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);

        //4.创建目的地(具体是队列还是主题)
        Topic topic = session.createTopic(TOPIC_NAME);

        //5.创建消息的消费者
        MessageConsumer consumer = session.createConsumer(topic);

        //6.监听方式来消费消息
        consumer.setMessageListener(new MessageListener() {
            @Override
            public void onMessage(Message message) {
                if (message instanceof TextMessage && message != null) {
                    try {
                        TextMessage textMessage = (TextMessage) message;
                        String textMessageText = textMessage.getText();
                        System.out.println("接收消息: " + textMessageText);
                    } catch (JMSException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        //保证控制台不灭 暂时不灭
        System.in.read();
    }
}

4.4、启动多个消费者实例

发现多个消费者都可以收到该主题的消息,启动多个消费者实例的方式:
在这里插入图片描述

4.5、ActiveMQ控制台

topic有多个消费者时,消费消息的数量 ≈ 在线消费者数量*生产消息的数量。
下图展示了:我们先启动了2个消费者,再启动一个生产者,并生产了3条消息。
在这里插入图片描述

4.6、Tpoic和Queue对比

比较项目Queue 模式队列Topic模式队列
工作模式负载均衡模式,如果当前没有消费者,消息也不会丢弃,如果有多个消费者,那么一条消息也只会发送给其中一个消费者, 并且要求消费者ack信息。订阅发布模式,如果当前没有订阅者,消息将会被丢弃。如果有多个订阅者,那么这些订阅者都会收到消息
有无状态默认会在mq服务器上以文件形式保存,比如Active MQ一般保存在$AMQ_ HOME\data\kr-store\data下面,也可以配置成DB存储。无状态
传递完整性消息不会丢弃如果没有订阅者,消息会被丢弃
处理效率由于一条消息只发送给一个消费者,所以就算消费者再多,性能也不会有明显降低。当然不同消息协议的具体性能也是有差异的由于消息要按照订阅者的数量进行复制,所以处理性能会随着订阅者的增加而明显降低,并且还要结合不同消息协议自身的性能差异

五、JMS规范

5.1、JMSMessage介绍

Java消息服务指的是两个应用程序之间进行异步通信的API,它为标准协议和消息服务提供了一组通用接口,包括创建、发送、读取消息等,用于支持Java应用程序开发。在JavaEE中,当两个应用程序使用JMS进行通信时,它们之间不是直接相连的,而是通过一个共同的消息收发服务组件关联起来以达到解耦/异步削峰的效果。

JMSMessage是描述消息的规范,包含以下几个部分:

  • 消息头:描述消息的关键信息
  • 消息体:承载具体的消息内容
  • 消息属性:描述消息的额外信息

5.2、消息头

JMS的消息头有哪些属性:

  • JMSDestination
    在消息中指定消息的目的地。除了之前的同⼀设置会话的目的地外,也可以为某⼀条消息指定目的地。
  • JMSDeliveryMode
    设置消息的持久化模式。注意,这里是消息的持久化模式。在服务器宕机重启后,持久化消息还会被消费,但非持久化消息会丢失。
  • JMSExpiration
    设置消息的超时时间,如果发送后,在消息过期时间之后还没有被发送到目的地,则该消息被清除。默认是消息永不过期。
  • JMSPriority
    设置消息的优先级,优先级越⾼的消息不⼀定会优先达到,但是⾼优先级(5-9)的消息⼀定会早于默认优先级(4)的消息优先达到。
  • JMSMessageID:描述消息的唯⼀id,⽤于标识某⼀条消息。后面介绍如何解决幂等性。

消息的生产者可以set这些属性,消息的消费者可以get这些属性。
这些属性在send方法里面也可以设置。

5.3、消息体

JMS Message提供了五种Message,发送和接收消息体类型必须一致对应。

  • TextMessage:普通字符串消息,包括一个string。
  • MapMessage:一个Map类型的消息,key为string类型,而值为java的基本类型。
  • BytesMessage:二进制数组消息,包含一个byte[]。
  • StreamMessage:Java数据流消息,用标准流操作来顺序的填充和读取。
  • ObjectMessage:对象消息,包含一个可序化的Java对象。

5.4、消息属性

如果需要除消息头字段之外的值,那么可以使用消息属性。他是识别/去重/重点标注等操作,非常有用的方法。
他们是以属性名和属性值对的形式制定的。可以将属性是为消息头得扩展,属性指定一些消息头没有包括的附加信息,比如可以在属性里指定消息选择器。消息的属性就像可以分配给一条消息的附加消息头一样。它们允许开发者添加有关消息的不透明附加信息。它们还用于暴露消息选择器在消息过滤时使用的数据。

生产者设置消息属性

//设置消息属性
textMessage.setStringProperty("author", "xuxiansheng");

消费者获取息属性

String author = textMessage.getStringProperty("author");
System.out.println("author: " + author);

六、消息可靠性的实现

6.1、消息的持久化

持久化消息是保证消息只被传送一次和成功使用一次。在持久性消息传送至目标时,消息服务将其放入持久性数据存储。如果消息服务由于某种原因导致失败,它可以恢复此消息并将此消息传送至相应的消费者。虽然这样增加了消息传送的开销,但却增加了可靠性。

简单理解:在消息生产者将消息成功发送给MQ消息中间件之后。无论是出现任何问题,如:MQ服务器宕机、消费者掉线等。都保证(topic要之前注册过,queue不用)消息消费者,能够成功消费消息。如果消息生产者发送消息就失败了,那么消费者也不会消费到该消息。

6.2、Queue消息非持久和持久

  • queue非持久,当服务器宕机,消息不存在(消息丢失了)。即便是非持久,消费者在不在线的话,消息也不会丢失,等待消费者在线,还是能够收到消息的。
  • queue持久化,当服务器宕机,消息依然存在。queue消息默认是持久化的。

持久化消息,保证这些消息只被传送一次和成功使用一次。对于这些消息,可靠性是优先考虑的因素。

可靠性的另一个重要方面是确保持久性消息传送至目标后,消息服务在向消费者传送它们之前不会丢失这些消息。

持久化的生产者,和之前的代码一样。
运行结果证明:当生产者成功发布消息之后,MQ服务端宕机重启,消息生产者仍然能够收到该消息。

//持久化
producer.setDeliveryMode(DeliveryMode.PERSISTENT);

非持久化的生产者,和之前的代码不一样。
运行结果证明:当生产者成功发布消息之后,MQ服务端宕机重启,消息生产者就收不到该消息了。

//非持久化
producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);

6.3、Topic的发布订阅模式

ActiveMQ是重topic的MQ,对于topic模式更加常⽤,⽽发布订阅模式是针对于topic使⽤的⽅式。

发布订阅topic消费者代码

/**
 * 消费者的消息订阅
 */
public class JmsTopicConsumerSubscribe {
    private static final String ACTIVE_URL = "tcp://192.168.0.101:61616";
    private static final String TOPIC_NAME = "topic_2";

    public static void main(String[] args) throws JMSException, IOException {
        //1.创建连接工厂
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVE_URL);

        //2.通过连接工厂 获得连接
        Connection connection = activeMQConnectionFactory.createConnection();

        //设置客户端id
        connection.setClientID("1234");

        //3.创建会话session
        //两个参数 第一个事务 第二个签收
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);

        //4.创建目的地(具体是队列还是主题)
        Topic topic = session.createTopic(TOPIC_NAME);

        //5.使用消息订阅方式
        TopicSubscriber s1 = session.createDurableSubscriber(topic, "s1");

        //6.启动连接
        connection.start();
        while (true) {
            Message message = s1.receive();
            if (Objects.nonNull(message) && message instanceof TextMessage) {
                TextMessage textMessage = (TextMessage) message;
                System.out.println(textMessage.getText());

            }
        }
    }
}

6.4、Topic的持久化

消费者通过订阅主题的⽅式启动后,即会作为该主题的订阅者,记录在broker中。

消费者下线后再次上线,依然能收到该主题中的未消费的消息。在消费者下线和上线的过程中,如果服务器出现宕机,则持久化与非持久化的区别将会明显的出现。

  • 非持久化:消费者再次上线后不会收到下线之后,及上线之前的消息。(默认的)
//设置非持久化(默认)
producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
  • 持久化:消费者再次上线后可以收到下线之后,及上线之前的消息。
//设置持久化
producer.setDeliveryMode(DeliveryMode.PERSISTENT);

注意:
1.一定要先运行一次消费者,等于向MQ注册,类似我订阅了这个主题。
2.然后再运行生产者发送消息。
3.之后无论消费者是否在线,都会收到消息。如果不在线的话,下次连接的时候,会把没有收过的消息都接收过来。

控制台介绍:
topic页面还是和之前的一样。另外在subscribers页面也会显示。如下:
在这里插入图片描述

6.5、消息的事务性

生产者开启事务后,执行commit方法,这批消息才真正的被提交。不执行commit方法,这批消息不会提交。执行rollback方法,之前的消息会回滚掉。生产者的事务机制,要高于签收机制,当生产者开启事务,签收机制不再重要。

消费者开启事务后,执行commit方法,这批消息才算真正的被消费。不执行commit方法,这些消息不会标记已消费,下次还会被消费。执行rollback方法,是不能回滚之前执行过的业务逻辑,但是能够回滚之前的消息,回滚后的消息,下次还会被消费。消费者利用commit和rollback方法,甚至能够违反一个消费者只能消费一次消息的原理。

开启事务

//两个参数 第一个事务 第二个签收
Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);

生产者提交事务和回退事务

//8.发送消息
try {
    for (int i = 0; i < 10; i++) {
        //9.创建消息对象
        TextMessage message = session.createTextMessage("hello activemq, this is topic message " + i);
        //8.发送
        producer.send(message);
    }
} catch (JMSException e) {
    e.printStackTrace();
    //回退事务
    session.rollback();
}
//提交事务
session.commit();

消费者提交事务和回退事务

while (true) {
    try {
        Message message = s1.receive();
        if (Objects.nonNull(message) && message instanceof TextMessage) {
            TextMessage textMessage = (TextMessage) message;
            System.out.println(textMessage.getText());
        }
    } catch (JMSException e) {
        e.printStackTrace();
        session.rollback();
    }
    session.commit();
}

消费者和生产者的事务,完全没有关联,各自是各自的事务。
事务偏生产者,签收偏消费者。

6.6、消息的签收机制

签收的几种方式

  • 自动签收(Session.AUTO_ACKNOWLEDGE)
    该方式是默认的。该种方式,无需我们程序做任何操作,框架会帮我们自动签收收到的消息。
  • 手动签收(Session.CLIENT_ACKNOWLEDGE)
    该种方式,需要我们手动调用Message.acknowledge(),来签收消息。如果不签收消息,该消息会被我们反复消费,只到被签收。
  • 允许重复消息(Session.DUPS_OK_ACKNOWLEDGE)
    多线程或多个消费者同时消费到一个消息,因为线程不安全,可能会重复消费。该种方式很少使用到。
  • 事务下的签收(Session.SESSION_TRANSACTED)
    开始事务的情况下,可以使用该方式。该种方式很少使用到。

事务和签收的关系

  • 在事务性会话中,当一个事务被成功提交则消息被自动签收。如果事务回滚,则消息会被再次传送。事务优先于签收,开始事务后,签收机制不再起任何作用。
  • 非事务性会话中,消息何时被确认取决于创建会话时的应答模式。
  • 生产者事务开启,只有commit后才能将全部消息变为已消费。
  • 事务偏向生产者,签收偏向消费者。也就是说,生产者使用事务更好点,消费者使用签收机制更好点。

消费者开启手动签收

//两个参数 第一个事务 第二个签收
Session session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE); 

消费者ack确诊

while (true) {
    Message message = s1.receive();
    if (Objects.nonNull(message) && message instanceof TextMessage) {
        TextMessage textMessage = (TextMessage) message;
        System.out.println(textMessage.getText());
    }
    message.acknowledge();
}

在事务性会话中,当一个事务被成功提交则消息被自动签收。
如果事务回滚,则消息会被再次传送。
非事务性会话中,消息何时被确认取决于创建会话时的应答模式(acknowledgementmode)
commit() 大于 acknowledge()

6.7、Queue和Topic总结

Queue总结

点对点模型是基于队列的,生产者发消息到队列,消费者从队列接收消息,队列的存在使得消息的异步传输成为可能。和我们平时给朋友发送短信类似。

如果在Session关闭时有部分消息己被收到但还没有被签收(acknowledged),那当消费者下次连接到相同的队列时,这些消息还会被再次接收,队列可以长久地保存消息直到消费者收到消息。消费者不需要因为担心消息会丢失而时刻和队列保持激活的连接状态,充分体现了异步传输模式的优势。

Topic的发布订阅总结

  • JMS的发布订阅总结
    JMS Pub/Sub 模型定义了如何向一个内容节点发布和订阅消息,这些节点被称作topic。
    主题可以被认为是消息的传输中介,发布者(publisher)发布消息到主题,订阅者(subscribe)从主题订阅消息。
    主题使得消息订阅者和消息发布者保持互相独立不需要解除即可保证消息的传送
  • 非持久订阅
    非持久订阅只有当客户端处于激活状态,也就是和MQ保持连接状态才能收发到某个主题的消息。
    如果消费者处于离线状态,生产者发送的主题消息将会丢失作废,消费者永远不会收到。
    一句话:先订阅注册才能接受到发布,只给订阅者发布消息。
  • 持久订阅
    客户端首先向MQ注册一个自己的身份ID识别号,当这个客户端处于离线时,生产者会为这个ID保存所有发送到主题的消息,当客户再次连接到MQ的时候,会根据消费者的ID得到所有当自己处于离线时发送到主题的消息
    当持久订阅状态下,不能恢复或重新派送一个未签收的消息。
    持久订阅才能恢复或重新派送一个未签收的消息。
  • 非持久和持久化订阅如何选择
    当所有的消息必须被接收,则用持久化订阅。当消息丢失能够被容忍,则用非持久订阅。

七、Spring整合ActiveMQ

SpringBoot整合ActiveMQ很重要,Spring提供了一个模板,简化了代码,减少我们工作中遇到坑,能够满足开发中90%以上的功能。

7.1、添加依赖

<!-- activemq核心依赖包  -->
<dependency>
   <groupId>org.apache.activemq</groupId>
   <artifactId>activemq-all</artifactId>
   <version>5.16.3</version>
</dependency>

<!--  嵌入式activemq的broker所需要的依赖包   -->
<dependency>
   <groupId>com.fasterxml.jackson.core</groupId>
   <artifactId>jackson-databind</artifactId>
   <version>2.10.1</version>
</dependency>
<!-- activemq连接池 -->
<dependency>
   <groupId>org.apache.activemq</groupId>
   <artifactId>activemq-pool</artifactId>
   <version>5.15.10</version>
</dependency>
<!-- spring支持jms的包 -->
<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-jms</artifactId>
   <version>5.3.9</version>
</dependency>

<!--spring相关依赖包-->
<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-context</artifactId>
   <version>5.3.9</version>
</dependency>
<dependency>
   <groupId>org.apache.activemq</groupId>
   <artifactId>activemq-all</artifactId>
   <version>5.15.9</version>
   <scope>compile</scope>
</dependency>

7.2、配置文件spring-context.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    <!--  开启包的自动扫描  -->
    <context:component-scan base-package="com.java521.demo"/>
    <!--  配置生产者  -->
    <bean id="connectionFactory" class="org.apache.activemq.pool.PooledConnectionFactory" destroy-method="stop">
        <property name="connectionFactory">
            <!--      正真可以生产Connection的ConnectionFactory,由对应的JMS服务商提供      -->
            <bean class="org.apache.activemq.spring.ActiveMQConnectionFactory">
                <property name="brokerURL" value="tcp://192.168.0.101:61616"/>
            </bean>
        </property>
        <property name="maxConnections" value="100"/>
    </bean>

    <!--  这个是队列目的地,点对点的Queue  -->
    <bean id="destinationQueue" class="org.apache.activemq.command.ActiveMQQueue">
        <!--    通过构造注入Queue名    -->
        <constructor-arg index="0" value="spring-active-queue"/>
    </bean>

    <!--  这个是队列目的地,  发布订阅的主题Topic-->
    <bean id="destinationTopic" class="org.apache.activemq.command.ActiveMQTopic">
        <constructor-arg index="0" value="spring-active-topic"/>
    </bean>

    <!--  Spring提供的JMS工具类,他可以进行消息发送,接收等  -->
    <bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
        <!--    传入连接工厂    -->
        <property name="connectionFactory" ref="connectionFactory"/>
        <!--    传入目的地    -->
        <property name="defaultDestination" ref="destinationQueue"/>
        <!--    消息自动转换器    -->
        <property name="messageConverter">
            <bean class="org.springframework.jms.support.converter.SimpleMessageConverter"/>
        </property>
    </bean>

</beans>

7.3、Queue

生产者

@Component
public class SpringProducer {

    @Autowired
    private JmsTemplate jmsTemplate;

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-context.xml");
        SpringProducer producer = context.getBean(SpringProducer.class);
        producer.jmsTemplate.send(new MessageCreator() {
            @Override
            public Message createMessage(Session session) throws JMSException {
                TextMessage textMessage = session.createTextMessage("spring 和 activemq 的整合");
                return textMessage;
            }
        });
        System.out.println(" *** send task over ***");
    }
}

消费者

@Component
public class SpringConsumer {
    @Autowired
    private JmsTemplate jmsTemplate;

    public static void main(String[] args) throws JMSException {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-context.xml");
        SpringConsumer consumer = context.getBean(SpringConsumer.class);
        TextMessage textMessage = (TextMessage) consumer.jmsTemplate.receive();
        String messageText = textMessage.getText();
        System.out.println("messageText = " + messageText);
    }
}

7.4、Topic

生产者和消费者不变,修改xml的目的地同,改为主题模式

<!--  Spring提供的JMS工具类,他可以进行消息发送,接收等  -->
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
    <!--    传入连接工厂    -->
    <property name="connectionFactory" ref="connectionFactory"/>
    <!--    传入目的地    -->
    <property name="defaultDestination" ref="destinationTopic"/>
    <!--    消息自动转换器    -->
    <property name="messageConverter">
        <bean class="org.springframework.jms.support.converter.SimpleMessageConverter"/>
    </property>
</bean>

7.5、配置消费者的监听类

xml添加

<bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
    <property name="connectionFactory" ref="connectionFactory"/>
    <property name="destination" ref="destinationTopic"/>
    <!--实现 MyMessageListener-->
    <property name="messageListener" ref="springProdderListener"/>
</bean>

监听类

@Component
public class SpringProdderListener implements MessageListener {
    @Override
    public void onMessage(Message message) {
        if (null!= message && message instanceof TextMessage){
            TextMessage textMessage = (TextMessage) message;
            String messageText = null;
            try {
                messageText = textMessage.getText();
            } catch (JMSException e) {
                e.printStackTrace();
            }
            System.out.println("messageText = " + messageText);
        }
    }
}

八、SpringBoot整合ActiveMQ

8.1、依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-activemq</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot
        </groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

8.2、yml配置

server:
  port: 9001 #端口
spring:
  application:
    name: activemq-springboot # 服务名称
  # springboot与activemq整合配置
  activemq:
    broker-url: tcp://192.168.0.101:61616 # 连接地址
    user: admin # activemq用户名
    password: admin # activemq密码
  # 指定发送模式 (点对点 false , 发布订阅 true)
  jms:
    # false=queue true=topic
    pub-sub-domain: false
queue_name: springboot_queue
topic_name: springboot_topic

8.3、配置类

@Component
//@EnableJms 可加可不加
public class JMSConfig {

    @Value("${queue_name}")
    private String queueName;

    @Value("${topic_name}")
    private String topicName;

    @Bean
    public ActiveMQQueue queue() {
        return new ActiveMQQueue(queueName);
    }

    @Bean
    public ActiveMQTopic topic() {
        return new ActiveMQTopic(topicName);
    }
}

8.4、Queue

生产者

@Component
public class SpingBootProducerQueue {
    @Autowired
    private JmsMessagingTemplate jmsMessagingTemplate;

    @Autowired
    private Queue queue;

    public void sendQueueMessage(String message) {
        jmsMessagingTemplate.convertAndSend(queue, message);
    }
}

消费者

@Component
public class SpingBootConsumerQueue {

    @JmsListener(destination = "${queue_name}")
    public void getPtpMessage(Message message) {
        if (message instanceof TextMessage) {
            try {
                TextMessage textMessage = (TextMessage) message;
                String messageText = textMessage.getText();
                System.out.println("消费者接收queue消息: " + messageText);
            } catch (JMSException e) {
                e.printStackTrace();
            }
        }
    }
}

测试

@SpringBootTest
class SpringbootActivemqApplicationTests {

    @Autowired
    private SpingBootProducerQueue spingBootProducerQueue;

    @Test
    public void testSendQueueMessage() {
        for (int i = 1; i <= 5; i++) {
            System.out.println("生产者生产第 " + i + " 条消息......");
            spingBootProducerQueue.sendQueueMessage("SpringBoot与ActiveMQ Queue整合..." + i);
        }
    }
}

8.5、Topic

生产者

@Component
public class SpingBootProducerTopic {

    @Autowired
    private JmsMessagingTemplate jmsMessagingTemplate;

    @Autowired
    private Topic topic;

    public void sendTopicMessage(String message) {
        jmsMessagingTemplate.convertAndSend(topic, message);
    }
}

消费者

@Component
public class SpingBootConsumerTopic {
    @JmsListener(destination = "${topic_name}")
    public void getPtpMessage(Message message) {
        if (message instanceof TextMessage) {
            try {
                TextMessage textMessage = (TextMessage) message;
                String messageText = textMessage.getText();
                System.out.println("消费者接收topic消息: " + messageText);
            } catch (JMSException e) {
                e.printStackTrace();
            }
        }
    }
}

测试

@SpringBootTest
class SpringbootActivemqApplicationTests {

    @Autowired
    private SpingBootProducerQueue spingBootProducerQueue;

    @Autowired
    private SpingBootProducerTopic spingBootProducerTopic;

    //Queue测试
    @Test
    public void testSendQueueMessage() {
        for (int i = 1; i <= 5; i++) {
            System.out.println("生产者生产第 " + i + " 条消息......");
            spingBootProducerQueue.sendQueueMessage("SpringBoot与ActiveMQ Queue整合..." + i);
        }
    }

    //Topic测试
    @Test
    public void testSendTopicMessage() {
        for (int i = 1; i <= 5; i++) {
            System.out.println("生产者生产第 " + i + " 条消息......");
            spingBootProducerTopic.sendTopicMessage("SpringBoot与ActiveMQ Topic整合..." + i);
        }
    }
}

这个时候测试发现发送topic消息正常,但是没有监听到topic消息,经过排查发现忘记修改pub-sub-domain属性的值。

server:
  port: 9001 #端口
spring:
  application:
    name: activemq-springboot # 服务名称
  # springboot与activemq整合配置
  activemq:
    broker-url: tcp://192.168.0.101:61616 # 连接地址
    user: admin # activemq用户名
    password: admin # activemq密码
  # 指定发送模式 (点对点 false , 发布订阅 true)
  jms:
    # false=queue true=topic
    pub-sub-domain: true
queue_name: springboot_queue
topic_name: springboot_topic

九、ActiveMQ的传输协议

9.1、简介

ActiveMQ支持的client-broker通讯协议有:TVP、NIO、UDP、SSL、Http(s)、VM。

其中配置Transport Connector的文件在ActiveMQ安装目录的conf/activemq.xml中的<transportConnectors>标签之内。

ActiveMQ传输协议的官方文档:http://activemq.apache.org/configuring-version-5-transports.html

<transportConnectors>
  <!-- DOS protection, limit concurrent connections to 1000 and frame size to 100MB -->
  <transportConnector name="openwire" uri="tcp://0.0.0.0:61616?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
  <transportConnector name="amqp" uri="amqp://0.0.0.0:5672?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
  <transportConnector name="stomp" uri="stomp://0.0.0.0:61613?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
  <transportConnector name="mqtt" uri="mqtt://0.0.0.0:1883?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
  <transportConnector name="ws" uri="ws://0.0.0.0:61614?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
</transportConnectors>

在配置信息中,URI描述信息的头部都是采用协议名称

  • 描述amqp协议的监听端口时,采用的URI描述格式为“amqp://······”。
  • 描述Stomp协议的监听端口时,采用URI描述格式为“stomp://······”。
  • 唯独在进行openwire协议描述时,URI头却采用的“tcp://······”,这是因为ActiveMQ中默认的消息协议就是openwire。

9.2、支持的传输协议

除了tcp和nio协议,其他的了解就行。

各种协议有各自擅长该协议的中间件,工作中一般不会使用activemq去实现这些协议。如: mqtt是物联网专用协议,采用的中间件一般是mosquito。ws是websocket的协议,是和前端对接常用的,一般在java代码中内嵌一个基站(中间件)。stomp好像是邮箱使用的协议的,各大邮箱公司都有基站(中间件)。

注意:协议不同,我们的代码都会不同。

协议描述
TCP默认的协议,性能相对可以
NIO基于TCP协议之上的
UDP性能比TCP更好,但是不具有可靠性
SSL安全链接
TCP默认的协议
HTTP(S)基于HTTP或者HTTPS
VMVM本身不是协议,当客户端和代理在同一个Java虚拟机(VM)中运行时,他们之间需要通信,但不想占用网络通道,而是直接通道,可以使用该方式

9.3、TCP协议

1、这是默认的Broker配置,TCP的Client监听端口61616。

2、在网络传输数据前,必须要先序列化数据,消息是通过一个叫wire protocol的来序列化成字节流。

3、TCP连接的URI形式如:tcp://HostName:port?key=value&key=value,后面的参数是可选的。

4、TCP传输的的优点:

  • TCP协议传输可靠性高,稳定性强
  • 高效率:字节流方式传递,效率很高
  • 有效性、可用性:应用广泛,支持任何平台

5、关于Transport协议的可选配置参数可以参考官网http://activemq.apache.org/configuring-version-5-transports.html

9.4、NIO协议

1、New I/O API Protocol(NIO)
2、NIO协议和TCP协议类似,但NIO更侧重于底层的访问操作。它允许开发人员对同一资源可有更多的client调用和服务器端有更多的负载。
3、适合使用NIO协议的场景:
可能有大量的Client去连接到Broker上,一般情况下,大量的Client去连接Broker是被操作系统的线程所限制的。因此,NIO的实现比TCP需要更少的线程去运行,所以建议使用NIO协议。
可能对于Broker有一个很迟钝的网络传输,NIO比TCP提供更好的性能。
4、NIO连接的URI形式:nio://hostname:port?key=value&key=value
5、关于Transport协议的可选配置参数可以参考官网http://activemq.apache.org/configuring-version-5-transports.html

<broker>
  ...
  <transportConnectors>
    <transportConnector name="nio" uri="nio://0.0.0.0:61616"/>  
  </<transportConnectors>
  ...
</broker>

9.5、AMQP协议

Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同开发语言等条件限制。

9.6、STOMP协议

STOP,Streaming Text Orientation Message Protocol,是流文本定向消息协议,是一种为MOM(Message Oriented Middleware,面向消息中间件)设计的简单文本协议。

9.7、MQTT协议

MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)是IBM开发的一个即时通讯协议,有可能成为物联网的重要组成部分。该协议支持所有平台,几乎可以把所有联网物品和外部连接起来,被用来当作传感器和致动器(比如通过Twitter让房屋联网)的通信协议。

9.8、使用NIO协议

ActiveMQ这些协议传输的底层默认都是使用BIO网络的IO模型。只有当我们指定使用nio才使用NIO的IO模型。

修改配置文件activemq.xml

如果你不特别指定ActiveMQ的网络监听端口,那么这些端口都讲使用BIO网络IO模型,所以为了首先提高单节点的网络吞吐性能,我们需要明确指定ActiveMQ网络IO模型。

如下所示:URI格式头以“nio”开头,表示这个端口使用以TCP协议为基础的NIO网络IO模型。

修改配置文件activemq.xml在 节点下添加如下内容:

<transportConnector name="nio" uri="nio://0.0.0.0:61618?trace=true" />

修改后

<transportConnectors>
    <!-- DOS protection, limit concurrent connections to 1000 and frame size to 100MB -->
    <transportConnector name="openwire" uri="tcp://0.0.0.0:61616?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
    <transportConnector name="amqp" uri="amqp://0.0.0.0:5672?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
    <transportConnector name="stomp" uri="stomp://0.0.0.0:61613?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
    <transportConnector name="mqtt" uri="mqtt://0.0.0.0:1883?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
    <transportConnector name="ws" uri="ws://0.0.0.0:61614?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
    <!--添加nio-->
    <transportConnector name="nio" uri="nio://0.0.0.0:61618?trace=true" />
</transportConnectors>

修改完成后重启activemq:

./activemq restart

查看管理后台,可以看到页面多了nio
在这里插入图片描述
修改配置文件

spring:
  activemq:
    broker-url: nio://192.168.0.101:61616 # 修改nio
    user: admin # activemq用户名
    password: admin # activemq密码

9.9、使用auto+协议

目的
上面是Openwire协议传输底层使用NIO网络IO模型。 如何让其他协议传输底层也使用NIO网络IO模型呢?

如何进一步优化?

  • URI格式以"nio"开头,代表这个端口使用TCP协议为基础的NIO网络模型。
  • 但是这样的设置方式,只能使这个端口支持Openwire协议。

我们怎么能够让这个端口既支持NIO网络模型,又让他支持多个协议呢?

  • 使用auto关键字
  • 使用"+"符号来为端口设置多种特性
  • 如果我们既需要使用某一个端口支持NIO网络模型,又需要它支持多个协议

修改配置文件activemq.xml

<transportConnector name="auto+nio" uri="auto+nio://0.0.0.0:61620"/>

完整配置

<transportConnectors>
  <!-- DOS protection, limit concurrent connections to 1000 and frame size to 100MB -->
  <transportConnector name="openwire" uri="tcp://0.0.0.0:61616?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
  <transportConnector name="amqp" uri="amqp://0.0.0.0:5672?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
  <transportConnector name="stomp" uri="stomp://0.0.0.0:61613?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
  <transportConnector name="mqtt" uri="mqtt://0.0.0.0:1883?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
  <transportConnector name="ws" uri="ws://0.0.0.0:61614?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
  <!-- <transportConnector name="nio" uri="nio://0.0.0.0:61618?trace=true" />-->
  <transportConnector name="auto+nio" uri="auto+nio://0.0.0.0:61620"/>
</transportConnectors>

auto:针对所有的协议,他会识别我们是什么协议。
nio:使用NIO网络IO模型

修改配置文件后重启activemq

./activemq restart

修改application.yml

spring:
  activemq:
    broker-url: tcp://192.168.0.101:61620 #连接地址

或者

spring:
  activemq:
    broker-url: nio://192.168.0.101:61620 #连接地址

十、ActiveMQ的消息存储和持久化

10.1、介绍

此处持久化和之前的持久化的区别
请添加图片描述
MQ高可用:事务、可持久、签收,是属于MQ自身特性,自带的。这里的持久化是外力,是外部插件。之前讲的持久化是MQ的外在表现,现在讲的的持久是是底层实现。

是什么

官网文档:http://activemq.apache.org/persistence]

持久化是什么?一句话就是:ActiveMQ宕机了,消息不会丢失的机制。

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

10.2、有哪些

  • AMQ Message Store(了解)
    基于文件的存储机制,是以前的默认机制,现在不再使用。
    AMQ是一种文件存储形式,它具有写入速度快和容易恢复的特点。消息存储再一个个文件中文件的默认大小为32M,当一个文件中的消息已经全部被消费,那么这个文件将被标识为可删除,在下一个清除阶段,这个文件被删除。AMQ适用于ActiveMQ5.3之前的版本。

  • kahaDB(默认)

  • JDBC消息存储

  • LevelDB消息存储(了解)
    过于新兴的技术,现在有些不确定。
    这种文件系统是从ActiveMQ5.8之后引进的,它和KahaDB非常相似,也是基于文件的本地数据库储存形式,但是它提供比KahaDB更快的持久性。
    但它不使用自定义B-Tree实现来索引预写日志,而是使用基于LevelDB的索引
    默认配置如下:

<persistenceAdapter >
	<levelDBdirectory="activemq-data"/>
</persistenceAdapter >
  • JDBC Message Store with ActiveMQ Journal

10.3、kahaDB消息存储

1、介绍
基于日志文件,从ActiveMQ5.4(含)开始默认的持久化插件。
官网文档:http://activemq.aache.org/kahadb
官网上还有一些其他配置参数。
配置文件activemq.xml中,如下

<persistenceAdapter>
      <kahaDB directory="${activemq.data}/kahadb"/>
</persistenceAdapter>

日志文件的存储目录在:%activemq安装目录%/data/kahadb

2、说明
KahaDB是目前默认的存储方式,可用于任何场景,提高了性能和恢复能力。
消息存储使用一个事务日志和仅仅用一个索引文件来存储它所有的地址。
KahaDB是一个专门针对消息持久化的解决方案,它对典型的消息使用模型进行了优化。
数据被追加到data logs中。当不再需要log文件中的数据的时候,log文件会被丢弃。
在这里插入图片描述
3、KahaDB的存储原理
KahaDB在消息保存的目录中有4类文件和一个lock,跟ActiveMQ的其他几种文件存储引擎相比,这就非常简洁了。

  • db-number.log
    KahaDB存储消息到预定大小的数据纪录文件中,文件名为db-number.log。当数据文件已满时,一个新的文件会随之创建,number数值也会随之递增,它随着消息数量的增多,如没32M一个文件,文件名按照数字进行编号,如db-1.log,db-2.log······。当不再有引用到数据文件中的任何消息时,文件会被删除或者归档。
    在这里插入图片描述

  • db.data
    该文件包含了持久化的BTree索引,索引了消息数据记录中的消息,它是消息的索引文件,本质上是B-Tree(B树),使用B-Tree作为索引指向db-number。log里面存储消息。

  • db.free
    当问当前db.data文件里面哪些页面是空闲的,文件具体内容是所有空闲页的ID

  • db.redo
    用来进行消息恢复,如果KahaDB消息存储再强制退出后启动,用于恢复BTree索引。

  • lock
    文件锁,表示当前kahadb独写权限的broker。

10.4、JDBC消息存储

10.4.1、设置步骤

1、原理图
请添加图片描述2、添加mysql数据库的驱动包到lib文件夹
在这里插入图片描述
3、jdbcPersistenceAdapter配置

修改前的KahaDB修改后的jdbcPersisteceAdapter
<persistenceAdapter>
<kahaDB directory=“${activemq.data}/kahadb”/>
</persistenceAdapter>
<persistenceAdapter>
<jdbcPersistenceAdapter dataSource=“#mysql-ds” />
</persistenceAdapter>
dataSource是指定将要引用的持久化数据库的bean名称。
createTableOnStartup是否在启动的时候创建数据库表,默认是true,这样每次启动都会去创建表了,一般是第一次启动的时候设置为true,然后再去改成false。

修改配置文件activemq.xml。将之前的替换为jdbc的配置。

<!--  
<persistenceAdapter>
  	  <kahaDB directory="${activemq.data}/kahadb"/>
</persistenceAdapter>

<persistenceAdapter>  
      <jdbcPersistenceAdapter dataSource="#mysql-ds"/> 
</persistenceAdapter>

<!--  这个是可以自动生成表结构的-->
<!--  
<persistenceAdapter>  
      <jdbcPersistenceAdapter dataSource="#mysql-ds" createTableOnStartup="true"/> 
</persistenceAdapter>
-->

4、数据库连接池配置

需要我们准备一个mysql数据库,并创建一个名为activemq的数据库。

在标签和标签之间插入数据库连接池配置

</broker>

<bean id="mysql-ds" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://mysql数据库URL:3306/activemq?relaxAutoCommit=true"/>
    <property name="username" value="mysql数据库用户名"/>
    <property name="password" value="mysql数据库密码"/>
    <property name="poolPreparedStatements" value="true"/>
</bean>

<bean id="mysql-ds" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://47.93.36.15:3306/activemq?relaxAutoCommit=true"/>
    <property name="username" value="root"/>
    <property name="password" value="root"/>
    <property name="poolPreparedStatements" value="true"/>
</bean>

<import resource="jetty.xml"/>

之后需要建一个数据库,名为activemq。新建的数据库要采用latin1 或者ASCII编码。https://blog.csdn.net/JeremyJiaming/article/details/88734762

默认是的dbcp数据库连接池,如果要换成其他数据库连接池,需要将该连接池jar包,也放到lib目录下。

5、建库SQL和创表说明
重启activemq。会自动生成如下3张表。如果没有自动生成,需要我们手动执行SQL。我个人建议要自动生成,我在操作过程中查看日志文件,发现了不少问题,最终解决了这些问题后,是能够自动生成的。如果不能自动生成说明你的操作有问题。如果实在不行,下面是手动建表的SQL

建表SQL

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for activemq_acks
-- ----------------------------
DROP TABLE IF EXISTS `activemq_acks`;
CREATE TABLE `activemq_acks`  (
  `CONTAINER` varchar(250) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL,
  `SUB_DEST` varchar(250) CHARACTER SET latin1 COLLATE latin1_swedish_ci NULL DEFAULT NULL,
  `CLIENT_ID` varchar(250) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL,
  `SUB_NAME` varchar(250) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL,
  `SELECTOR` varchar(250) CHARACTER SET latin1 COLLATE latin1_swedish_ci NULL DEFAULT NULL,
  `LAST_ACKED_ID` bigint(20) NULL DEFAULT NULL,
  `PRIORITY` bigint(20) NOT NULL DEFAULT 5,
  `XID` varchar(250) CHARACTER SET latin1 COLLATE latin1_swedish_ci NULL DEFAULT NULL,
  PRIMARY KEY (`CONTAINER`, `CLIENT_ID`, `SUB_NAME`, `PRIORITY`) USING BTREE,
  INDEX `ACTIVEMQ_ACKS_XIDX`(`XID`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = latin1 COLLATE = latin1_swedish_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of activemq_acks
-- ----------------------------

-- ----------------------------
-- Table structure for activemq_lock
-- ----------------------------
DROP TABLE IF EXISTS `activemq_lock`;
CREATE TABLE `activemq_lock`  (
  `ID` bigint(20) NOT NULL,
  `TIME` bigint(20) NULL DEFAULT NULL,
  `BROKER_NAME` varchar(250) CHARACTER SET latin1 COLLATE latin1_swedish_ci NULL DEFAULT NULL,
  PRIMARY KEY (`ID`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = latin1 COLLATE = latin1_swedish_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of activemq_lock
-- ----------------------------
INSERT INTO `activemq_lock` VALUES (1, NULL, NULL);

-- ----------------------------
-- Table structure for activemq_msgs
-- ----------------------------
DROP TABLE IF EXISTS `activemq_msgs`;
CREATE TABLE `activemq_msgs`  (
  `ID` bigint(20) NOT NULL,
  `CONTAINER` varchar(250) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL,
  `MSGID_PROD` varchar(250) CHARACTER SET latin1 COLLATE latin1_swedish_ci NULL DEFAULT NULL,
  `MSGID_SEQ` bigint(20) NULL DEFAULT NULL,
  `EXPIRATION` bigint(20) NULL DEFAULT NULL,
  `MSG` blob NULL,
  `PRIORITY` bigint(20) NULL DEFAULT NULL,
  `XID` varchar(250) CHARACTER SET latin1 COLLATE latin1_swedish_ci NULL DEFAULT NULL,
  PRIMARY KEY (`ID`) USING BTREE,
  INDEX `ACTIVEMQ_MSGS_MIDX`(`MSGID_PROD`, `MSGID_SEQ`) USING BTREE,
  INDEX `ACTIVEMQ_MSGS_CIDX`(`CONTAINER`) USING BTREE,
  INDEX `ACTIVEMQ_MSGS_EIDX`(`EXPIRATION`) USING BTREE,
  INDEX `ACTIVEMQ_MSGS_PIDX`(`PRIORITY`) USING BTREE,
  INDEX `ACTIVEMQ_MSGS_XIDX`(`XID`) USING BTREE,
  INDEX `ACTIVEMQ_MSGS_IIDX`(`ID`, `XID`, `CONTAINER`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = latin1 COLLATE = latin1_swedish_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of activemq_msgs
-- ----------------------------

SET FOREIGN_KEY_CHECKS = 1;

ACTIVEMQ_MSGS: 消息表,缺省表名ACTIVEMQ_MSGS,Queue和Topic都存在里面

ID:自增的数据库主键
CONTAINER:消息的Destination
MSGID_PROD:消息发送者的主键
MSG_SEQ:是发送消息的顺序,MSGID_PROD+MSG_SEQ可以组成JMS的MessageID
EXPIRATION:消息的过期时间,存储的是从1970-01-01到现在的毫秒数
MSG:消息本体的Java序列化对象的二进制数据
PRIORITY:优先级,从0-9,数值越大优先级越高

ACTIVEMQ_ACKS: 用于存储订阅关系。如果是持久化Topic,订阅者和服务器的订阅关系在这个表保存

ACTIVEMQ_LOCK:
表ACTIVEMQ_LOCK在集群环境下才有用,只有一个Broker可以获取消息,称为Master Broker,其他的只能作为备份等待Master Broker不可用,才可能成为下一个Master Broker。这个表用于记录哪个Broker是当前的Master Broker

10.4.2、queue验证和数据表变化

生产者和消费者代码

private static final String ACTIVE_URL = "tcp://47.93.36.15:61616";
private static final String QUEUE_NAME = "jdbc01";

messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);

在点对点类型中
当DeliveryMode设置为NON_PERSISTENCE时,消息被保存在内存中
当DeliveryMode设置为PERSISTENCE时,消息保存在broker的相应的文件或者数据库中。

而且点对点类型中消息一旦被Consumer消费,就从数据中删除

queue模式,非持久化不会将消息持久化到数据库。
queue模式,持久化会将消息持久化数据库。

我们使用queue模式持久化,发布3条消息后,发现ACTIVEMQ_MSGS数据表多了3条数据。
在这里插入图片描述
启动消费者,消费了所有的消息后,发现数据表的数据消失了
在这里插入图片描述
非持久化

messageProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);

mysql中不会有数据

10.4.3、topic验证和说明

生产者代码

public class Producer {
    private static final String ACTIVEMQ_URL = "tcp://47.93.36.15:61616";
    private static final String ACTIVEMQ_TOPIC_NAME = "topic-jdbc";

    public static void main(String[] args) throws JMSException {
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory();
        activeMQConnectionFactory.setBrokerURL(ACTIVEMQ_URL);
        Connection connection = activeMQConnectionFactory.createConnection();
        connection.setClientID("我是生产者张三");
        Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
        Topic topic = session.createTopic(ACTIVEMQ_TOPIC_NAME);
        MessageProducer messageProducer = session.createProducer(topic);
        messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);
        connection.start();
        for (int i = 0; i < 3; i++) {
            TextMessage textMessage = session.createTextMessage("Topic-JdbcPersistence测试消息" + i);
            messageProducer.send(textMessage);
        }
        session.commit();
        System.out.println("主题发送到MQ完成");
        messageProducer.close();
        session.close();
        connection.close();
    }
}

消费者代码

public class Consumer1 {
    private static final String ACTIVEMQ_URL = "tcp://47.93.36.15:61616";
    public static final String TOPIC_NAME = "topic-jdbc";

    public static void main(String[] args) throws Exception{
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
        Connection connection = activeMQConnectionFactory.createConnection();
        connection.setClientID("marrry");
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        Topic topic = session.createTopic(TOPIC_NAME);
        TopicSubscriber topicSubscriber = session.createDurableSubscriber(topic,"remark...");
        connection.start();
        Message message = topicSubscriber.receive();
        while (null != message){
            TextMessage textMessage = (TextMessage)message;
            System.out.println(" 收到的持久化 topic :"+textMessage.getText());
            message = topicSubscriber.receive();
        }
        session.close();
        connection.close();
    }
}

我们启动持久化生产者发布3个数据,ACTIVEMQ_MSGS数据表新增3条数据,消费者消费所有的数据后,ACTIVEMQ_MSGS数据表的数据并没有消失。持久化topic的消息不管是否被消费,是否有消费者,产生的数据永远都存在,且只存储一条。这个是要注意的,持久化的topic大量数据后可能导致性能下降。这里就像公总号一样,消费者消费完后,消息还会保留。
在这里插入图片描述
在这里插入图片描述

10.4.4、总结

① jdbc效率低,kahaDB效率高,jdbc+Journal效率较高。
② 持久化消息主要指的是:MQ所在服务器宕机了消息不会丢试的机制。
③ 持久化机制演变的过程:
从最初的AMQ Message Store方案到ActiveMQ V4版本退出的High Performance Journal(高性能事务支持)附件,并且同步推出了关于关系型数据库的存储方案。ActiveMQ5.3版本又推出了对KahaDB的支持(5.4版本后被作为默认的持久化方案),后来ActiveMQ 5.8版本开始支持LevelDB,到现在5.9提供了标准的Zookeeper+LevelDB集群化方案。

④ ActiveMQ消息持久化机制有:

AMQ基于日志文件
KahaDB基于日志文件,从ActiveMQ5.4开始默认使用
JDBC基于第三方数据库
Replicated LevelDB Store从5.9开始提供了LevelDB和Zookeeper的数据复制方法,用于Master-slave方式的首选数据复制方案。

10.4.5、开发中的坑

在配置关系型数据库作为ActiveMQ的持久化存储方案时,有坑

  • 数据库jar包
    注意把对应版本的数据库jar或者你自己使用的非自带的数据库连接池jar包

  • 数据库的编码

  • createTablesOnStartup属性
    默认为true,每次启动activemq都会自动创建表,在第一次启动后,应改为false,避免不必要的损失。

  • java.lang.IllegalStateException: LifecycleProcessor not initialized
    确认计算机主机名名称没有下划线

10.5、JDBC Message Store with ActiveMQ Journal

(1) 说明
这种方式克服了JDBC Store的不足,JDBC每次消息过来,都需要去写库读库。ActiveMQ Journal,使用高速缓存写入技术,大大提高了性能。当消费者的速度能够及时跟上生产者消息的生产速度时,journal文件能够大大减少需要写入到DB中的消息。
举个例子:生产者生产了1000条消息,这1000条消息会保存到journal文件,如果消费者的消费速度很快的情况下,在journal文件还没有同步到DB之前,消费者已经消费了90%的以上消息,那么这个时候只需要同步剩余的10%的消息到DB。如果消费者的速度很慢,这个时候journal文件可以使消息以批量方式写到DB。
为了高性能,这种方式使用日志文件存储+数据库存储。先将消息持久到日志文件,等待一段时间再将未消费的消息持久到数据库。该方式要比JDBC性能要高。

(2) 配置
下面是基于上面JDBC配置,再做一点修改:

修改配置前

<persistenceAdapter> 
	<jdbcPersistenceAdapter dataSource="#mysql-ds" /> 
</persistenceAdapter>

修改配置后

<persistenceFactory>        
	<journalPersistenceAdapterFactory journalLogFiles="5" journalLogFileSize="32768" useJournal="true" useQuickJournal="true" dataSource="#mysql-ds" dataDirectory="../activemq-data" /> 
</persistenceFactory>

10.6、总结

① jdbc效率低,kahaDB效率高,jdbc+Journal效率较高。
② 持久化消息主要指的是:MQ所在服务器宕机了消息不会丢试的机制。
③ 持久化机制演变的过程:
从最初的AMQ Message Store方案到ActiveMQ V4版本退出的High Performance Journal(高性能事务支持)附件,并且同步推出了关于关系型数据库的存储方案。ActiveMQ5.3版本又推出了对KahaDB的支持(5.4版本后被作为默认的持久化方案),后来ActiveMQ 5.8版本开始支持LevelDB,到现在5.9提供了标准的Zookeeper+LevelDB集群化方案。

④ ActiveMQ消息持久化机制有:

AMQ基于日志文件
KahaDB基于日志文件,从ActiveMQ5.4开始默认使用
JDBC基于第三方数据库
Replicated LevelDB Store从5.9开始提供了LevelDB和Zookeeper的数据复制方法,用于Master-slave方式的首选数据复制方案。

十一、高级特性

11.1、异步投递

ActiveMQ支持同步,异步两种发送的模式将消息发送到broker,模式的选择对发送延时有巨大的影响。producer能达到怎么样的产出率(产出率=发送数据总量/时间)主要受发送延时的影响,使用异步发送可以显著提高发送的性能。ActiveMQ默认使用异步发送的模式:除非明确指定使用同步发送的方式或者在未使用事务的前提下发送持久化的消息,这两种情况都是同步发送的。如果你没有使用事务且发送的是持久化的消息,每一次发送都是同步发送的且会阻塞producer知道broker返回一个确认,表示消息已经被安全的持久化到磁盘。确认机制提供了消息安全的保障,但同时会阻塞客户端带来了很大的延时。很多高性能的应用,允许在失败的情况下有少量的数据丢失。如果你的应用满足这个特点,你可以使用异步发送来提高生产率,即使发送的是持久化的消息。

异步发送

它可以最大化producer端的发送效率。我们通常在发送消息量比较密集的情况下使用异步发送,它可以很大的提升Producer性能;不过这也带来了额外的问题,就是需要消耗更多的Client端内存同时也会导致broker端性能消耗增加;此外它不能有效的确保消息的发送成功。在userAsyncSend=true的情况下客户端需要容忍消息丢失的可能。

异步是指生产者和broker之间发送消息的异步,不是指生产者和消费者之间异步。

总结:

  • 异步发送可以让生产者发的更快。
  • 如果异步投递不需要保证消息是否发送成功,发送者的效率会有所提高。如果异步投递还需要保证消息是否成功发送,并采用了回调的方式,发送者的效率提高不多,这种就有些鸡肋。

异步发送的实现⽅式

//⽅式⼀
public static final String URL = "tcp://192.168.0.101:61620?jms.useAsyncSend=true";
//⽅式⼆
connectionFactory.setUseAsyncSend(true);
//⽅式三
((ActiveMQConnection)connection).setUseAsyncSend(true);

异步发送如何确认发送成功

异步发送丢失消息的场景是:生产者设置userAsyncSend=true,使用activeMQMessageProducer.send(msg)持续发送消息。
如果消息不阻塞,生产者会认为所有send的消息均被成功发送至MQ。
如果MQ突然宕机,此时生产者端内存中尚未被发送至MQ的消息都会丢失。

所以,正确的异步发送方法是需要接收回调的。

同步发送和异步发送的区别就在此

  • 同步发送等send不阻塞了就表示一定发送成功了。
  • 异步发送需要客户端回执并由客户端再判断一次是否发送成功。

⽣产者异步投递且设置broker回调⽅法

producer.send(message, new AsyncCallback() {
    @Override
    public void onException(JMSException e) {
        System.out.println("发送失败");
    }

    @Override
    public void onSuccess() {
        System.out.println("发送成功");
    }
});

11.2、 延迟投递和定时投递

官网文档:http://activemq.apache.org/delay-and-schedule-message-delivery.html

ActiveMQ运行生产者延时投递消息,也称为计划发送,且能设置重复投递的间隔时时间及次数。

属性名称类型描述
AMQ_SCHEDULED_DELAYlong消息在计划由代理传递之前将等待的时间(以毫秒为单位)
AMQ_SCHEDULED_PERIODlong开始时间后等待再次调度消息之前的 等待时间(以毫秒为单位
AMQ_SCHEDULED_REPEATint重复调度消息以进行传递的次数
AMQ_SCHEDULED_CRONString使用 Cron 表达式来设置计划

修改配置文件activemq.xml修改代码,之后重启activemq。

<broker xmlns="http://activemq.apache.org/schema/core" brokerName="localhost" dataDirectory="${activemq.data}"  schedulerSupport="true" >
long delay =  10*1000;
long period = 5*1000;
int repeat = 3 ;

for (int i = 0; i < 3; i++) {
	  TextMessage textMessage = session.createTextMessage("tx msg--" + i);
	   // 延迟的时间
	   textMessage.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY, delay);
	   // 重复投递的时间间隔
	   textMessage.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_PERIOD, period);
	   // 重复投递的次数
	   textMessage.setIntProperty(ScheduledMessage.AMQ_SCHEDULED_REPEAT, repeat);
	   // 此处的意思:该条消息,等待10秒,之后每5秒发送一次,重复发送3次。
	   messageProducer.send(textMessage);
}

11.3、消息消费的重试机制

官网文档:http://activemq.apache.org/redelivery-policy

消费者收到消息,之后出现异常了,没有告诉broker确认收到该消息,broker会尝试再将该消息发送给消费者。尝试n次,如果消费者还是没有确认收到该消息,那么该消息将被放到死信队列重,之后broker不会再将该消息发送给消费者。

具体哪些情况会引发消息重发

  • 消费者在事务中调用了rollback(),进行事务回滚。
  • 消费者在没有提交commit。
  • 消费者在手动提交模式中,执行了session.recover()进行手动重试。

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

属性说明

参数默认值描述
backOffMultiplier5重试时间间隔递增倍数,只有值大于1和启用 useExponentialBackOff参数时才生效。
collisionAvoidanceFactor0.15如果启用,则避免碰撞范围的百分比。
initialRedeliveryDelay1000L初始重新传递延迟(以毫秒为单位)。
maximumRedeliveries6设置消息在被视为有毒消息并返回到代理之前将被重新传递的最大次数,以便它可以进入死信队列。设置 -1 为无限重新投递。
maximumRedeliveryDelay-1useExponentialBackOff 如果设置了该选项,则设置将 应用的最大传递延迟 。(使用值 -1 来定义不应用最大值)(v5.5)。
redeliveryDelay1000L重试延迟时间
useCollisionAvoidance ffalse重新交付策略是否应该使用碰撞避免。
useExponentialBackOfffalse是否应使用指数退避,即指数增加超时。

代码验证

生产者。发送3条数据。代码省略…
消费者。开启事务,却没有commit。重启消费者,前6次都能收到消息,到第7次,不会再收到消息。代码省略…

  • 生产者
    发送3条数据。代码省略…
  • 消费者
    开启事务,却没有commit。重启消费者,前6次都能收到消息,到第7次,不会再收到消息。代码省略…
  • activemq管理后台。多了一个名为ActiveMQ.DLQ队列,里面多了3条消息。

请添加图片描述
代码修改默认参数

// 修改默认参数,设置消息消费重试3次,更多的设置请参考官网文档。
RedeliveryPolicy redeliveryPolicy = new RedeliveryPolicy();
redeliveryPolicy.setMaximumRedeliveries(3);
activeMQConnectionFactory.setRedeliveryPolicy(redeliveryPolicy);

11.4、死信队列

官网文档: http://activemq.apache.org/redelivery-policy

死信队列:异常消息规避处理的集合,主要处理失败的消息,⼀般为业务提供错误回退的方案。

比如:订单超过支付时间应该被取消,这样的业务中可以使用死信队列来完成。

上游创建订单时,将消息发送到等待支付的队列,消息的超时时间为30分钟,如果三⼗分钟没有支付,则该消息进入到死信队列。而死信队列配有专门的消费者,来处理死信队列的消息,具体的处理方案就是把死信队列中的订单状态改为“已取消”。

配置死信队列
在activemq.xml中加入自定义死信队列的配置。

<!--加⼊死信队列的配置-->
<policyEntry queue=">" >
	<deadLetterStrategy>
 		<individualDeadLetterStrategy queuePrefix="DLQ." useQueueForQueueMessages="true" />
	</deadLetterStrategy>
 </policyEntry>
<!--结束死信队列的配置-->

比如未消费的超时的消息将会进入死信队列,死信队列的名称为“DLQ.队列的名称”。
在这里插入图片描述
此时,让死信队列的消费者,消费死信队列即可。

11.5、幂等性消费

所谓的幂等性指的是对数据的多次处理,结果是⼀致的。在restful请求中幂等性得到了充分的体现,比如get查询操作是幂等的,post新增操作是非幂等的。

如果因为网络延迟等原因,MQ无法及时接收到消费方的应答,导致MQ重试。在重试过程中造成重复消费的问题。

对于消息队列来说,如果消费者为了防止消息丢失,就有可能重复消费消息,因此要保障重复消费的幂等性。

幂等性如何保证

  • 如果消费方是做数据库操作,那么可以把消息的ID作为表的唯一主键,这样在重试的情况下,会触发主键冲突,从而避免数据出现脏数据。
  • 如果消费方不是做数据库操作,那么可以借助第三方的应用,例如Redis,来记录消费记录。每次消息被消费完成时候,把当前消息的ID作为key存入redis,每次消费前,先到redis查询有没有该消息的消费记录。
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值