ActiveMQ简单教程,适合初学者,学习笔记yyds

MQ概述

  • 消息中间件主要是为了解决解耦,削峰,异步的问题
  • 解耦:假如有一个中心系统,其他系统则需要从该中心系统获取数据,而一旦其他系统里有系统崩溃或者修改,该中心系统与之对应的接口可能也要修改.这样耦合性是非常高的.但如果中心系统将数据抽象到MQ中,俗称消息中间件,中间件便会通过发布订阅的机制让其他系统去消费,中心系统就不用管理其他系统是否出现问题的情况,若是其他系统出问题,直接取消与中间件的消费即可.
  • 削峰:假如突然间有大量的请求进来,系统可能一次性消化不过来,可以先堆积在MQ中,系统每次最多向MQ中获取自身能承受的请求数去处理.虽然处理的速度跟不上请求的过程,但是请求都是堆积在MQ中的,不影响系统的压力,一旦该高峰期一过,系统便可以快速消化MQ中的请求,又可以快速减轻负担.
  • 异步:假如有一个接收系统,一个处理系统,接收系统接收信息很快,但处理系统却很慢,这个时候对于用户来说,总的时间变得很大,不友好,所以需要一个MQ,先来堆积用户的请求,给用户视觉上看是成功了,之后再慢慢去处理这些业务.
  • 所以,MQ的大致过程是这样的:发送者发送消息给中间件,中间件将消息存在于队列或主题中,在合适的时候,中间件会将该消息发送给接受者,这两个过程是异步的,发送过程和接收过程的生命周期也没有必然的联系,而且中间件和接受者是一对多的关系,即一个消息可以由多个接受者接收.例子:在现实生活中,发送手机消息,如果对方关机,若用最原始的方式进行消息传递,那么这个消息肯定是接收不了的,但有了MQ,这个消息便可以在对方关机的时候发送出去,一旦对方开机了,MQ便可以将这个消息发布到对方中.

ActiveMQ安装与启动

  • 官网下载https://activemq.apache.org/components/classic/download/ linux版本apache-activemq-5.16.3
  • 然后将该文件夹转移到/opt/software目录下.
  • 然后在该目录下解压文件.解压完毕后,将解压文件转移到opt/model 下的文件夹active.
  • 进入解压目录的bin目录.输入./activemq start启动,启动之前必须安装jdk1.8(start更换stop为停止)
  • ps -ef|grep activemq|grep -v grep 查看是否启动
    1781是进程号,不是端口号,activemq默认端口号是61616
    下面的命令都可以查看启动状态,若端口号被占用了,则表明启动了
    ./activemq status
[root@dboop1 bin]# ps -ef|grep activemq|grep -v grep
root       1781      1 13 04:39 pts/0    00:00:14 /usr/local/java/jdk/jdk1.8.0_301/bin/java -Xms64M -Xmx1G -Djava.util.logging.config.file=logging.properties -Djava.security.auth.login.config=/opt/model/activemq/apache-activemq-5.16.3//conf/login.config -Dcom.sun.management.jmxremote -Djava.awt.headless=true -Djava.io.tmpdir=/opt/model/activemq/apache-activemq-5.16.3//tmp -Dactivemq.classpath=/opt/model/activemq/apache-activemq-5.16.3//conf:/opt/model/activemq/apache-activemq-5.16.3//../lib/: -Dactivemq.home=/opt/model/activemq/apache-activemq-5.16.3/ -Dactivemq.base=/opt/model/activemq/apache-activemq-5.16.3/ -Dactivemq.conf=/opt/model/activemq/apache-activemq-5.16.3//conf -Dactivemq.data=/opt/model/activemq/apache-activemq-5.16.3//data -jar /opt/model/activemq/apache-activemq-5.16.3//bin/activemq.jar start
[root@dboop1 bin]# netstat -anp|grep 61616
tcp6       0      0 :::61616                :::*                    LISTEN      1781/java

访问activemq前台

  • 在activemq中,有一个和tomcat类似的前台.
  • 访问路径为:linux ip:8161 (8161是前台端口,61616是后台端口)
  • 如果访问不了,查看linux防火墙是否关闭,或者查看windows防火墙是否关闭.关闭了才行.
  • 如果还不行,进入conf配置包中的jetty.xml配置文件,修改一下属性 1199行左右
    注意,127.0.0.1是windows的默认路径,所以这里我们可以改成广播路径0.0.0.0,之后,
    ./activemq restart重启便可以访问了
<bean id="jettyPort" class="org.apache.activemq.web.WebConsolePort" init-method="start">
             <!-- the default port number for the web console -->
        <property name="host" value="127.0.0.1"/>
        <property name="port" value="8161"/>
    </bean>

初始demo

消息生产者编码

  • 创建maven工程,导入依赖
<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <version>4.12</version>
  <scope>test</scope>
</dependency>

<dependency>
  <groupId>org.apache.activemq</groupId>
  <artifactId>activemq-all</artifactId>
  <version>5.15.9</version>
</dependency>
<!--与spring的整合包-->
<dependency>
  <groupId>org.apache.xbean</groupId>
  <artifactId>xbean-spring</artifactId>
  <version>3.2</version>
</dependency>

<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-api</artifactId>
  <version>1.7.25</version>
</dependency>

<dependency>
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-classic</artifactId>
  <version>1.2.3</version>
</dependency>

<!--简化javabean开发-->
<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
  <version>1.16.18</version>
  <scope>provided</scope>
</dependency>
  • 编写测试类
package com.hyb;

import org.apache.activemq.ActiveMQConnectionFactory;

import javax.jms.*;

public class ActiveConnectionFactory {

    private static final String CONNECT="tcp://192.168.188.99:61616";
    private static final String QUEUE="queue_1";

    public static void main(String[] args) throws Exception{
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(CONNECT);
//        创建连接
        Connection connection = activeMQConnectionFactory.createConnection();
        connection.start();
//        创建session
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//          创建队列
        Queue queue = session.createQueue(QUEUE);
//           创建消息的生产者
        MessageProducer producer = session.createProducer(queue);
//        创建消息
        TextMessage message1 = session.createTextMessage("message1");
//        发送消息
        producer.send(message1);
//        倒着关闭资源
        producer.close();
        session.close();
        connection.close();

    }
}
  • 查看前台队列
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V8MoVLfc-1653452866769)(C:\Users\46894\AppData\Local\Temp\WeChat Files\7e5c75f61fbac0a4674917504931735.png)]

消费者编码

  • 编写代码,逻辑基本一样
package com.hyb;

import org.apache.activemq.ActiveMQConnectionFactory;

import javax.jms.*;

public class MessageConsumer {

    private static final String CONNECT="tcp://192.168.188.99:61616";
    private static final String QUEUE="queue_1";

    public static void main(String[] args) throws Exception{
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(CONNECT);
        Connection connection = activeMQConnectionFactory.createConnection();
        connection.start();
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        Queue queue = session.createQueue(QUEUE);
        javax.jms.MessageConsumer consumer = session.createConsumer(queue);
        while (true){
            TextMessage textMessage = (TextMessage) consumer.receive();
            if (textMessage!=null){
                String text = textMessage.getText();
                System.out.println("接收到的消息是->"+text);
            }else {
                break;
            }
        }
        consumer.close();
        session.close();
        connection.close();
    }
}
  • 测试,然后观察控制台输出和前台队列情况
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZECi08MM-1653452866770)(C:\Users\46894\AppData\Local\Temp\WeChat Files\aaa31eb4130ca29407c547ffc285a78.png)]

receive() 方法说明

  • receive没有参数传入的时候,会一直等待.
  • 传入参数,毫秒为单位的值,表示超时时间.
  • 还有一个receiveNotWait() 表示不等待,取不到消息直接返回为空.

消息监听器

  • 在demo里,除了直接获取信息,还可以使用消息异步非阻塞监听器进行获取,这样的效果会更好一些.
        /*
        * 监听器取出消息
        * */
        consumer.setMessageListener(new MessageListener() {
            @Override
            public void onMessage(Message message) {
                if (message!=null&&message instanceof TextMessage){
                    try {
                        System.out.println("消息为-->"+((TextMessage) message).getText());
                    } catch (JMSException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
//        让控制台延迟,防止关闭的太快,消费不了,因为你上面的是采用匿名类取实现接口的,所以下一步执行的代码很快就会跳到关闭处
//        但如果加上控制台延迟,那么便不会先执行关闭操作.
        System.in.read();
        consumer.close();
        session.close();
        connection.close();
    }

消费顺序

  • 如果有多个消费者异步进行消费,消费顺序按先到先得原则.
  • 如果多个消费者同步消费,消费顺序趋向均衡原则.

队列

  • 队列在消息是一对一的,一旦队列中某个消息被消费,该队列会立马出队,下一个消费者不能消费已经出队的消息.

主题

  • 在现实生活中,主题好比一个公众号,只有关注了该公众号的账号才有资格接收该公众号的消息.
  • 所以,在消息队列中,一般先启动消费者才启动生产者,不然生产者的主题发布得便没有了意义.
  • 而且,该消息不同于队列,主题中的消息是一对多的,一个消息可以由多个消费者来消费.

队列和主题对比

  • 我们先把主题的代码写出来,先写出两个消费者,一个生产者.
    这段代码重新复制一份,建立另一个消费者.
    然后建立主题发布者
package com.hyb;

import org.apache.activemq.ActiveMQConnectionFactory;

import javax.jms.*;
import javax.jms.MessageConsumer;

public class TopicConsumer {

    private static final String CONNECT="tcp://192.168.188.99:61616";
    private static final String TOPIC="topic_1";

    public static void main(String[] args) throws Exception{
        System.out.println("消费者2");
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(CONNECT);
        Connection connection = activeMQConnectionFactory.createConnection();
        connection.start();
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        Topic topic = session.createTopic(TOPIC);
        MessageConsumer consumer = session.createConsumer(topic);
        consumer.setMessageListener(new MessageListener() {
            @Override
            public void onMessage(Message message) {
                if (message!=null&&message instanceof TextMessage){
                    try {
                        System.out.println("消息为-->"+((TextMessage) message).getText());
                    } catch (JMSException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        System.in.read();
        consumer.close();
        session.close();
        connection.close();
    }
}
package com.hyb;

import org.apache.activemq.ActiveMQConnectionFactory;

import javax.jms.*;

public class TopicFactory {

    private static final String CONNECT="tcp://192.168.188.99:61616";
    private static final String TOPIC="topic_1";

    public static void main(String[] args) throws Exception {
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(CONNECT);
        Connection connection = activeMQConnectionFactory.createConnection();
        connection.start();
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        Topic topic = session.createTopic(TOPIC);
        MessageProducer producer = session.createProducer(topic);
        TextMessage textMessage = session.createTextMessage("这是一个主题");
        producer.send(textMessage);
    }
}
  • 在对比的过程中,我们首先启动发布者,然后启动两个消费者,你会发现,即使发布者发布了消息,此刻的消费者是消费不到消息的.
  • 而如果我们先启动两个消费者,再启动生产者,这个时候消费者就会消费到,而且是两个消费者同时可以消费到.
  • 以上两点都区别于队列.
  • 但是,如果我们先启动一个消费者,然后启动生产者,消费者也确实消费了,但是当第二个消费者启动的时候,就消费不到第一个消费者消费的消息了.这个先来先到的特性和队列也一致.
  • 总结
    • 从工作模式上比较:队列趋向于均衡模式,如果同时消费,消息接收数量均衡.而主题则趋向全部模式,如果同时消费,消息接收数量保持一致,数量是总消息数量.
    • 从状态上来讲:主题没有状态,而队列会在mq服务器上以文件的形式保存,也可以配置成数据库存储.
    • 从完整性上讲:主题的发布的消息如果没有订阅者会造成丢失,而队列则不会.
    • 从处理效率上讲:主题处理的效率是受限于消费者的数量,消费者在同一时间数量越多,处理效率越低,而队列因为是一对一的关系,效率不会有明显下降.

JMS

  • JMS是javaee的一个子产品,用来实现两个应用程序之间通讯的API.当两个应用程序之间通讯时,他们并不是相连接的,而是通过一个共同的服务消息收发组件来达到异步,削峰,解耦的效果.
  • 四大元素:生产者,消息的产生者.消息:产生的消息.提供者,消息中间件.消费者:接收和处理消息.

JMS Message

  • 一个消息由消息头,消息属性,消息体构成.

消息头

JMS Destination: 发送消息的目的地,主要是指Queue和Topic
JMS DeliveryMode: 是否持久化,一条持久性的消息:应该被传送“一次仅仅一次”,这就意味着如果JMS提供者出现故障,该消息并不会丢失,它会在服务器恢复之后再次传递。一条非持久的消息:最多会传送一次,这意味着服务出现故障,该消息将永远丢失。
JMS Expiration: 可以设置消息在一定时间后过期,默认是永不过期。消息过期时间等于Destination的send方法中TimeToLive值加上发送时刻的GMT时间值。如果timeToLive值等于零,表示消息永不过期。如果发送后,在消息过期时间之后消息还没有被发送到目的地,则该消息被清除。
JMS Priority: 消息优先级,从0-9十个级别,0-4是普通消息,5-9是加急消息。JMS不要求MQ严格按照这是个优先级发送消息,但必须保证加急消息要优先于普通消息到达。默认是4。
JMS MessageID: 唯一识别每个消息的标识由MQ产生。

JMSTimestamp 一个JMS Provider在调用send()方法时自动设置的。它是消息被发送和消费者实际接收的时间差。自动分配
JMSCorrelationID 用来连接到另外一个消息,典型的应用是在回复消息中连接到原消息。在大多数情况下,JMSCorrelationID用于将一条消息标记为对JMSMessageID标示的上一条消息的应答,不过,JMSCorrelationID可以是任何值,不仅仅是JMSMessageID。由开发者设置
JMSReplyTo 提供本消息回复消息的目的地址。由开发者设置
JMSType 消息类型的识别符。由开发者设置
JMSRedelivered 如果一个客户端收到一个设置了JMSRedelivered属性的消息,则表示可能客户端曾经在早些时候收到过该消息,但并没有签收(acknowledged)。如果该消息被重新传送,JMSRedelivered=true反之,JMSRedelivered =false。自动设置

消息体

封装具体的消息数据,5种消息体格式,发送和接受的消息必须一致对应
TextMessage: 普通字符串消息,包含一个String
MapMessage: 一个map类型的消息,key为String类型,而值为Java的基本类型
BytesMessage: 二进制数组消息,包含一个byte[]
StreamMessage: Java数据流消息,用标准流来顺序的填充和读取
ObjectMessage: 对象消息,包含一个可序列化的Java对象

消息属性

如果需要除消息头字段之外的值,那么可以使用消息属性。他是识别/去重/重点标注等操作,非常有用的方法。

他们是以属性名和属性值对的形式制定的。可以将属性是为消息头得扩展,属性指定一些消息头没有包括的附加信息,比如可以在属性里指定消息选择器。消息的属性就像可以分配给一条消息的附加消息头一样。它们允许开发者添加有关消息的不透明附加信息。它们还用于暴露消息选择器在消息过滤时使用的数据。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qgZejYbH-1653452866771)(C:\Users\46894\AppData\Local\Temp\WeChat Files\a68e9bfc228a8c6282aa2cdc822bc0d.png)]

消息可靠性

持久化和非持久

  • 持久化允许MQ宕机后,消息仍然存在.非持久化则相反.
  • 队列和主题默认持久化

主题持久化例子

  • 前面说过,在主题中,首先要启动消费者等提供者发送消息的时候消费者才能订阅得到.
  • 但通过订阅的程序代码,便可以实现提供者先提供,而后消费者才能订阅的情况,但在这种情况下一定要先进行一次先启动消费者,再启动提供者,这样做的目的是为订阅提供者,当提供者再次发布,即使订阅者下线了,上线的时候也能接收.这种情况在持久化和非持久化下都能做到.
  • 代码几乎一样,但是有些地方需要修改
  • 一是生产者的开启连接位置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-14dpix6v-1653452866772)(C:\Users\46894\AppData\Local\Temp\WeChat Files\4e5f7a283002173c4143727285bb2b6.png)]

这里我们需要确定是否持久化后,再开启连接.
  • 二是订阅的时候,要先订阅才开启连接,同时需要标注自己的ID
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hyQvP7SB-1653452866772)(C:\Users\46894\AppData\Local\Temp\WeChat Files\f8a63804f7a555dd1698259c7a6af9f.png)]
  • 测试:第一步,先开启订阅,然后开启生产,这一次订阅成功,第二步之后,开启生产和消费的顺序无关紧要,只要第一次订阅了,生产者有消息了,订阅者无论何时上线都能接受到上次生产者将接受的消息.
    其次,这段代码还有一个特点,开启了持久化的存储数据,如果不开启,也是默认开启,一旦MQ发生宕机和错误,这些数据不会发生异常,所以,即使在MQ宕机后,只要MQ之前发送的是持久化数据,这段宕机的时间中,如果订阅者上线,还是能接受到消息的.

事务

  • 该事务原理和mysql的一样,要么失败,要么成功,失败可以回滚.在MQ中,也必须做到这一点,才能防止错误的发生.
  • 下面以生产者为例子.
  • 其他代码都一样,但是在创建session的时候,需要传入布尔型的事务参数[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Pu7g0NcF-1653452866773)(C:\Users\46894\AppData\Local\Temp\WeChat Files\485420e6d46b69678819604c5e7efb3.png)]
  • 而且,一旦设置事务后,必须在session关闭后进行提交
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dYFANIZI-1653452866773)(C:\Users\46894\AppData\Local\Temp\WeChat Files\cade7e505bab391cf075792344115b6.png)]
  • 如果中间发生错误,可以进行回滚
session.rollback();
  • 虽然这是针对生产者的事务,但消费者也是一样,虽然说生产者在建立事务的时候没有提交,发送消息会失败,但是对于消费者而言,如果自己开启事务的时候,没有提交而去消费了,这个时候就可以消费成功,但实际上消息还是没有消费,这就会造成重复消息,这是非常危险的,所以事务的提交这个操作非常重要.

签收

  • createSession方法中,第一个实参传入布尔事务值,而第二个则选择签收类型.
  • 自动签收(Session.AUTO_ACKNOWLEDGE):该方式是默认的。该种方式,无需我们程序做任何操作,框架会帮我们自动签收收到的消息。
  • 手动签收(Session.CLIENT_ACKNOWLEDGE):手动签收。该种方式,需要我们手动调用Message.acknowledge(),来签收消息。如果不签收消息,该消息会被我们反复消费,直到被签收.[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iRB5RKsX-1653452866774)(C:\Users\46894\AppData\Local\Temp\WeChat Files\a135a6d0e5f0f4b53fc878b3ad3e694.png)]只到被签收
  • 允许重复消息(Session.DUPS_OK_ACKNOWLEDGE):多线程或多个消费者同时消费到一个消息,因为线程不安全,可能会重复消费。该种方式很少使用到。
  • 事务下的签收(Session.SESSION_TRANSACTED):开始事务的情况下,可以使用该方式。该种方式很少使用到。
  • 注意:该说明是在非事务的情况下探讨的签收.

事务和签收

  • 事务无论在任何情况上,开启了就一定得提交.
  • 在事务性会话中,当一个事务被成功提交则消息被自动签收。如果事务回滚,则消息会被再次传送。事务优先于签收,开始事务后,签收机制不再起任何作用。
  • 非事务性会话中,便取决于签收模式.
  • 事务偏向生产者,签收偏向消费者。也就是说,生产者使用事务更好点,消费者使用签收机制更好点。

Broker

  • 嵌入式MQ实例,可脱离于linux服务器,但不是安装在window上的MQ,二是嵌入在java程序里的MQ实例.
  • 在linux中,bin/activemq start是启动MQ的命令,其中activemq 这个词语的意思是,我们启动了activemq.xml的配置文件,这个配置文件在conf目录下是可以找到的.所以,我们可以复制一份该配置文件,然后进行修改,修改出性能更高的或者更小巧的配置文件出来,然后启动的时候按照这个配置文件进行启动,这样便可以复制出多个不同状态的服务器了.
  • Broker便采用了这样的原理,我们将服务器打造成实例对象,然后内嵌在java中,等我们需要用的时候,可以直接实例化该对象出来,使用该服务器.
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.13.0</version>
</dependency>

  • 运行该代码后,可以创建出一个服务器,并启动着.
public static void main(String[] args) throws Exception {
    BrokerService brokerService = new BrokerService();
    brokerService.setUseJmx(true);
    brokerService.addConnector("tcp://localhost:61616");
    brokerService.start();
}
  • 这个时候,其他代码不变,只要修改服务地址便可以进行生产服务和消费服务.
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GdiRtkOW-1653452866775)(C:\Users\46894\AppData\Local\Temp\WeChat Files\67dda28551e3fa409640aa230c88563.png)]

Spring整合

生产者和消费者

  • 继续导入依赖
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-jms</artifactId>
  <version>5.3.6</version>
</dependency>

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aop</artifactId>
  <version>5.3.13</version>
</dependency>

    <dependency>
      <groupId>org.apache.activemq</groupId>
      <artifactId>activemq-pool</artifactId>
      <version>5.16.0</version>
    </dependency>

    <dependency>
      <groupId>org.apache.activemq</groupId>
      <artifactId>activemq-core</artifactId>
      <version>5.5.1</version>
    </dependency>
  • 编写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"></context:component-scan>

    <!--配置activemq工厂-->
    <bean id="jmsFactory" class="org.apache.activemq.pool.PooledConnectionFactory" destroy-method="stop">
        <property name="connectionFactory">
            <!--真正可以产生连接的,由jms产商提供-->
            <bean class="org.apache.activemq.ActiveMQConnectionFactory">
                <property name="brokerURL" value="tcp://192.168.188.99:61616"/>
            </bean>
        </property>
        <property name="maxConnections" value="100"></property>
    </bean>

    <!--创建队列目的地-->
    <bean id="destinationQueue" class="org.apache.activemq.command.ActiveMQQueue">
        <constructor-arg index="0" value="spring-active-queue"/>
    </bean>

    <!--spring提供的jms模板,它可以进行消息接收发送-->
    <bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
        <property name="connectionFactory" ref="jmsFactory"></property>
        <property name="defaultDestination" ref="destinationQueue"></property>
        <property name="messageConverter">
            <bean class="org.springframework.jms.support.converter.SimpleMessageConverter"/>
        </property>
    </bean>
</beans>
  • 编写生产者测试类
package com.springmq;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.MessageCreator;
import org.springframework.stereotype.Service;

import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.Session;
import javax.jms.TextMessage;

@Service
public class Producer_1 {
    @Autowired
    JmsTemplate jmsTemplate;

    public static void main(String[] args) {
        ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("application.xml");
        Producer_1 producer_1=(Producer_1) classPathXmlApplicationContext.getBean("producer_1");
        producer_1.jmsTemplate.send((session)-> {
            TextMessage textMessage=session.createTextMessage("Spring 整合jms 第一个实例");
            return textMessage;
        });
        System.out.println("生产完毕");
    }

}
  • 编写消费者
public static void main(String[] args) {
    ClassPathXmlApplicationContext application = new ClassPathXmlApplicationContext("application.xml");
    Consumer_1 consumer_1=(Consumer_1) application.getBean("consumer_1");
    String text=(String) consumer_1.jmsTemplate.receiveAndConvert();
    System.out.println(text);
}
  • 前面的目的地是队列,现在可以换成主题,只要修改配置文件就可以了
    只需要修改配置文件,其他代码和队列的一样,但是要注意先启动消费者,再启动生产者才有效.
<bean id="destinationTopic" class="org.apache.activemq.command.ActiveMQTopic">
    <constructor-arg index="0" value="spring-active-topic"/>
</bean>
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
    <property name="connectionFactory" ref="jmsFactory"></property>
    <property name="defaultDestination" ref="destinationTopic"></property>
    <property name="messageConverter">
        <bean class="org.springframework.jms.support.converter.SimpleMessageConverter"/>
    </property>
</bean>

监听器

  • 我们还可以配置消息监听器.
    注意,监听器的目的地一定要和jms模板的一样
    最后,写监听器的实现类
<!--配置消息监听器-->
<bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
    <property name="connectionFactory" ref="jmsFactory"></property>
    <property name="destination" ref="destinationTopic"></property>
    <property name="messageListener" ref="myMessageListener"></property>
</bean>

<bean id="myMessageListener" class="com.springmq.MyMessageListener"/>
public class MyMessageListener implements MessageListener {

    @Override
    public void onMessage(Message message) {
        if (message!=null&&message instanceof TextMessage){
            try {
                System.out.println(((TextMessage) message).getText());
            } catch (JMSException e) {
                e.printStackTrace();
            }

        }
    }
}
  • 写完后,我们测试,可以不用开启消费者先了,直接开启生产者,监听器便会自动给我们监听.

Springboot整合

初始化生产者

  • 新建springboot工程,添加依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <version>2.6.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-activemq</artifactId>
    <version>2.6.1</version>
</dependency>
  • 编辑yml文件
server:
  port: 777

spring:
  activemq:
    broker-url: tcp://192.168.188.99:61616
    user: admin
    password: admin
  jms:
    pub-sub-domain: false #false为队列 true为主题,默认是队列


#自定义队列名
myqueue: boot-activemq-queue
  • 编写配置文件类
package com.hyb.config;
@Configuration
/*开启适配jms的注解*/
@EnableJms
@ComponentScan("com.hyb.config")
public class ConfigClass {

    @Value("${myqueue}")
    private String queue;

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

}
  • 编写生产者类
package com.hyb.config;

@Component
public class QueueProducer {

    @Autowired
    JmsMessagingTemplate jmsMessagingTemplate;
    @Autowired
    Queue queue;

    public void sendMessage(){
        jmsMessagingTemplate.convertAndSend(queue,"send->"+ UUID.randomUUID().toString().substring(0,6));
    }

}
  • 在启动类中扫描配置类
@Import(ConfigClass.class)
  • 编写测试类测试
@Autowired
QueueProducer queueProducer;

@Test
void t1(){
    queueProducer.sendMessage();
}

定时投送消息

  • 定时投送消息可以让程序每隔一段时间来进行发送消息
  • 在需要定时投送消息的生产者中
    然后在启动类上
    测试直接开启启动类,发现控制台不结束,前台查看服务的网址不断有服务生成便成功.
//    开启定时投放功能
@Scheduled(fixedDelay = 3000)
public void sendMessageByTime(){
   jmsMessagingTemplate.convertAndSend(queue,"send by time->"+UUID.randomUUID().toString().substring(0,6));
}
@EnableScheduling

消费者

  • 新建一个消费者类
@Component
public class QueueConsumer {

    /*
    * 注册监听器,指定监听目的地是队列
    * */
    @JmsListener(destination = "${myqueue}")
    public void receive(TextMessage textMessage) throws JMSException {
        System.out.println("收到的消息-->"+textMessage.getText());
    }
}

订阅

  • 订阅的写法和队列的一样,只不过第一次启动的时候先启动订阅者再启动消费者.

传输信息协议

  • 在传输的过程,AMQ的传输协议有多种.
    • **tcp:**TCP是Broker默认配置的协议,默认监听端口是61616。 在网络传输数据前,必须要先序列化数据,消息是通过一个叫wireprotocol的来序列化成字节流。默认情况下,ActiveMQ把wireprotocol叫做OpenWire,它的目的是促使网络上的效率和数据快速交互。
      TCP连接的URI形式如:tcp://HostName:port?key=value&key=value,后面的参数是可选的
      TCP传输的的优点:
      TCP协议传输可靠性高,稳定性强 高效率:
      字节流方式传递,效率很高
      有效性、可用性:应用广泛,支持任何平台
    • nio:NIO协议和TCP协议类似,但NIO更侧重于底层的访问操作。它允许开发人员对同一资源可有更多的client调用和服务器端有更多的负载。
      适合使用NIO协议的场景:
      2.1 可能有大量的Client去连接到Broker上,一般情况下,大量的Client去连接Broker是被操作系统的线程所限制的。因此,NIO的实现比TCP需要更少的线程去运行,所以建议使用NIO协议。
      2.2 可能对于Broker有一个很迟钝的网络传输,NIO比TCP提供更好的性能。
      NIO连接的URI形式:nio://hostname:port?key=value&key=value
      关于Transport协议的可选配置参数可以参考官网http://activemq.apache.org/configuring-version-5-transports.html
    • AMQP协议:
    • stomp协议:
    • Secure Socket Layer Protocol(SSL)
    • mqtt协议:
    • ws协议:
  • 我们可以将tcp协议更改为nio协议,效率更高
    在conf目录中的activemq.xml文件中可以修改[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4H6epCXC-1653452866776)(C:\Users\46894\AppData\Local\Temp\WeChat Files\c3cab5ec730d4b76fe162a2f27914fb.png)]
    修改完毕后, 停止服务,然后重启虚拟机.
    然后重新登录windows的服务前台,查看connections选项,发现出现nio选项便说明成功了.
    之后在代码中便可以使用该协议进行连接,该协议连接的代码和tcp一样.
  • 其实 NIO 底层也是 tcp 实现的,不过现实的原理是非阻塞的。并且我们实际开发中我们都是需要让某个端口即支持 NIO 网络的 IO 模型,同时也应该支持其它 BIO 的网络 IO 模型。
    为了解决这个问题,官网为我们实现了 auto 这个关键字,auto 表示支持同时 openwire、amqp、stomp、mqtt 这四种协议。而通过 auto 和 “+” 符号来为端口设置多种特性,比如我们以 61608 端口为例:
    从配置文件中我们可以看出 url 使用的是 auto+nio,表示该端口支持 auto 的四大类型和 nio 类型。
    配置好后这个端口在使用的时候就可以切换不同的协议(只需要协议的对应端口为我们配置的 auto 端口 61608 即可):
<transportConnector name="auto+nio" uri="auto+nio://0.0.0.0:61608?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600&amp;org.apache.activemq.transport.nio.SeletorManager.corePoolSize=20&amp;org.apache.activemq.transport.nio.SeletorManager.maximumPoolSize=50"/>
// 支持 tcp 
public static final String ACTIVEMQ_URL = "tcp://localhost:61608";
public static final String QUEUE_NAME = "autonio";
// 同时也支持 nio
public static final String ACTIVEMQ_URL = "nio://localhost:61608";
public static final String QUEUE_NAME = "autonio";

持久化存储机制

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

KahaDB

  • KahaDB 是目前默认的存储方式,可用于任何场景,提高了性能和恢复能力。
    消息存储使用一个事务日志和仅仅用一个索引文件来存储它所有的地址。
    KahaDB 是一个专门针对消息持久化的解决方案,它对典型的消息使用模式进行了优化。
    数据被追加到 data logs 中。当不再需要 log 文件中的数据的时候,log 文件会被丢弃。
  • 在/opt/model/activemq/apache-activemq-5.16.3/data/kahadb目录下,有五个重要的kahadb的重要组成文件.
    db-.log:
    KahaDB 存储消息到预定义大小的数据记录文件中,文件命名为 db-.log 。当数据文件已满时,一个新的
    文件会随之创建,number数值也会随之递增,它随着消息数量的增多,如每32M一个文件,文件名按照数字进行编号,如
    db-1.log、db-2.log、db-3.log …。当不再有引用到数据文件中的任何消息时,文件会被删除或归档。
    **db-data:**该文件包含了持久化的 BTree 索引,索引了消息数据记录中的消息,它是消息的索引文件,本质上是 B-
    Tree(B树),使用B-Tree作为索引指向db-.log里面存储的消息。
    **db.free:**存储db-data文件中哪些页面是空闲的,文件具体内容是多有空闲页的ID
    **db.redo:**用来进行消息恢复,如果 KahaDB 消息存储在强制退出后启动,用于恢复 BTree 索引。
    lock 文件锁,表示当前获得 kahadb 读写权限的 broker 。

LevelDB(适用ActiveMQ 5.8及更高版本)

该文件系统是从ActiveMQ5.8之后引进的,它和KahaDB很相似,也是基于文件的本地数据库存储形式,但是它提供比KahaDB更快的持久性,但它不再使用自定义B-Tree实现来索引预写日志,而是使用基于LevelDB的索引。其索引具有几个不错的属性:快速更新(无需进行随机磁盘更新)
并发读取
使用硬链接快速索引快照
KahaDB和LevelDB存储都必须定期执行垃圾收集周期,以确定可以删除哪些日志文件。KahaDB由于增加了存储的数据量并且在收集发生时可能导致读/写停顿,因此可能非常慢。LevelDB存储使用更快的算法来确定何时收集日志文件并避免这些停顿。

 <broker brokerName="broker" ... >
    ...
    <persistenceAdapter>
      <levelDB directory="activemq-data"/>
    </persistenceAdapter>
    ...
  </broker>

jDBC消息存储

  • 该存储机制提供了我们在传输数据的过程中,将数据保存到数据库中,这里以mysql为例
  • 首先,需要修改activemq.xml文件中的选项
    <persistenceAdapter>
        <!--<kahaDB directory="${activemq.data}/kahadb"/>-->
        <jdbcPersistenceAdapter dataSource="#mysql-ds" createTablesOnStartup="false"/>
    </persistenceAdapter>
  • 使用第三方连接池druid,将其jar包导入lib目录下,同时要导入mysql数据库的驱动jar包mysql-connnector
  • 数据库连接池配置(使用官方自带的连接池),将其配置到activemq.xml配置文件中的标签之后,标签之前:
    注意,这个&符号一定在linux系统中一定要改成&+amp+“;”
   <bean id="mysql-ds" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
            <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
            <property name="url" value="jdbc:mysql://localhost:3306/activemq?useUnicode=true&amp;characterEncoding=UTF-8&amp;useSSL=false"/>
             <property name="username" value="root"/>
              <property name="password" value="15717747056HYb!"/>
              <property name="poolPreparedStatements" value="true"/>
    </bean>
  • 重启服务,观察可以登录windows服务前台,并且数据库里创建了三张表.
    ACTIVEMQ_ACKS: 用于存储持久化订阅的信息
    ACTIVEMQ_LOCK: 用于集群环境的时候,实现master的选举
    ACTIVEMQ_MSGS: 用于存储消息(包括队列和主题)
  • 如果是queue模式,在没有消费者的情况下会将消息保存到activemq_msgs表中,只要有任意一个消费者已经消费过,相应的消息将会立即被删除。
  • 如果是topic模式,一般是先启动消费订阅然后再启动生产的情况下会将主题保存到activemq_acks表中.虽然这个主题有可能被消费,但是不会消失,直到消费者下线,
  • 但在activemq_msgs的表中,会保存消费的信息,这个信息包括队列的和主题的
  • 若报错“java.lang.lllegalStateException:BeanFactory not initialized or already closed”这是因为操作系统的机器名中有“_”符号,更改机器名并且重启后即可解决。

高速缓存的 JDBC

JDBC Message Store with ActiveMQ Journal
  • 这种方式克服了 JDBC 存储的不足:JDBC 每次消息过来,都要去读写数据库,比较消耗性能,所以引进了 ActiveMQ Journal,使用高速缓存写入技术,大大提高了性能,
  • 用以下代码修改activemq.xml文件
    用该代码代替下面的代码
<persistenceFactory> 
   <journalPersistenceAdapterFactory 
        journalLogFiles="4"
        journalLogFileSize="32768"
        useJournal="true"
        useQuickJournal="true"
        dataSource="#mysql-ds"
        dataDirectory="activemq-data"/>
</persistenceFactory>
<persistenceAdapter>
    <!--<kahaDB directory="${activemq.data}/kahadb"/>-->
    <jdbcPersistenceAdapter dataSource="#mysql-ds" createTablesOnStartup="false"/>
</persistenceAdapter>
  • 高速缓存的执行结果是,每次都会首先将消息写进joural里,如果发现长时间发现写到日志的操作变得很少了,这个日志便会将积累的消息写入数据库中.

高可用机制

  • 避免因单个服务器出现故障而导致整个系统崩溃.(单点故障)

  • 高可用的原理:首先,broker启动时,它向ZooKeeper(集群)注册自己的信息(brokerName,消息日志的版本戳等),如果此时group中没有其他broker实例,会阻塞初始化过程,等待足够多的broker加入group,当broker的数量达到“replicas/2+1”时,zookeeper将根据”消息日志的版本戳“、”权重“的大小决定选举出一个master,即”版本戳“越大(数据最新)、权重越高的broker优先成为master,只有成为master的broker 可以提供服务,其他的broker 处于待机状态,被视为slave。
    当一个broker成为master时,它会向zookeeper注册自己的sync地址信息,slave根据sync地址与master建立连接,并同步消息文件(download)。当足够多的slave数据同步结束后,master将初始化transportConnector,此后client将可以与master进行数据交互。

  • 规划部署
    | 主机IP | bind端口 | zk集群端口 | MQ后台端口 | MQ前台端口 | 安装路径 |
    | — | — | — | — | — | — |
    | 192.168.188.99(hadoop1) | 63631 | 2181 | 61616 | 8161 | /opt/model |
    | 192.168.188.100(hadoop2) | 63632 | 2182 | 61617 | 8162 | /opt/model |
    | 192.168.188.101(hadoop3) | 63633 | 2183 | 61618 | 8163 | /opt/model |

  • 第一步,准备三台服务器,分别进入zk的zoo.cfg文件,修改clientport端口号.

  • 第二步:修改管理控制端口,在jetty.xml文件中,先修改一个,但集群分发后,记得修改每台服务器的管控端口.
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Gbo1Cro5-1653452866776)(C:\Users\46894\AppData\Local\Temp\WeChat Files\10827e77fbea63ff44d2c9a6329b53a.png)]

  • 配置集群,先配置一个
    这段代码要覆盖下面这段代码,如果没有这段代码,说明你之前用其他配置覆盖过.
    不仅如此,还要将mysql-ds的数据源删除掉.

<persistenceAdapter>
    <replicatedLevelDB
            directory="${activemq.data}/levelDB"
            replicas="3"
            bind="tcp://0.0.0.0:63631"
            zkAddress="hadoop1:2181,hadoop2:2182,hadoop3:2183" <!--集群名称,先配置好映射-->
            hostname="hadoop1"
            sync="local_disk"
            zkPath="/activemq/leveldb"/>
</persistenceAdapter>
<persistenceAdapter>
        <jdbcPersistenceAdapter  dataSource="#mysql-ds" createTablesOnStartup="false"/><persistenceAdapter>
  • 下一步,是在上一步的文件中,修改后台控制端口,先修改一个.
  • 利用一键分发脚本将activemq的文件夹发送给其他服务器.
  • 分发完毕后,将管理控制端口,和集群配置,后台端口都修改成自身的.
  • 配置启动一键启动文件
  • 最后,如果报错
    一是你配置jdk的时候,在/etc/profile的文件中,JAVA_HOME和JAVACMD没有暴露出来.
    二是activemq的配置文件有问题,amq识别java的配置文件在/apache-activemq-5.16.3/bin/env文件中,JAVA_HOME和JAVACMD是自动识别,这样会出问题,将这两个配置写成固定路径就好了.
Configuration variable JAVA_HOME or JAVACMD is not defined correctly
JAVA_HOME="/usr/local/java/jdk/jdk1.8.0_301"
JAVACMD="/usr/local/java/jdk/jdk1.8.0_301/bin/java" #不要这句,保留文件原始的auto值就可以,但是auto的值若是识别不出这个路径,得自己进行修改
  • 最后登录zk,然后查看跟节点,如果存在activemq的节点,便说明配置成功
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hUejaNzH-1653452866777)(C:\Users\46894\AppData\Local\Temp\WeChat Files\82afc3102a794b7d6c1043662a0d8c6.png)]
  • 如何查看那台机器是master?只要我们登录zk,然后get一个leveldb下的节点,就可以了
  • 注意:这个master和zk的leader是不一样的
  • 特性: 我们以管控端口为例子,如果有一台服务器挂掉了,会自动让另一个服务器变成master,所以每次管控端口只能浏览master.
  • 而当我们用代码连接集群的时候
    url便可以变为
failover://(tcp://172.16.85.3:61616,tcp://172.16.85.4:61616,tcp://172.16.85.5:61616)?initialReconnectDelay=1000&timeout=3000

高级特性

异步投递

  • 同步投递:生产者和消费者生产或接收消息都是在同步进行,但如果两者的效率不在同一个级别上,比如生产者消息发送太快,消费者消费太慢,就会造成信息堵塞.在amq中,同步投递需要自行设置,或者当非事务的情况下发送持久化消息.
  • 异步投递:生产者和消费者异生产或接收异步进行,互不影响.但这也有问题,因为两者的操作是异步进行的,无法保证消息是否接收成功,生产者发出了消息,就默认了发送成功,但消费者可能还未消费该消息MQ已经宕机,就会造成消息丢失.
  • 异步投递是amq默认的投递方式.而解决信息丢失的问题,保证消息发送成功,可以采用回调函数的方式,将成功和失败的情况反馈.
    显式使用异步投递,有三种方式
    在连接url后者以参数的方式使用,如:
    在connetcionFactory中设置
    在Connection中设置
    注意:这些设置都是基于ActiveMQ的类型
    采用回调的方式保证消息的发送情况.注意:无法保证一定发送成功,异步通讯允许有少量的消息丢失,但可以提高极大的性能.
tcp://localhost:61616?jms.useAsyncSend=true
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(CONNECT);
        activeMQConnectionFactory.setUseAsyncSend(true);
ActiveMQConnection connection = (ActiveMQConnection) activeMQConnectionFactory.createConnection();
connection.setUseAsyncSend(true);
//           创建消息的生产者
        ActiveMQMessageProducer producer = (ActiveMQMessageProducer) session.createProducer(queue);
        
        producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
//        创建消息
        TextMessage message1 = session.createTextMessage("message5");

        producer.send(message1, new AsyncCallback() {
            @Override
            public void onSuccess() {
//                成功情况
            }

            @Override
            public void onException(JMSException e) {
//              失败情况
            }
        });

延迟投递和定时投递

  • 投递四大属性
    | 属性名 | 类型 | 含义 |
    | — | — | — |
    | AMQ_SCHEDULED_DELAY | long | 延迟投递的时间 |
    | AMQ_SCHEDULED_PERIOD | long | 重复投递的时间间隔 |
    | AMQ_SCHEDULED_REPEAT | int | 重复投递次数 |
    | AMQ_SCHEDULED_CRON | String | Cron表达式(非activmq的方式,linux下直接设置) |

  • 延迟投递:
    1)在activemq.xml文件中,增加以下配置(如果配置了集群,最好每个都要配置)
    2)重启服务器.
    3)测试,一起测试延迟,投递间隔,重复次数

    在上面的图片你可以看到,如果你给一条消息设置了延迟二十秒发送,该消息首先会进行该标题栏中,然后等待二十秒后,你会发现queue标题栏中多出了一条信息,说明该信息延迟了二十秒才投进队列中.
    然后你会发现,每隔五秒,该信息会堆积一次,五次后,该信息堆积了五条.

<!--将schedulerSupport设置为true-->
<broker xmlns="http://activemq.apache.org/schema/core" brokerName="mybroker" dataDirectory="${activemq.data}" schedulerSupport="true">
//          针对该信息而言,延迟二十秒发送,以后每间隔五秒就投一次该信息,总投递五次
        message1.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY,20000);
//        间隔5s投一次
        message1.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_PERIOD,5000);
//        发送五次,注意是:setIntProperty
        message1.setIntProperty(ScheduledMessage.AMQ_SCHEDULED_REPEAT,5);

消息重发机制

应用场景

  • 在实际生产场景过程中,当消费者消费消息时,可能由于种种原因,导致消费者消费消息失败。例如:在一个通知系统中,生产者将通知Message
    放入队列中,而消费者从队列中将消息读取出来之后,除了进行自身业务处理,还需要调用第三方服务发送短信通知用户,但在发送短信服务的时候,由于网络超时等原有导致消费失败。在这种异常情况下,希望可以建立一种机制。当未发送成功的消息,能够重新发送。处理超过一定次数后还处理不成功的,放弃处理该消息,记录下来。继续对别的消息进行处理。

重发场景

  • 事务会话中,当还未进行session.commit()时,进行session.rollback(),那么所有还没commit的消息都会进行重发。
    使用客户端手动签收(CLIENT_ACKNOWLEDGE)的方式时,还未进行确认并且执行Session.recover(),那么所有还没acknowledge的消息都会进行重发。
    所有未ack(一种回应)的消息,当进行session.closed()关闭事务,那么所有还没ack的消息broker端都会进行重发,而且是马上重发。
    消息被消费者拉取之后,超时没有响应ack,消息会被broker重发。

重发调节

  • amq中,默认重发次数为6,重发间隔是1s,一旦有消息超过了六次,该消息就会被打上 “poison ack” 有毒消息的标签,放在一种特殊的队列中,这个队列成为死信队列.
  • 使用代码,可以修改默认的重发机制
  • 结合Spring配置文件
<bean id="activeMQRedeliveryPolicy" class="org.apache.activemq.RedeliveryPolicy">
    <!--是否在每次尝试重新发送失败后,增长这个等待时间-->
    <property name="useExponentialBackOff" value="true"></property>
    <!--重发次数,默认为6次-->
    <property name="maximumRedeliveries" value="5"></property>
    <!--重发时间间隔,默认为1秒-->
    <property name="initialRedeliveryDelay" value="1000"></property>
    <!--第一次失败后重新发送之前等待500毫秒,第二次失败再等待500 * 2毫秒,这里的2就是value-->
    <property name="backOffMultiplier" value="2"></property>
    <!--最大传送延迟,只在useExponentialBackOff为true时有效(V5.5),假设首次重连间隔为10ms,倍数为2,那么第 二次重连时间间隔为 20ms,第三次重连时间间隔为40ms,当重连时间间隔大的最大重连时间间隔时,以后每次重连时间间隔都为最大重连时间间隔。-->
    <property name="maximumRedeliveryDelay" value="1000"></property>
</bean>
<amq:connectionFactory id="amqConnectionFactory"
                           brokerURL="tcp://127.0.0.1:61616?tcpNoDelay=true" userName="admin" password="admin"  >
        <property name="redeliveryPolicy" ref="activeMQRedeliveryPolicy" />  <!-- 引用重发机制 -->
</amq:connectionFactory>

死信队列

  • 死信队列主要是用于保存处理失败或者过期的持久化消息,非持久化消息不会保存到死信队列中.
  • 默认情况下,所有被打上标记的消息都会保存在一个死信队列中,但这不方便管理,所以可以通过配置进行区分.
  • 首先测试发送的消息是队列形式的情况下.
    该配置有两种,一种是该消息到了死信队列中都以队列的形式保存,另一种是该消息到了私信队列中都以主题的形式保存.
    这些配置可以在activemq.xml中修改
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J7JtVVcv-1653452866777)(C:\Users\46894\AppData\Local\Temp\WeChat Files\2812188f67713eac9f11e504c85deaa.png)]
    从上面的配置可以看到,当发送过来的消息是队列的时候,我们可以设置一个该消息以哪种形式保存,但这一般都不搞混,队列是队列,主题是主题.
    如果发送来的消息是主题,我们可以设置其在私信队列中以主题的形式保存
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lcCdBLtP-1653452866778)(C:\Users\46894\AppData\Local\Temp\WeChat Files\c0379bb44ba74abc06942ae31e0755b.png)]
    当然在死信队列中也可以是队列形式保存

重复消费

  • 幂等性问题其中便有重复消费问题.
  • 那什么是幂等性消费?
    那么幂等是什么意思呢?幂等(idempotent、idempotence)实际上是一个数学与计算机学概念,在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同,通俗点说就是:同样的参数或者数据去调用同一个接口,无论重复调用多少次,总能保证数据的正确性,不能出错,这就是接口的幂等性。这里“数据的正确性”和具体的业务相关,不同的业务,对于幂等性的定义是不一样的。
    比如一个业务是读取和更新,该业务多次执行的结果都一样,就是幂等性问题.
    而该业务是向数据库插入一条数据呢?该业务便是典型的非幂等性问题,因为每次插入的数据都有唯一的标识,就会造成数据的不同.
  • 在发送消息的过程中,如果出现异常,会造成重复发送,此刻消费者便可能会重复消费.
  • 但是这样是可以解决的:只要给每个发送的消息标记唯一标识,而消费的时候就可以查看该唯一标识即可.
  • 解决该问题最常见的是用redis,利用redis保存消费记录,每次消费便可以查看redis是否有消费记录,如果没有,就消费,这样下次就不会造成重复消息.
  • 还可以这样操作:比如在上面我们使用了jdbc消息存储,我们将消息记录加上主键标识,这样消费的时候进行比较即可.
  • 还可以利用token进行解决幂等性问题.
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值