ActiveMQ

ActiveMQ

官网地址

入门概要

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

  • Api发送和接收
  • MQ的高可用
  • MQ集群和容错配置
  • MQ的持久化
  • 延迟发送/定时投送
  • 签收机制
  • Spring整合
  • ...
产品 kafka rabbitmq rocketmq activemq
编程语言 scala erlang java java
特点 大数据领域的主流MQ 基于erlang语言,不好修改底层,不要查找问题的原因,不建议选用 适用于大型项目。适用于集群。 适用于中小型项目。

MQ的产生背景

微服务架构后,链式调用是我们在写程序时候的一般流程,为了完成一个整体功能会将其拆分成多个函数(或子模块),比如模块A调用模块B,模块B调用模块C,模块C调用模块D。但在大型分布式应用中,系统间的RPC交互繁杂,一个功能背后要调用上百个接口并非不可能,从单机架构过渡到分布式微服务架构的通例

  1. 系统之间接口耦合比较严重

    每新增一个下游功能,都要对上游的相关接口进行改造

  2. 面对大流量并发时,容易被冲垮

    每个接口模块的吞吐能力是有限的,这个上限能力如果是堤坝,当大流量(洪水)来临时,容易被冲垮(秒杀业务)

  3. 等待同步存在性能问题

    RPC接口上基本都是同步调用,整体的服务性能遵循“木桶理论”,即整体系统的耗时取决于链路中最慢的那个接口

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

  • 要做到系统解耦,当新的模块接进来时,可以做到代码改动最小;能够解耦

  • 设置流量缓冲池,可以让后端系统按照自身吞吐能力进行消费,不被冲垮;能削峰

  • 强弱依赖梳理能将非关键调用链路的操作异步化并提升整体系统的吞吐能力;能够异步

MQ主要作用

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

MQ定义

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

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

MQ的特点

  1. 采用异步处理模式

    消息发送者可以发送一个消息而无须等待响应。消息发送者将消息发送到一条虚拟的通道(主题或者队列)上;

    消息接收者则订阅或者监听该条通道。一条消息可能最终转发给一个或者多个消息接收者,这些消息接收者都无需对消息发送者做出同步回应。整个过程都是异步的

  2. 应用系统之间解耦合

    发送者和接受者不必了解对方,只需要确认消息。

    发送者和接受者不必同时在线。

  3. 整体架构

    Title: MQ整体架构
    participant 生产者
    participant MQ
    participant 消费者
    生产者 -> MQ: 生产消息
    MQ -> 消费者: 消费消息
    

MQ的缺点

两个系统之间不能同步调用,不能实时回复,不能响应某个调用的回复

ActiveMQ安装和控制台

ActiveMQ安装

  1. 官方下载

    ActiveMQ官网

    点击 Download Latest 开始下载

  2. 安装步骤

    # 新建activemq文件夹,并进入该文件夹,执行命令解压文件
    [root@localhost /]# cd /opt
    [root@localhost opt]# mv apache-activemq-5.16.4-bin.tar.gz activemq/
    [root@localhost opt]# cd ./activemq/
    [root@localhost activemq]# tar -zxvf apache-activemq-5.16.4-bin.tar.gz
    # 在/etc/init.d/目录增加增加activemq文件 (加入系统服务设置快捷启动)
    [root@localhost apache-activemq-5.16.4]# cd /etc/init.d/
    [root@localhost init.d]# vim activemq
    # 注意:将下面内容全部复制。 要先安装jdk,在下面配置jdk的安装目录。
    #!/bin/sh
    #
    # /etc/init.d/activemq
    # chkconfig: 345 63 37
    # description: activemq servlet container.
    # processname: activemq 5.14.3
    
    # Source function library.
    #. /etc/init.d/functions
    # source networking configuration.
    #. /etc/sysconfig/network
    
    export JAVA_HOME=/opt/jdk/jdk1.8.0_161 # 自己的jdk目录 也可在/activemq/bin/env下指定
    export ACTIVEMQ_HOME=/opt/activemq/apache-activemq-5.16.4 # 自己的activemq目录
    
    case $1 in
        start)
            sh $ACTIVEMQ_HOME/bin/activemq start
        ;;
        stop)
            sh $ACTIVEMQ_HOME/bin/activemq stop
        ;;
        restart)
            sh $ACTIVEMQ_HOME/bin/activemq stop
            sleep 1
            sh $ACTIVEMQ_HOME/bin/activemq start
        ;;
    
    esac
    exit 0
    # 对activemq文件授予权限
    [root@localhost init.d]# chmod 777 activemq
    

    可选

    # 设置开机启动并启动activemq
    chkconfig activemq on
    service activemq start
    # 查看activemq状态
    service activemq status
    # 启动和关闭activemq服务
    service activemq start
    service activemq stop
    # 设置开机启动或不启动activemq服务
    chkconfig activemq on
    chkconfig activemq off
    

    直接进入activemq 的 文件下的activemq 下的 bin 目录,使用 ./activemq start 命令启动

    说明

    说明:activemq需要预先安装JDK

    如果ip地址无法访问,请配置防火墙端口:-A INPUT -p tcp -m state --state NEW -m tcp --dport 8161 -j ACCEPT

    如果该服务器同时安装了RabbitMQ,那么启动时会提示端口5672被占用,进入conf目录,打开配置文件activemq.xml文件,修改5672端口为5673.

    主要用端口

    8161 是后台管理端口

    61616 是程序用的tcp端口

    加入firewalld防火墙 或 者关闭防火墙

    # firewall-cmd --zone=public --add-port=61616/tcp --permanent
    # firewall-cmd --zone=public --add-port=8161/tcp --permanent
    # firewall-cmd –reload
    
    # systemctl stop firewalld
    # systemctl stop iptables
    

    修改后台访问端口

    vim activemq/conf/jetty.xml
    

    找到jettyPorthost127.0.0.1 修改为 0.0.0.0 (大概117行)

    注: 无法访问检测思路

    • 检查MQ是否正常启动
    • 检查linux本机是否可以访问
    • 主机互相pingIP地址
    • 百度版本配置文件是否存在IP限制
  3. 访问页面

    URL: http://IP地址:8161/ 账号:admin 密码: admin

  4. 因为主机名不符合规范导致无法启动activemq

    启动ActiveMQ异常:java.net.URISyntaxException: Illegal character in hostname at index

    翻译一下这个异常,就是:主机名主机名中包含非法字符,那么非法字符是什么呢?是“_”下划线;

    解决办法

    • 使用hostnamectl命令

      hostnamectl set-hostname 主机名

    • 修改配置文件 /etc/hostname 保存退出

      hostnamectl set-hostname activemq

  5. 启动时指定日志输出文件(重要)

    activemq日志默认的位置是在:%activemq安装目录%/data/activemq.log

    这是我们启动时指定日志输出文件:

    [root@VM_0_14_centos raohao]# service activemq start > /usr/local/log/activemq.log

  6. 查看程序启动是否成功的3种方式| 也是查看端口的三种方法(通用)

    1. 查看进程

      ps -ef | grep activemq

      ps -ef|grep activemq|grep -v grep grep -v grep 可以不让显示grep 本来的信息

    2. 查看端口是否被占用

      netstat -anp|grep 61616 # activemq 的默认后台端口是61616

    3. 查看端口是否被占用

      lsof -i:61616

ActiveMQ控制台

  1. 访问activemq管理页面

    地址:http://IP地址:8161/ 账户admin 密码admin

  2. 进入

    点击Manage ActiveMQ broker/admin/页面

入门案例、MQ标准、API详解

  1. 新建普通maven工程

  2. pom.xml导入依赖

    <dependencies>
    	<!--  activemq  所需要的jar 包-->
    	<!-- https://mvnrepository.com/artifact/org.apache.activemq/activemq-all -->
    	<dependency>
        	<groupId>org.apache.activemq</groupId>
        	<artifactId>activemq-all</artifactId>
        	<version>5.16.4</version>
    	</dependency>
    	<!--  activemq 和 spring 整合的基础包 -->
    	<!-- https://mvnrepository.com/artifact/org.apache.xbean/xbean-spring -->
    	<dependency>
        	<groupId>org.apache.xbean</groupId>
        	<artifactId>xbean-spring</artifactId>
        	<version>4.20</version>
    	</dependency>
    </dependencies>
    

JMS编码总体规范

Java 消息中间件的服务接口规范,activemq 之上是 mq , 而 mq 之上是JMS 定义的消息规范 。 activemq 是mq 技术的一种理论实现(与之相类似的实现还有 Kafka RabbitMQ RockitMQ ),而 JMS 是更上一级的规范。

  • JMS 开发基本步骤

    1. 创建一个 connection factory
    2. 通过 connection factory 来创建 JMS connection
    3. 启动 JMS connection
    4. 通过 connection 创建 JMS session
    5. 创建 JMS destination
    6. 创建 JMS producer 或者创建 JMS message 并设置 destination
    7. 创建 JMS consumer 或者注册一个 JMS message listener
    8. 发送或者接收 JMS message(s)
    9. 关闭所有 JMS 资源(connection, session, producer, consumer等 )
  • Destination简介

    Destination是目的地。目的地,我们可以理解为是数据存储的地方。

    Destination分为两种:队列和主题

    • 在点对点的消息传递时,目的地称为 队列 queue (1 -> 1)
    • 在发布订阅消息传递中,目的地称为 主题 topic (1 -> many)

队列消息(Queue)

队列消息生产者的入门案例
public class JmsProducer {
    /**
     * linux 上部署的activemq 的 IP 地址 + activemq 的端口
     */
    private static final String ACTIVEMQ_URL = "tcp://192.168.227.129:61616";

    /**
     * 目的地的名称
     */
    private static final String QUEUE_NAME = "aq1";

    public static void main(String[] args) throws JMSException {
        // 1 按照给定的url创建连接工厂,这个构造器采用默认的用户名密码。该类的其他构造方法可以指定用户名和密码。
        ActiveMQConnectionFactory activeMqConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
        // 2 通过连接工厂,获得连接 connection 并启动访问。
        Connection connection = activeMqConnectionFactory.createConnection();
        // 启动  connection
        connection.start();
        // 3 创建会话session 。第一参数是是否开启事务, 第二参数是消息签收的方式
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        // 4 创建目的地(两种 :队列/主题)。Destination是Queue和Topic的父类
        Destination queue = session.createQueue(QUEUE_NAME);
        // 5 创建消息的生产者
        MessageProducer producer = session.createProducer(queue);
        // 6 通过messageProducer 生产 3 条 消息发送到消息队列中
        for (int i = 0; i < 3; i++) {
            // 7  创建消息
            Message textMessage = session.createTextMessage("queue msg -> " + i);
            // 8  通过messageProducer发送给mq
            producer.send(textMessage);
        }
        // 9 关闭资源
        producer.close();
        session.close();
        connection.close();
        System.out.println("*** 消息发送到MQ完成 ***");
    }
}
ActiveMQ控制台之队列

点击控制台Queues,即/admin/queues.jsp"

可以看到名为aq1的队列有3条挂起的消息

  • Number Of Pending Messages:等待消费的消息,这个是未出队列的数量,公式=总接收数-总出队列数。

  • Number Of Consumers:消费者数量,消费者端的消费者数量。

  • Messages Enqueued:进队消息数,进队列的总消息量,包括出队列的。这个数只增不减。

  • Messages Dequeued:出队消息数,可以理解为是消费者消费掉的数量。

总结

当有一个消息进入这个队列时,等待消费的消息是1,进入队列的消息是1,出队列的消息是0。

当消息消费后,等待消费的消息是0,进入队列的消息是1,出队列的消息是1。

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

队列消息消费者的入门案例
public class JmsConsumer {
    /**
     * ACTIVE_MQ连接地址
     */
    private static final String ACTIVEMQ_URL = "tcp://192.168.227.129:61616";

    /**
     * 队列名称
     */
    public static final String QUEUE_NAME = "aq1";

    public static void main(String[] args) throws JMSException {
        ActiveMQConnectionFactory activeMqConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
        Connection connection = activeMqConnectionFactory.createConnection();
        connection.start();
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        Destination queue = session.createQueue(QUEUE_NAME);
        // 5 创建消息的消费者
        MessageConsumer consumer = session.createConsumer(queue);
        while (true) {
            // reveive() 一直等待接收消息,在能够接收到消息之前将一直阻塞。 是同步阻塞方式 。和socket的accept方法类似的。
            // reveive(Long time) : 等待n毫秒之后还没有收到消息,就是结束阻塞。
            // 因为消息发送者是 TextMessage,所以消息接受者也要是TextMessage
            Message message = consumer.receive();
            if (message == null) {
                // 其实根本进不来
                // receive 的 time 后,到达指定时间message才会为null
                break;
            }
            if (message instanceof TextMessage) {
                TextMessage textMessage = (TextMessage) message;
                String text = textMessage.getText();
                System.out.println("消费者读取消息:" + text);
            }
        }
    }
}

控制台显示

Name Number Of Pending Messages Number Of Consumers Messages Enqueued Messages Dequeued
aq1 0 1 3 3
异步监听式消费者(MessageListener)
import org.apache.activemq.ActiveMQConnectionFactory;

import javax.jms.*;
import java.io.IOException;

/**
 * jms消费者
 *
 * @author junjian
 * @since 2022/3/5 20:53 星期六
 */
public class JmsConsumer02 {
    /**
     * ACTIVE_MQ连接地址
     */
    private static final String ACTIVEMQ_URL = "tcp://192.168.227.129:61616";

    /**
     * 队列名称
     */
    public static final String QUEUE_NAME = "aq1";

    public static void main(String[] args) throws JMSException, IOException {
        ActiveMQConnectionFactory activeMqConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
        Connection connection = activeMqConnectionFactory.createConnection();
        connection.start();
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        Destination queue = session.createQueue(QUEUE_NAME);
        // 5 创建消息的消费者
        MessageConsumer consumer = session.createConsumer(queue);
        /* 通过监听的方式来消费消息,是异步非阻塞的方式消费消息。
           通过messageConsumer 的setMessageListener 注册一个监听器,
           当有消息发送来时,系统自动调用MessageListener 的 onMessage 方法处理消息 */
        consumer.setMessageListener(message -> {
            //  instanceof 判断是否A对象是否是B类的子类
            if (message instanceof TextMessage) {
                TextMessage textMessage = (TextMessage) message;
                try {
                    String text = textMessage.getText();
                    System.out.println("消费者读取消息:" + text);
                } catch (JMSException e) {
                    e.printStackTrace();
                }
            }
        });
        // 让主线程不要结束。因为一旦主线程结束了,其他的线程(如此处的监听消息的线程)也都会被迫结束。(因为是异步)
        // 实际开发中,我们的程序会一直运行,这句代码都会省略。
        int read = System.in.read();
        System.out.println(read);
        consumer.close();
        session.close();
        connection.close();
    }
}

生产者再生产三条消息,消费者消费

控制台显示

Name Number Of Pending Messages Number Of Consumers Messages Enqueued Messages Dequeued
aq1 0 1 6 6
队列消息(Queue)总结
  1. 两种消费方式

    • 同步阻塞方式(receive)

      消费者用MessageConsumer的receive()方法来接收消息,receive方法在能接收到消息之前(或超时之前)将一直阻塞。

    • 异步非阻塞方式(监听器onMessage())

      消费者通过MessageConsumer的setMessageListener(MessageListener listener)注册一个消息监听器,当消息到达之后,系统会自动调用监听器MessageListener的onMessage(Message message)方法。

  2. 队列的特点

    点对点的消费传递域的特点如下:

    • 每个消息只能有一个消费者,类似一对一的关系。
    • 消息的生产者和消费者之间没有时间的相关性。无论消费者在生产者发送消息的时候是否处于运行状态,消费者都可以提取消息。
    • 消息被消费者消费后队列中不再存储,所以消费者不会消费到已经被消费的消息。
  • 这里的一点经验: activemq 好像自带负载均衡,当先启动两个队列(Queue)的消费者时,在启动生产者发出消息,此时的消息平均的被两个消费者消费。 并且消费者不会消费已经被消费的消息(即为已经出队的消息)
  • 但是当有多个主题(Topic)订阅者时,发布者发布的消息,每个订阅者都会接收所有的消息。topic 更像是被广播的消息,但是缺点是不能接受已经发送过的消息。先要有订阅者,生产者才有意义。

补充

消息消费情况

存在消费者1和2

  • 情况1:只启动消费者1。

    结果:消费者1会消费所有的数据。

  • 情况2:先启动消费者1,再启动消费者2。

    结果:消费者1消费所有的数据。消费者2不会消费到消息。

  • 情况3:生产者发布6条消息,在此之前已经启动了消费者1和消费者2。

    结果:消费者1和消费者2平摊了消息。各自消费3条消息。

疑问:怎么去将消费者1和消费者2不平均分摊呢?而是按照各自的消费能力去消费。我觉得,现在activemq就是这样的机制。

发布 - 订阅模式(Topic)

topic介绍

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

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

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

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

生产者案例

与队列消息相比,仅第4步创建目的地不同

package jms.topic;

import org.apache.activemq.ActiveMQConnectionFactory;

import javax.jms.*;

public class JmsProducer {
    /**
     * linux 上部署的activemq 的 IP 地址 + activemq 的端口
     */
    private static final String ACTIVEMQ_URL = "tcp://192.168.227.129:61616";

    /**
     * 目的地的名称
     */
    private static final String TOPIC_NAME = "at1";


    public static void main(String[] args) throws JMSException {
        // 1 按照给定的url创建连接工厂,这个构造器采用默认的用户名密码。该类的其他构造方法可以指定用户名和密码。
        ActiveMQConnectionFactory activeMqConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
        // 2 通过连接工厂,获得连接 connection 并启动访问。
        Connection connection = activeMqConnectionFactory.createConnection();
        // 启动  connection
        connection.start();
        // 3 创建会话session 。第一参数是是否开启事务, 第二参数是消息签收的方式
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        // 4 创建目的地(两种 :队列/主题)。Destination是Queue和Topic的父类
        Destination topic = session.createTopic(TOPIC_NAME);
        // 5 创建消息的生产者
        MessageProducer producer = session.createProducer(topic);
        // 6 通过messageProducer 生产 6 条 消息发送到消息队列中
        for (int i = 0; i < 6; i++) {
            // 7  创建消息
            Message textMessage = session.createTextMessage("topic msg -> " + i);
            // 8  通过messageProducer发送给mq
            producer.send(textMessage);
        }
        // 9 关闭资源
        producer.close();
        session.close();
        connection.close();
        System.out.println("*** 消息发送到MQ完成 ***");
    }
}
消费者入门案例
package jms.topic;

import org.apache.activemq.ActiveMQConnectionFactory;

import javax.jms.*;
import java.io.IOException;

public class JmsConsumer {
    /**
     * ACTIVE_MQ连接地址
     */
    private static final String ACTIVEMQ_URL = "tcp://192.168.227.129:61616";

    /**
     * 队列名称
     */
    public static final String TOPIC_NAME = "at1";

    public static void main(String[] args) throws JMSException, IOException {
        ActiveMQConnectionFactory activeMqConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
        Connection connection = activeMqConnectionFactory.createConnection();
        connection.start();
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        Destination topic = session.createTopic(TOPIC_NAME);
        // 5 创建消息的消费者
        MessageConsumer consumer = session.createConsumer(topic);
        // 监听
        consumer.setMessageListener(message -> {
            if (message instanceof TextMessage) {
                TextMessage textMessage = (TextMessage) message;
                try {
                    System.out.println("消费者读取消息:" + textMessage.getText());
                } catch (JMSException e) {
                    e.printStackTrace();
                }
            }
        });
        // 防止主线程结束
        System.in.read();
        consumer.close();
        session.close();
        connection.close();
    }
}

仅启动消费者后生产的消息可以被消费;存在多个消费者,每个消费者都能收到,自从消费者启动后所有生产的消息。

ActiveMQ控制台

topic有多个消费者时,消费消息的数量 ≈ 在线消费者数量*生产消息的数量。

  1. 启动生产者生产6条消息
Name Number Of Consumers Messages Enqueued Messages Dequened
at1 0 6 0
  1. 启动一个消费者,再启动生产者生产6条消息
Name Number Of Consumers Messages Enqueued Messages Dequened
at1 1 12 6
  1. 再启动一个消费者,然后启动生产者生产6条消息
Name Number Of Consumers Messages Enqueued Messages Dequened
at1 2 18 18

注意:点击Topics 页面 Create 按钮 如出现 Error! 提示,可前往activemq/data(默认)文件夹下查看报错日志 tail -n 行数 文件名

解决办法:使用非 chrome 内核 浏览器 打开 activemq 后台

Tpoic 和 Queue 对比

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

JMS规范

JAVAEE 是一套使用Java 进行企业级开发的13 个核心规范工业标准 , 包括:
 JDBC  数据库连接
 JNDI  Java的命名和目录接口
 EJB   Enterprise java bean
 RMI   远程方法调用    一般使用TCP/IP 协议
 Java IDL    接口定义语言
 JSP    
 Servlet 
 XML
 JMS    Java 消息服务
 JTA        
 JTS
 JavaMail
 JAF 

JMS是什么

  • Java消息服务

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

  • JMS的组成结构和特点

JMS 部件 JMS provider JMS producer JMS consumer JMS message
含义 实现JMS 的消息中间件,也就是MQ服务器 消息生产者,创建和发送消息的客户端 消息消费者,接收和处理消息的客户端 JMS 消息,分为消息头、消息属性、消息体

消息头

  • JMS的消息头有哪些属性:

    • JMSDestination:消息目的地,分Queue 和 Topic

    • JMSDeliveryMode:消息持久化模式,持久还是非持久,默认为持久化模式

    • JMSExpiration:消息过期时间,默认永久

    • JMSPriority:消息的优先级,默认是4,有0~9 ,5-9 是紧急的,0-4 是普通的,同级别下与数字大小无关

    • JMSMessageID:消息的唯一标识符。用来解决幂等性问题。

    说明: 消息的生产者可以set这些属性,消息的消费者可以get这些属性。

    这些属性在send方法里面也可以设置。

    package jms.heard;
    
    import org.apache.activemq.ActiveMQConnectionFactory;
    
    import javax.jms.*;
    
    public class JmsProducer {
        /**
         * linux 上部署的activemq 的 IP 地址 + activemq 的端口
         */
        private static final String ACTIVEMQ_URL = "tcp://192.168.227.129:61616";
    
        /**
         * 目的地的名称
         */
        private static final String TOPIC_NAME = "at2";
    
    
        public static void main(String[] args) throws JMSException {
            // 1 按照给定的url创建连接工厂,这个构造器采用默认的用户名密码。该类的其他构造方法可以指定用户名和密码。
            ActiveMQConnectionFactory activeMqConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
            // 2 通过连接工厂,获得连接 connection 并启动访问。
            Connection connection = activeMqConnectionFactory.createConnection();
            // 启动  connection
            connection.start();
            // 3 创建会话session 。第一参数是是否开启事务, 第二参数是消息签收的方式
            Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
            // 4 创建目的地(两种 :队列/主题)。Destination是Queue和Topic的父类
            Destination topic = session.createTopic(TOPIC_NAME);
            // 5 创建消息的生产者
            MessageProducer producer = session.createProducer(topic);
            // 6 通过messageProducer 生产 6 条 消息发送到消息队列中
            for (int i = 0; i < 6; i++) {
                // 7  创建消息
                Message textMessage = session.createTextMessage("topic msg -> " + i);
                // 这里可以指定每个消息的目的地
                textMessage.setJMSDestination(topic);
                /* 持久模式和非持久模式。
                    一条持久性的消息:应该被传送“一次仅仅一次”,这就意味着如果JMS提供者出现故障
                    ,该消息并不会丢失,它会在服务器恢复之后再次传递。
                    一 条非持久的消息:最多会传递一次,这意味着服务器出现故障,该消息将会永远丢失。*/
                textMessage.setJMSDeliveryMode(DeliveryMode.NON_PERSISTENT);
                /*可以设置消息在一定时间后过期,默认是永不过期。
                消息过期时间,等于Destination的send方法中的timeToLive值加上发送时刻的GMT时间值。
                如果timeToLive值等于0,则JMSExpiration被设为0,表示该消息永不过期。
                如果发送后,在消息过期时间之后还没有被发送到目的地,则该消息被清除。*/
                textMessage.setJMSExpiration(1000);
                /*  消息优先级,从0-9十个级别,0-4是普通消息5-9是加急消息。
                JMS不要求MQ严格按照这十个优先级发送消息但必须保证加急消息要先于普通消息到达。默认是4级。*/
                textMessage.setJMSPriority(5);
                // 唯一标识每个消息的标识。MQ会给我们默认生成一个,我们也可以自己指定。
                textMessage.setJMSMessageID("ABCD");
                // 上面有些属性在 send 方法里和 MessageProducer 也能设置  
                producer.send(textMessage);
            }
            // 9 关闭资源
            producer.close();
            session.close();
            connection.close();
            System.out.println("*** 消息发送到MQ完成 ***");
        }
    }
    

    查看源码发现send方法取的是 MessageProducer 的属性信息

消息体

  • 封装具体的消息数据
  • 5种消息体格式
    • TextMessage:普通字符串消息,包含一个String
    • MapMessage:Map 类型的消息, key为String类型, 值为Java 基本类型
    • BytesMessage:二进制数组消息,包含一个byte[]
    • StreamMessage: Java 数据流消息,用标准流操作来顺序的填充读取
    • ObjectMessage:对象消息,包含一个可序列化的Java 对象
  • 发送和接收消息体格式必须一致
{
    // ...
	// 发送MapMessage  消息体。set方法: 添加,get方式:获取
	MapMessage mapMessage = session.createMapMessage();
	mapMessage.setString("name", "张三" + i);
	mapMessage.setInt("age", 18 + i);
	producer.send(mapMessage);
	// ...
}

{
    // ...
    // 监听
    consumer.setMessageListener(message -> {
        // 判断类型后转换
        try {
            if (message instanceof MapMessage) {
                MapMessage mapMessage = (MapMessage) message;
                String name = mapMessage.getString("name");
                int age = mapMessage.getInt("age");
                System.out.println("消费者读取消息(" + name + ":" + age + ")");
            }
            if (message instanceof TextMessage) {
                TextMessage textMessage = (TextMessage) message;
                System.out.println("消费者读取消息:" + textMessage.getText());
            }
        } catch (JMSException e) {
            e.printStackTrace();
        }
    });
    // ...
}

注意:如果消费者启动,但只写了读取TextMessage转换的代码,消费者依旧可以读取到消息,只是没有在控制台打印

消息属性

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

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

{
    // ...
    // 7  创建消息
    Message textMessage = session.createTextMessage("topic msg -> " + i);、、
    // 设置消息属性
    textMessage.setStringProperty("From", "ZhangSan@qq.com");
    textMessage.setByteProperty("Spec", (byte) 1);
    textMessage.setBooleanProperty("Invalid", true);
    // 8  通过messageProducer发送给mq
    producer.send(textMessage);
    // ...
}

{
    // ...
    // 监听
    consumer.setMessageListener(message -> {
        if (message instanceof TextMessage) {
            TextMessage textMessage = (TextMessage) message;
            try {
                System.out.println("消费者读取消息体:" + textMessage.getText());
                System.out.println("消费者读取消息属性:" + textMessage.getStringProperty("From"));
                System.out.println("消费者读取消息属性:" + textMessage.getByteProperty("Spec"));
                System.out.println("消费者读取消息属性:" + textMessage.getBooleanProperty("Invalid"));
            } catch (JMSException e) {
                e.printStackTrace();
            }
        }
    });
    // ...
}

如何保证消息的可靠性

JMS 可靠性:Persistent 持久性 、 事务 、 Acknowledge 签收

消息的持久化

什么是持久化消息?

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

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

  • 非持久

    messageProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);

    非持久化: 当服务器宕机,消息不存在

  • 持久

    messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);

    持久化:当服务器宕机,消息依旧存在

Queue消息非持久和持久
  1. 生产者添加如下代码

    {
        // ...
        // 持久化
        producer.setDeliveryMode(DeliveryMode.PERSISTENT);
        // 或者
        // 非持久化
        producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
        // ...
    }       
    
    
  2. 执行[root@localhost kahadb]# service activemq restart

  3. 启动消费者

    • 当消息为持久化时,消费者读取到消息
    • 当消息为非持久化时,消费者无法读取到消息

    也可以重启后通过控制台查看等待消费的消息数量

  • 经测试Queue消息默认为持久化

    消息持久化重启后Messages Enqueued和Messages Enqueued数据丢失,但等待消费的消息数量正常

    通过设置 Message#setJMSDeliveryMode(DeliveryMode.NON_PERSISTENT);无法达到消息非持久化目的,与MessageProducer#send|get|set方法默认读取this.newDeliveryMode有关

Topic消息持久化

topic消息默认是持久化的,消费者默认是非订阅的,因为生产者生产消息时,消费者也要在线,这样消费者才能消费到消息。

topic消息持久化,只要消费者向MQ服务器注册过,所有生产者发布成功的消息,该消费者都能收到,不管是MQ服务器宕机还是消费者不在线。

注意:

  1. 一定要先运行一次消费者,等于向MQ注册,类似我订阅了这个主题。

  2. 然后再运行生产者发送消息。

  3. 之后无论消费者是否在线,都会收到消息。如果不在线的话,下次连接的时候,会把没有收过的消息都接收过来。

{
 	// 设置持久化topic
    // 此处决定宕机后消息是否还存在
    producer.setDeliveryMode(DeliveryMode.PERSISTENT);
    // 或者
    // 设置非持久化topic
    producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
}

public class JmsConsumer {
    /**
     * ACTIVE_MQ连接地址
     */
    private static final String ACTIVEMQ_URL = "tcp://192.168.227.129:61616";

    /**
     * 队列名称
     */
    public static final String TOPIC_NAME = "at1";

    public static void main(String[] args) throws JMSException, IOException, InterruptedException {
        ActiveMQConnectionFactory activeMqConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
        Connection connection = activeMqConnectionFactory.createConnection();
        // 设置客户端ID。向MQ服务器注册自己的名称
        connection.setClientID("marrry");
        // 之后再开启连接 (一定要在setClientID()方法之后调用start()即可)
        connection.start();
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        Topic topic = session.createTopic(TOPIC_NAME);
        // 创建一个topic订阅者对象。一参是topic,二参是订阅者名称 
   		// 此处决定消费者离线后是否还可以消费消息
        TopicSubscriber topicSubscriber = session.createDurableSubscriber(topic, "remark...");
       	// while (true) {
        //     Message message = topicSubscriber.receive();
        //     if (message == null) {
        //         break;
        //     }
        //     if (message instanceof TextMessage) {
        //         TextMessage textMessage = (TextMessage) message;
        //         System.out.println("收到的持久化 topic:" + textMessage.getText());
        //     }
        // }
        topicSubscriber.setMessageListener(message -> {
            if (message instanceof TextMessage) {
                TextMessage textMessage = (TextMessage) message;
                try {
                    System.out.println("收到的持久化 topic:" + textMessage.getText());
                } catch (JMSException e) {
                    e.printStackTrace();
                }
            }
        });
        topicSubscriber.close();
        session.close();
        connection.close();
    }
}

控制台介绍

topic页面还是和之前的一样。另外在subscribers页面也会显示

  • Active Durable Topic Subscribers:在线的topic持久化消费者

  • Offline Durable Topic Subscribers:离线的topic持久化消费者

消息的事务性

  • producer提交时的事务

    • false: 只要执行send就进入队列中,关闭事务,那么第二个参数的设置需要有效
    • true:先执行send再执行commit,消息才会被真正的提交到队列中,用于消息需要批量提交,消息需要缓冲处理的场景
  • 事务偏生产者,签收偏消费者

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

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

  • 消费者和生产者的事务,完全没有关联,各自是各自的事务

  • 生产者事务

    先启动一个消费者

    // 开启事务,此时第二个参数意义不大
    Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
    

    发现消费者并没有消费到消息(也可以控制台查看)

    // 在所有消息发送后,提交消息
    session.commit();
    

    发现消费者接收到消息了

    for (int i = 0; i < 6; i++) {
        Message textMessage = session.createTextMessage("topic msg -> " + i);
        producer.send(textMessage);
        if (i == 3) {
            // 消息回滚
            session.rollback();
        }
    }
    

    发现控制台只打印了

    消费者读取消息:topic msg -> 4
    消费者读取消息:topic msg -> 5
    

    前面的消息都被回滚了

  • 消费者事务(以QUEUE模式来试验)

    // 两个消费者都开始事务后,启动,生产者发送消息
    Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
    

    发现两个两个消费者各消费了一半的消息,查看控制台,待消费的消息却没有清0

    // 一个消费者加上事务提交语句,重启,该消费者消费掉所有待消费的消息,控制台Number Of Pending Messages 变为0
    // 加在 textMessage.getText() 语句下
    session.commit();
    

    关闭一个消费者,另一个消费者做如下改动

    consumer.setMessageListener(message -> {
        //  instanceof 判断是否A对象是否是B类的子类
        if (message instanceof TextMessage) {
            TextMessage textMessage = (TextMessage) message;
            try {
                String text = textMessage.getText();
                session.rollback();
                System.out.println("消费者读取消息:" + text);
            } catch (JMSException e) {
                e.printStackTrace();
            }
        }
    });
    

    发现控制台将各消息消费了7次后停止打印,查看控制台发现多出了ActiveMQ.DLQ队列,并且刚才生产者生产的消息都在该队列的待消费消息里,发送的队列待消费消息为0

    这是因为消息被回滚一直未被标记为消费,消息的重发尝试超过了超过了为重发策略配置的最大重发次数,一个“Poison ACK”被发送回the broker(ActiveMQ 实例),让他知道消息被认为是毒丸。the broker然后接收消息并将其发送到死信队列,在activemq中死信队列叫做ActiveMQ.DLQ,所有无法传递的消息将被发送到这个队列,这很难管理。

    因此,您可以在Activemq.xml配置文件的目标策略映射中设置个体死信策略,它允许您为队列或主题指定特定的死信队列前缀。如果你能接受,你可以应用这个策略,让所有队列都有自己的死信队列,如下面的例子所示。

    <broker>
      <destinationPolicy>
        <policyMap>
          <policyEntries>
            <!-- 使用“>”通配符在所有队列上设置以下策略-->
            <policyEntry queue=">">
              <deadLetterStrategy>
                <!--
                  使用前缀'DLQ'作为目标名称,并使DLQ成为队列而不是主题
                -->
                <individualDeadLetterStrategy queuePrefix="DLQ." useQueueForQueueMessages="true"/>
              </deadLetterStrategy>
            </policyEntry>
          </policyEntries>
        </policyMap>
      </destinationPolicy>
    </broker>
    

消息的签收机制

  • 签收的几种方式

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

    • 在事务性会话中,当一个事务被成功提交则消息被自动签收。如果事务回滚,则消息会被再次传送。事务优先于签收,开始事务后,签收机制不再起任何作用。

    • 非事务性会话中,消息何时被确认取决于创建会话时的应答模式。

    • 生产者事务开启,只有commit后才能将全部消息变为已消费。

    • 事务偏向生产者,签收偏向消费者。也就是说,生产者使用事务更好点,消费者使用签收机制更好点。

  • 生产者不变,消费者改为

    // 设置消息为手动签收,启动消费者
    Session session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
    

    生产者生产消息,发现消费者控制台打印消息,查看控制台,发现所有消息都是待消费、

    // 手动签收消息,重启消费者( textMessage.getText() 后签收)
    /* 设置为Session.CLIENT_ACKNOWLEDGE后,要调用该方法,标志着该消息已被签收(消费)。
       如果不调用该方法,该消息的标志还是未消费,下次启动消费者或其他消费者还会收到改消息。*/
    textMessage.acknowledge();
    

    发现所有消息状态变为被消费

JMS的点对点总结

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

如果在Session关闭时有部分消息己被收到但还没有被签收(acknowledged),那当消费者下次连接到相同的队列时,这些消息还会被再次接收

队列可以长久地保存消息直到消费者收到消息。消费者不需要因为担心消息会丢失而时刻和队列保持激活的连接状态,充分体现了异步传输模式的优势

JMS的发布订阅总结

  • JMS的发布订阅总结

    JMS Pub/Sub 模型定义了如何向一个内容节点发布和订阅消息,这些节点被称作topic。

    主题可以被认为是消息的传输中介,发布者(publisher)发布消息到主题,订阅者(subscribe)从主题订阅消息。

    主题使得消息订阅者和消息发布者保持互相独立不需要解除即可保证消息的传送

  • 非持久订阅

    非持久订阅只有当客户端处于激活状态,也就是和MQ保持连接状态才能收发到某个主题的消息。

    如果消费者处于离线状态,生产者发送的主题消息将会丢失作废,消费者永远不会收到。

    一句话:先订阅注册才能接受到发布,只给订阅者发布消息。

  • 持久订阅

    客户端首先向MQ注册一个自己的身份ID识别号,当这个客户端处于离线时,生产者会为这个ID保存所有发送到主题的消息,当客户再次连接到MQ的时候,会根据消费者的ID得到所有当自己处于离线时发送到主题的消息

    非持久订阅状态下,不能恢复或重新派送一个未签收的消息。

    持久订阅才能恢复或重新派送一个未签收的消息。

  • 非持久和持久化订阅如何选择

    当所有的消息必须被接收,则用持久化订阅。当消息丢失能够被容忍,则用非持久订阅。

ActiveMQ的broker

  • broker是什么

    相当于一个ActiveMQ服务器实例。说白了,Broker其实就是实现了用代码的形式启动ActiveMQ将MQ嵌入到Java代码中,以便随时用随时启动,在用的时候再去启动这样能节省了资源,也保证了可用性。这种方式,我们实际开发中很少采用,因为他缺少太多了东西,如:日志,数据存储等等。

  • 启动broker时指定配置文件

    启动broker时指定配置文件,可以帮助我们在一台服务器上启动多个broker。实际工作中一般一台服务器只启动一个broker。

    [root@localhost conf]# pwd
    /opt/activemq/apache-activemq-5.16.4/conf
    [root@localhost conf]# ls
    activemq.xml  broker-localhost.cert  client.ks  credentials-enc.properties  groups.properties  jetty-realm.properties  jmx.access    log4j.properties    login.config
    broker.ks     broker.ts              client.ts  credentials.properties      java.security      jetty.xml               jmx.password  logging.properties  users.properties
    [root@localhost conf]# cp activemq.xml activemq02.xml 
    [root@localhost conf]# ls
    activemq02.xml  broker.ks              broker.ts  client.ts                   credentials.properties  java.security           jetty.xml   jmx.password      logging.properties  users.properties
    activemq.xml    broker-localhost.cert  client.ks  credentials-enc.properties  groups.properties       jetty-realm.properties  jmx.access  log4j.properties  login.config
    [root@localhost conf]# ../bin/activemq start xbean:file:/opt/activemq/apache-activemq-5.16.4/conf/activemq02.xml
    
  • 嵌入式的broker启动

    用ActiveMQ Broker作为独立的消息服务器来构建Java应用。

    ActiveMQ也支持在vm中通信基于嵌入的broker,能够无缝的集成其他java应用。

  • 代码

    pom文件文件添加

    <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.13.1</version>
    </dependency>
    

    嵌入式broke的启动类

    public class EmbedBroker {
        public static void main(String[] args) throws Exception {
            //ActiveMQ也支持在vm中通信基于嵌入的broker
            BrokerService brokerService = new BrokerService();
            brokerService.setPopulateJMSXUserID(true);
            brokerService.addConnector("tcp://127.0.0.1:61616");
            brokerService.start();
            // 防止线程结束,服务关闭
            System.in.read();
        }
    }
    

    根据控制台提示,发现连接url为tcp://account.wondershare.com:61616(只可进行tcp连接,即无页面)

    启动生产者消费者,发现正常使用

Spring整合ActiveMQ

我们之前介绍的内容也很重要,他更灵活,他支持各种自定义功能,可以满足我们工作中复杂的需求。很多activemq的功能,我们要看官方文档或者博客,这些功能大多是在上面代码的基础上修改完善的。如果非要把这些功能强行整合到spring,就有些缘木求鱼了。我认为另一种方式整合spring更好,就是将上面的类注入到Spring中,其他不变。这样既能保持原生的代码,又能集成到spring。

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

pom.xml添加依赖

<dependencies>
    <!-- activemq核心依赖包  -->
    <dependency>
        <groupId>org.apache.activemq</groupId>
        <artifactId>activemq-all</artifactId>
        <version>5.16.4</version>
    </dependency>
    <!--  嵌入式activemq的broker所需要的依赖包   -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.13.1</version>
    </dependency>
    <!-- activemq连接池 -->
    <!-- https://mvnrepository.com/artifact/org.apache.activemq/activemq-pool -->
    <dependency>
        <groupId>org.apache.activemq</groupId>
        <artifactId>activemq-pool</artifactId>
        <version>5.16.4</version>
    </dependency>
    <!-- spring支持jms的包 -->
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-jms -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jms</artifactId>
        <version>5.3.16</version>
    </dependency>
    <!--spring相关依赖包-->
    <dependency>
        <groupId>org.apache.xbean</groupId>
        <artifactId>xbean-spring</artifactId>
        <version>4.20</version>
    </dependency>
    <!-- spring-aop依赖 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aop</artifactId>
        <version>5.2.15.RELEASE</version>
    </dependency>
    <!-- Spring核心依赖 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>5.2.15.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.15.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aop</artifactId>
        <version>5.2.15.RELEASE</version>
    </dependency>
    <!-- orm: 对象关系映射 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-orm</artifactId>
        <version>5.2.15.RELEASE</version>
    </dependency>
</dependencies>

Spring的ActiveMQ配置文件

spring-activemq.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.wang.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.227.129:61616"/>
            </bean>
        </property>
        <property name="maxConnections" value="100"/>
    </bean>

    <!--  这个是主题  -->
    <bean id="destinationTopic" class="org.apache.activemq.command.ActiveMQTopic">
        <constructor-arg index="0" value="spring-active-topic"/>
    </bean>
    
    <!--  这个是队列目的地,  点对点-->
    <bean id="destinationQueue" class="org.apache.activemq.command.ActiveMQQueue">
        <constructor-arg index="0" value="spring-active-queue"/>
    </bean>


    <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>
</beans>

生产者

public class Producer {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-activemq.xml");
        JmsTemplate jmsTemplate = context.getBean("jmsTemplate", JmsTemplate.class);
        jmsTemplate.send(session -> session.createTextMessage("spring 和 activemq 的整合"));
        System.out.println(" *** send task over ***");
    }
}

消费者

public class Consumer {
    // 注意,启动消费者后需要生产者再发一次消息,因为没有订阅,无法收到消费者启动前的消息
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-activemq.xml");
        JmsTemplate jmsTemplate = context.getBean("jmsTemplate", JmsTemplate.class);
        Object retValue = jmsTemplate.receiveAndConvert();
        System.out.println("消费者收到消息:" + retValue);
    }
}

监听器

配置文件添加

<!-- 
    <bean id="myMessageListener" class="com.wang.demo.MyMessageListener"/>
-->

    <bean id="container" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
        <property name="connectionFactory" ref="connectionFactory"/>
        <property name="destination" ref="destinationTopic"/>
        <property name="messageListener" ref="myMessageListener"/>
    </bean>

并且可以在spring 中设置监听器,不用启动消费者,就可以自动监听到消息,并处理

@Component
public class MyMessageListener implements MessageListener {
    @Override
    public void onMessage(Message message) {
        if (message instanceof TextMessage) {
            TextMessage textMessage = (TextMessage) message;
            try {
                System.out.println(textMessage.getText());
            } catch (JMSException e) {
                e.printStackTrace();
            }
        }
    }
}

SpringBoot整合ActiveMQ

使用SpringBoot整合ActiveMQ的方式,会失去原生代码的部分功能和灵活性。但是工作中,这种做能够满足我们常见的需求,也方便和简化我们的代码,也为了适应工作中大家的习惯。

pom.xml

如果 https://start.spring.io/ 连接不上就改为 http://start.spring.io/,要是还连不上可使用镜像地址https://start.aliyun.com

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>springboot-activemq</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-activemq</name>
    <description>springboot-activemq</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <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>
        </dependency>
        <!--spring boot整合activemq的jar包-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-activemq</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

application.yml

# web占用端口
server:
  port: 9527
spring:
  activemq:
    # activemq的broker的url
    broker-url: tcp://192.168.227.129:61616
    # 连接activemq的broker所需的账号和密码
    user: admin
    password: admin
  jms:
    # 目的地是queue还是topic, false(默认) = queue    true =  topic
    pub-sub-domain: true

#  自定义队列名称。这只是个常量
myqueue: boot-activemq-queue
#  自定义主题名称
mytopic: boot-activemq-topic

配置目的地的Bean

// 让spring管理的注解,相当于spring中在xml 中写了个bean
@Configuration
// 开启jms适配
@EnableJms
public class ConfigBean {
    // 注入配置文件中的 myqueue
    @Value("${myqueue}")
    private String myQueue;

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


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

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

队列生产者代码

@Component
public class QueueProducer {
    // JMS模板
    @Autowired
    private JmsMessagingTemplate jmsMessagingTemplate;

    // 这个是我们配置的队列目的地 pub-sub-domain: false
    @Autowired
    private Queue queue;

    // 发送消息
    public void produceMessage() {
        // 一参是目的地,二参是消息的内容
        jmsMessagingTemplate.convertAndSend(queue, "***** "
                + UUID.randomUUID().toString().replace("-", "")
                + " *****");
    }

    // 定时任务。每3秒执行一次。非必须代码,仅为演示。记得主类开启定时任务支持
    @Scheduled(fixedDelay = 3000)
    public void produceMessageScheduled() {
        produceMessage();
    }
}

队列消费者代码

@Component
public class QueueConsumer {
    // 注册一个监听器。destination指定监听的主题。
    @JmsListener(destination = "${myqueue}")
    public void receive(TextMessage textMessage) throws JMSException {
        System.out.println(" *** Queue 消费者收到消息  ***" + textMessage.getText());
    }
}

主启动类

// 是否开启定时任务调度功能
@EnableScheduling
@SpringBootApplication
public class SpringbootActivemqApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootActivemqApplication.class, args);
    }
}

Topic生产者

@Component
public class TopicProducer {
    // JMS模板
    @Autowired
    private JmsMessagingTemplate jmsMessagingTemplate;

    // 这个是我们配置的topic目的地  pub-sub-domain: true
    @Autowired
    private Topic topic;

    // 发送消息
    public void produceMessage() {
        // 一参是目的地,二参是消息的内容
        jmsMessagingTemplate.convertAndSend(topic, "***** "
                + UUID.randomUUID().toString().replace("-", "")
                + " *****");
    }

    // 定时任务。每3秒执行一次。非必须代码,仅为演示。记得主类开启定时任务支持
    @Scheduled(fixedDelay = 3000)
    public void produceMessageScheduled() {
        produceMessage();
    }
}

Topic消费者

@Component
public class TopicConsumer {
    // 注册一个监听器。destination指定监听的主题。
    @JmsListener(destination = "${mytopic}")
    public void receive(TextMessage textMessage) throws JMSException {
        System.out.println(" *** Topic 消费者收到消息  ***" + textMessage.getText());
    }
}

ActiveMQ的传输协议

简介

ActiveMQ支持的client-broker通讯协议有:TCP、NIO、UDP、SSL、Http(s)、VM。其中配置Transport Connector的文件在ActiveMQ安装目录的conf/activemq.xml中的<transportConnectors>标签之内。

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

# 协议参见文件 $ACTIVEMQ_HOME/conf/activemq.xml 下面是文件的重要的内容
<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

支持的传输协议

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

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

协议 描述
TCP 默认的协议,性能相对可以
NIO 基于TCP协议之上的,进行了扩展和优化,具有更好的扩展性
UDP 性能比TCP更好,但是不具有可靠性
SSL 安全链接
HTTP(S) 基于HTTP或HTTPS
VM VM本身不是协议,当客户端和代理在同一个JAVA虚拟机(VM)中运行时,他们之间需要通信,但不想占用网络通道,而是直接通信,可以使用该方式
TCP协议
  1. Transmission Control Protocol(TCP)是默认的。TCP的Client监听端口61616
  2. 在网络传输数据前,必须要先序列化数据,消息是通过一个叫wire protocol的来序列化成字节流
  3. TCP连接的URI形式如:tcp://HostName:port?key=value&key=value,后面的参数是可选的
  4. TCP传输的的优点:
    • TCP协议传输可靠性高,稳定性强
    • 高效率:字节流方式传递,效率很高
    • 有效性、可用性:应用广泛,支持任何平台
  5. (1) 关于Transport协议的可选配置参数可以参考官网http://activemq.apache.org/tcp-transport-reference
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

AMQP协议

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

STOMP 协议

STOMP,Streaming Text Orientated Message Protocal, 是流文本定向消息协议,是一种为MOM(Message Orientated Middleware, 面向消息的中间件)设计的简单文本协议

MQTT 协议

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

NIO协议案例

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

修改配置文件activemq.xml

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

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

  2. 修改完成后重启activemq: service activemq restart

  3. 查看管理后台Connections,可以看到页面多了nio

代码 改为private static final StringACTIVEMQ_URL = "nio://192.168.227.129:61618"即可

NIO协议案例增强

  • 目的

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

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

    • 如何既可以让这个端口支持NIO网络IO模式,又可以支持多个协议?

  • 解决

    • 使用auto关键字
    • 使用"+"符号来为端口设置多种特性
    • http://activemq.apache.org/auto
    • auto 就像是一个网络协议的适配器,可以自动检测协议的类型,并作出匹配

修改配置文件activemq.xml

# 继续添加 ?后面的参数可选
<transportConnector name="auto+nio" uri="auto+nio://0.0.0.0:61608?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600&amp;org.apache.activemq.transport.nio.SelectorManager.corePoolSize=20&amp;org.apache.activemq.transport.nio.Se1ectorManager.maximumPoo1Size=50"/>
# auto:针对所有的协议,他会识别我们是什么协议。
#  nio:使用NIO网络IO模型

修改配置文件后重启activemq。

代码

// 使用nio模型的tcp协议生产者
private static final String ACTIVEMQ_URL = "tcp://192.168.227.129:61608";

// 使用nio模型的tcp协议消费者
private static final String ACTIVEMQ_URL = "tcp://192.168.227.129:61608";

// 使用nio模型的nio协议生产者
private static final String ACTIVEMQ_URL = "nio://192.168.227.129:61608";

// 使用nio模型的nio协议消费者
private static final String ACTIVEMQ_URL = "nio://192.168.227.129:61608";

// 以此类推

ActiveMQ的消息存储和持久化

介绍

  • MQ高可用:事务、可持久、签收,是属于MQ自身特性,自带的。这里要说的持久化是外力,是外部插件。之前讲的持久化是MQ的外在表现,现在讲的的持久是是底层实现

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

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

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

有哪些

  • 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

kahaDB消息存储

介绍

基于日志文件,从ActiveMQ5.4(含)开始默认的持久化插件。

官网文档:http://activemq.aache.org/kahadb

官网上还有一些其他配置参数。

<persistenceAdapter>
    <kahaDB directory="${activemq.data}/kahadb"/>
</persistenceAdapter>
<!-- 日志文件的存储目录在:$ACTIVEMQ_HOME/data/kahadb -->

说明

KahaDB是目前默认的存储方式,可以利用于任何场景,提高了性能和恢复能力。

消息存储使用一个事务日志和仅仅用一个索引文件来存储它所有的地址。

KahaDB是一个专门针对消息持久化存的解决方案,它对典型的消息使用模式进行了优化。

数据被追加到data logs中。当不再需要log文件中的数据的时候,log文件会被丢弃。

[root@localhost data]# pwd
/opt/activemq/apache-activemq-5.16.4/data
[root@localhost data]# ls
activemq.log  activemq.pid  activemq.pid.stop  audit.log  kahadb
[root@localhost data]# cd kahadb/
[root@localhost kahadb]# ls
db-1.log  db.data  db.redo  lock

KahaDB的存储原理

KahaDB在消息保存目录里面只有四类文件和一个lock锁,跟ActiveMQ的其它文件存储引擎相比就非常简洁了。

db-<Number>.log: KahaDB存消息到预定义大小的数据记录文件中,文件名命名为db-.log。当数据文件已满时,一个新的文件会随之创建,number数值也会随之递增,它随着消息数量的增多,如每32M一个文件,文件名按照数字进行编号,如db-1.log,db-2.log,db-3.log...。当不再有引用到数据文件中的任何消息时,文件会被删除或归档。

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

db.free: 记录db.data文件里面的空闲页面,存的是空闲页的id,方便建立索引时,先从空闲的开始建立保证索引的连续性,没有碎片。有时会被清除

db.redo: 进行消息恢复,如果KahaDB消息存储强制退出后启动,用于恢复B-Tree索引

lock: 文件锁,表示当前获取KahaDB读写权限的broker,类似mysql的悲观锁

JDBC消息存储

设置步骤

  1. 添加mysql数据库的驱动包到lib文件夹

    [root@localhost lib]# pwd
    /opt/activemq/apache-activemq-5.16.4/lib
    [root@localhost lib]# ls
    activemq-broker-5.16.4.jar        activemq-openwire-legacy-5.16.4.jar  camel                                        hawtbuf-1.11.jar                   jaxb-runtime-2.2.11.jar          web
    activemq-client-5.16.4.jar        activemq-protobuf-1.1.jar            extra                                        istack-commons-runtime-3.0.11.jar  jcl-over-slf4j-1.7.36.jar
    activemq-console-5.16.4.jar       activemq-rar.txt                     geronimo-j2ee-management_1.1_spec-1.0.1.jar  javax.activation-api-1.2.0.jar     mysql-connector-java-5.1.49.jar
    activemq-jaas-5.16.4.jar          activemq-spring-5.16.4.jar           geronimo-jms_1.1_spec-1.1.1.jar              jaxb-api-2.2.11.jar                optional
    activemq-kahadb-store-5.16.4.jar  activemq-web-5.16.4.jar              geronimo-jta_1.1_spec-1.1.1.jar              jaxb-core-2.2.11.jar               slf4j-api-1.7.36.jar
    
  2. jdbcPersistenceAdapter配置

    /opt/activemq/apache-activemq-5.16.4/conf路径下修改activemq.xml配置文件,按照如下路径:

    <persistenceAdapter>
        <!--<kahaDB directory="${activemq.data}/kahadb"/> -->
        <!-- 改为 82行 -->
        <jdbcPersistenceAdapter dataSource="#mysql-ds"  createTablesOnStartup="true"/> 
        <!-- dataSource指定将要引用的持久化数据库的bean名称,createTablesOnStartup是否在启动的时候创建数据表,默认为true
         这样每次启动都会去创建数据表了,一般是第一次启动的时候设置为true, 之后改为false. -->
    </persistenceAdapter>
    
  3. 数据库连接池配置

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

    <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://192.168.227.1:3306/db_activemq?relaxAutoCommit=true"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
        <property name="maxTotal" value="200"/>
        <property name="poolPreparedStatements" value="true"/>
    </bean>
    <!-- 在</broker>标签和<import>标签之间插入数据库连接池配置 127行 -->
    <!-- vim 命令模式 u:撤销 Ctrl + r:恢复撤销 -->
    

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

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

    如果无法启动,则查看data/activemq.log日志,如果是xml错误则仔细检查修改部分的单词和语法;
    如果显示如下错误,则开放mysql root 用户远程连接权限.
    远程连接Mysql服务器的数据库,错误代码是 1130,ERROR 1130: Host xxx.xxx.xxx.xxx  is not allowed to connect to this MySQL server null,  message from server: "Host 'DESKTOP-J2IEVTN' is not allowed to connect to this MySQL server"就是mysql无法给远程连接的用户权限。
    
    > mysql -u root -p 
    Enter password: ******
    mysql> use mysql;  
    mysql> select host,user from user where user ='root';  // 如果为 localhost 则改为 %
    mysql> update user set host = '%' where user ='root';  
    mysql> flush privileges;
    mysql> select host,user from user where user ='root';
    

    建库SQL和创表说明

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

    CREATE TABLE `activemq_acks` (
      `CONTAINER` varchar(250) NOT NULL, 
      `SUB_DEST` varchar(250) DEFAULT NULL,
      `CLIENT_ID` varchar(250) NOT NULL,
      `SUB_NAME` varchar(250) NOT NULL,
      `SELECTOR` varchar(250) DEFAULT NULL,
      `LAST_ACKED_ID` bigint(20) DEFAULT NULL,
      `PRIORITY` bigint(20) NOT NULL DEFAULT '5',
      `XID` varchar(250) DEFAULT NULL,
      PRIMARY KEY (`CONTAINER`,`CLIENT_ID`,`SUB_NAME`,`PRIORITY`),
      KEY `ACTIVEMQ_ACKS_XIDX` (`XID`)
    ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
    /* 
    activemq_acks表,用于存储订阅关系。如果是持久化Topic,订阅者和服务器的订阅关系在这个表保存。用于存储持久订阅的消息和最后				一个持久接收的消息ID
    CONTAINER: 消息的Destination
    SUB_DEST: 如果使用的是Static集群,这个字段会有集群其他系统的信息
    CLIENT_ID: 每个订阅者都必须有一个唯一的客户端ID用以区分
    SUB_NAME: 订阅者名称
    SELECTOR: 选择器,可以选择只消费满足条件的消息,条件可以用自定义属性实现,可支持多属性AND和OR操作
    LAST_ACKED_ID: 记录消费过消息的ID
    PRIORITY:优先级,默认5
    */
    
    CREATE TABLE `activemq_lock` (
      `ID` bigint(20) NOT NULL,
      `TIME` bigint(20) DEFAULT NULL,
      `BROKER_NAME` varchar(250) DEFAULT NULL,
      PRIMARY KEY (`ID`)
    ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
    /*
    activemq_locK表在集群环境中才有用,只有一个Broker可以获得消息,称为Master Broker,其他的只能作为备份等待,Master Broker不可用,才能成为下一个Master Broker.这个表记录哪个Broker是当前的Master Broker.  
    */
    
    CREATE TABLE `activemq_msgs` (
      `ID` bigint(20) NOT NULL,
      `CONTAINER` varchar(250) NOT NULL,
      `MSGID_PROD` varchar(250) DEFAULT NULL,
      `MSGID_SEQ` bigint(20) DEFAULT NULL,
      `EXPIRATION` bigint(20) DEFAULT NULL,
      `MSG` longblob,
      `PRIORITY` bigint(20) DEFAULT NULL,
      `XID` varchar(250) DEFAULT NULL,
      PRIMARY KEY (`ID`),
      KEY `ACTIVEMQ_MSGS_MIDX` (`MSGID_PROD`,`MSGID_SEQ`),
      KEY `ACTIVEMQ_MSGS_CIDX` (`CONTAINER`),
      KEY `ACTIVEMQ_MSGS_EIDX` (`EXPIRATION`),
      KEY `ACTIVEMQ_MSGS_PIDX` (`PRIORITY`),
      KEY `ACTIVEMQ_MSGS_XIDX` (`XID`),
      KEY `ACTIVEMQ_MSGS_IIDX` (`ID`,`XID`,`CONTAINER`)
    ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
    /* 
    activemq_msgs 消息表,queue和topic都存在里面
    ID:自增的数据库主键
    CONTAINER:消息的destination
    MSGID_PROD: 消息发送者的主键
    MSGID_SEQ:是发送消息的顺序,MSGID_PROD_MSG_SEQ可以组成JMS的MessageID
    EXPIRATION:消息的过期时间,存储的是从1970-01-01到现在的毫秒值
    MSG: 消息本体的Java序列化对象的二进制数据
    PRIORITY:优先级,从0-9,数值越大优先级越高
    */
    

queue验证和数据表变化

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

而且点对点类型中消息一旦被consumer消费就从broker中删除

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

我们使用queue模式持久化,发布6条消息后,发现ACTIVEMQ_MSGS数据表多了6条数据。

1 queue://aq1 ID:DESKTOP-19DRSCU-63437-1647009410025-1:1:1:1 1 0 (BLOB) 309 bytes 0
2 queue://aq1 ID:DESKTOP-19DRSCU-63437-1647009410025-1:1:1:1 2 0 (BLOB) 309 bytes 0
3 queue://aq1 ID:DESKTOP-19DRSCU-63437-1647009410025-1:1:1:1 3 0 (BLOB) 309 bytes 0
4 queue://aq1 ID:DESKTOP-19DRSCU-63437-1647009410025-1:1:1:1 4 0 (BLOB) 309 bytes 0
5 queue://aq1 ID:DESKTOP-19DRSCU-63437-1647009410025-1:1:1:1 5 0 (BLOB) 309 bytes 0
6 queue://aq1 ID:DESKTOP-19DRSCU-63437-1647009410025-1:1:1:1 6 0 (BLOB) 309 bytes 0

启动消费者,消费了所有的消息后,发现数据表的数据消失了。

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

topic验证和说明

先启动一下持久化topic的消费者。看到ACTIVEMQ_ACKS数据表多了一条消息

ACTIVEMQ_ACKS数据表,多了一个消费者的身份信息。一条记录代表:一个持久化topic消费者

topic://at1 topic://at1 marrry remark... 0 0

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

此时ACTIVEMQ_ACKS 消费了消息的订阅者LAST_ACKED_ID变为12,即该订阅者所消费到的最后消息的主键ID

总结

  • 如果是Queue

    在没有消费者消费的情况下会将消息保存到activemq_msgs表中,只要有任意一个消费者已经消费消费过了消息之后这些消息之后z这些消息将会被立即删除。

  • 如果是Topic

    一般先启动消费订阅然后再生产的情况下会将消息保存到activemq_acks.

在配置关系型数据库时有坑
1.数据库jar包
  记得需要将所用到的相关jar文件放置到ActiveMQ安装路径下的lib目录。mysql-jdbc驱动的jar包和对应的数据库连接池jar包
2.createTablesOnStartup属性
  在jdbcPersistenceAdapter标签中设置了createTablesOnStartup属性为true时在第一次启动ActiveMQ时,ActivemQ服务节点会自动创建所需要的数据表。启动完成后可以去掉这个属性或者改为false
3.下划线
  "java.lang.IllegalStateException:BeanFactory not initialized or already close"。这是因为您的操作系统的机器名中有"_"符号。请更改机器名并重启后解决问题。

如有不需要的订阅者或队列,Topic,请前往控制台删除

JDBC Message Store with ActiveMQ Journal

说明

这种方式克服了JDBC Store的不足,JDBC每次消息过来,都需要去写库读库。ActiveMQ Journal,使用高速缓存写入技术,大大提高了性能。当消费者的速度能够及时跟上生产者消息的生产速度时,journal文件能够大大减少需要写入到DB中的消息。

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

为了高性能,这种方式使用日志文件存储+数据库存储。先将消息持久到日志文件,等待一段时间再将未消费的消息持久到数据库。该方式要比JDBC性能要高

配置

下面是基于上面JDBC配置(/conf/activemq.xml),再做一点修改

<!-- 修改前 -->
<persistenceAdapter>
    <jdbcPersistenceAdapter dataSource="#mysql-ds"  createTablesOnStartup="true"/> 
</persistenceAdapter>

<!-- 修改后 -->
<persistenceFactory
     <journalPersistenceAdapterFactory 
		 journalLogFiles="4" 
		 journalLogFileSize="32768" 
		 useJournal="true"
		 dataSource="#mysql-ds" 
		 dataDirectory="activemq-data" />
</persistenceFactory>

总结

  1. jdbc效率低,kahaDB效率高,jdbc+Journal效率较高。

  2. 持久化消息主要指的是:MQ所在服务器宕机了消息不会丢试的机制

  3. 持久化机制演变的过程:

    从最初的AMQ Message Store方案到ActiveMQ V4版本退出的High Performance Journal(高性能事务支持)附件,并且同步推出了关于关系型数据库的存储方案。ActiveMQ5.3版本又推出了对KahaDB的支持(5.4版本后被作为默认的持久化方案),后来ActiveMQ 5.8版本开始支持LevelDB,到现在5.9提供了标准的Zookeeper+LevelDB集群化方案

  4. ActiveMQ消息持久化机制有

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

ActiveMQ多节点集群

大概步骤

  1. 环境和版本
  2. 关闭防火墙并保证win和linux可以互相ping通
  3. 要求具备zk集群并且可以成功启动
  4. 集群部署规划列表
  5. 创建3台集群目录
  6. 修改管理控制台端口
  7. hostname名字映射
  8. ActiveMQ集群配置
  9. 修改各节点的消息端口
  10. 按照顺序启动3个activeMQ节点,到这步前提是zk集群已经成功启动运行

目的
为了保证ActiveMQ的保证高可用,我们选择搭建集群

  • ZooKeeper: http://zookeeper.apache.org/
  • Replicated LevelDB Store: http://activemq.apache.org/replicated-Features/PersistenceFeatures/Persistence/Features/Persistence/leveldb-store
  • 集群: http://activemq.apache.org/replicated-leveldb-store ]( http://activemq.apache.org/replicated-leveldb-store

ActiveMQ集群如果工作

当Master宕机后,zookeper监听到没有心跳信息,则认为master宕机了,然后选举机制会从剩下的Slave中选出一个作为新的Master

实现

  • 搭建zookeper集群,搭建 activemq 集群

  • 新建activemq/cluster目录,将原始的解压文件复三份,修改端口(jetty.xml)

  • 增加IP到域名映射(/etc/hosts文件)

  • 修改为相同的brokername

  • 改为replica levelDB (3个都配,列举一个)

    <persistenceAdapter>
        <replicatedLevelDB
          directory="{activemq.data}/leveldb"
          replicas="3"
          bind="tcp://0.0.0.0:63631"
      zkAddress="localhost:2191,localhost:2192,localhost:2193"
          zkPassword="123456"
    	  sync="local_disk"
          zkPath="/activemq/leveldb-stores"
          hostname="wh-mq-server"
          />
      </persistenceAdapter>
    
  • 修改端口 02节点改为61617, 03 节点改为61618

  • 想要启动replica leavel DB 必须先启动所有的zookeper 服务,zookeper 的单机伪节点安装这里不细说了,主要说zookeper 复制三份后改配置文件,并让之自动生成 myid 文件,并将zk的端口改为之前表格中对应的端口.具体配置如下

tickTime=2000
initLimit=10
syncLimit=5
clientPort=2191    // 自行设置
server.1=192.168.17.3:2888:3888
server.2=192.168.17.3:2887:3887
server.3=192.168.17.3:286:3886
dataDir=/zk_server/data/log1    // 自行设置

批处理脚本 zk_batch.sh

#!/bin/sh             // 注意这个必须写在第一行

cd /zk_server/zk_01/bin
./zkServer.sh  start

cd /zk_server/zk_02/bin
./zkServer.sh  start

cd /zk_server/zk_03/bin
./zkServer.sh  start 

chmod  700    zk_batch.sh
命令即可让它变为可执行脚本,   ./zk_batch.sh   start  即可  (即启动了三个zk 的服务)

同理可以写一个批处理关闭zk 服务的脚本和 批处理开启mq 服务 关闭 mq 服务的脚本。

完成上述之后连接zk 的一个客户端

./zkCli.sh -server 127.0.0.1:2191

查看我的三个节点

查看我的节点状态:get /activemq/leveldb-stores/00000000003

此次验证表明 00000003 的节点状态是master (即为63631 的那个mq 服务) 而其余的(00000004 00000005) activemq 的节点是 slave

如此集群顺利搭建成功 !

此次测试表明只有 8161 的端口可以使用 经测试只有 61 可以使用,也就是61 代表的就是master

测试节点的可用性

首先

ActiveMQ的客户端只能访问Master的Broker,其余处于Slave的Broker不能访问,所以客户端连接的Broker应该使用failover协议(失败转移)

当一个ActiveMQ节点挂掉或者一个zookeeper节点挂掉,Active MQ服务依旧正常运转,如果仅剩一个ActiveMQ节点,由于不能选举Master,所以ActiveMQ不能正常运行;

同样的,如果Zookeeper仅剩一个节点活动,不管ActiveMQ各节点存活,ActiveMQ也不能正常提供服务(ActiveMQ集群的高可用依赖于Zookeeper集群的高可用)

// 修改代码
public static final String ACTIVEMQ_URL = "failover:(tcp://192.168.17.3:61616,tcp://192.168.17.3:61617,
tcp://192.168.17.3:61618)?randomize=false";

public static final String QUEUE_NAME = "queue_cluster";

生产者发送3条消息,看到控制台输出INFO | Successfully connected to tcp://192.168.17.3:61616

MQ服务收到3条消息,消费者启动, MQ服务也将消息出队,集群可正常使用

杀死 8061 端口的进程

killl -9 pid

刷新页面后 8161 端口宕掉,但是 8162 端口又激活了

重复刚才测试查看结果,至此,activeMQ集群可用性测试成功。

高级特性

异步投递

  • 异步投递是什么

    • ActiveMQ支持同步,异步两种发送的模式将消息发送到broker,模式的选择对发送延迟时有巨大的影响。producer能达到怎样的产出率(产出率=发送数据总量/时间)主要受发送延时的影响,使用异步发送可以显著的提要发送的性能。

      ActiveMQ默认使用异步发送的模式,除非明确指定使用同步发送的方式或者在未使用事务的前提下发送持久化的消息,这两种情况都是同步发送的。

      如果你没有使用事务且发送的是持久化消息,每一次发送都是同步发送的且会阻塞producer直到broker返回一个确认,表示消息已被安全的持久化到硬盘。确认机制提供了消息安全的保障,但同时会阻塞客户端带来了很大的延时。

      很多高性能的应用,允许在失败的情况下有少量的数据丢失。如果你的应用满足这个特点,你可以使用异步发送来提高生产率,即使发送的是持久化的消息。

    异步发送

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

    • 自我理解:此处的异步是指生产者和broker之间发送消息的异步。不是指生产者和消费者之间异步。

      官网介绍:http://activemq.apache.org/async-sends

      说明:对于一个Slow Consumer,使用同步发送消息可能出成Producer堵塞等情况,慢消费者适合使用异步发送。

  • 总结

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

    // 方式一
     private static final String ACTIVEMQ_URL = "tcp://118.24.20.3:61626?jms.useAsyncSend=true";
    // 方式二
    ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
    activeMQConnectionFactory.setUseAsyncSend(true);
    // 方式三
    Queue queue = session.createQueue(ACTIVEMQ_QUEUE_NAME);
    ActiveMQMessageProducer activeMQMessageProducer = (ActiveMQMessageProducer)session.createProducer(queue);
    
  • 异步发送如何确认发送成功

    异步发送丢失消息的场景是:生产者设置UserAsyncSend=true,使用producer.send(msg)持续发送消息。

    由于消息不阻塞,生产者会认为所有send的消息均会被成功发送至MQ。

    如果MQ突然宕机,此时生产者端内存中尚未被发送至MQ的消息会丢失。

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

    同步发送和异步发送的区别就在此,同步发送等send不阻塞了就表示一定发送成功了,异步发送需要接收回执并由客户端再来判断是否发送成功。

    public class Jms_TX_Producer {
    
        private static final String ACTIVEMQ_URL = "tcp://118.24.20.3:61626";
    
        private static final String ACTIVEMQ_QUEUE_NAME = "Async";
    
        public static void main(String[] args) throws JMSException {
            ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
            // 1
            activeMQConnectionFactory.setUseAsyncSend(true);
            Connection connection = activeMQConnectionFactory.createConnection();
            connection.start();
            Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
            Queue queue = session.createQueue(ACTIVEMQ_QUEUE_NAME);
            // 2
            ActiveMQMessageProducer activeMQMessageProducer = (ActiveMQMessageProducer)session.createProducer(queue);
            try {
                for (int i = 0; i < 3; i++) {
                    TextMessage textMessage = session.createTextMessage("tx msg--" + i);
                    textMessage.setJMSMessageID(UUID.randomUUID().toString()+"orderAtguigu");
                    final String  msgId = textMessage.getJMSMessageID();
                    // 3
                    activeMQMessageProducer.send(textMessage, new AsyncCallback() {
                        public void onSuccess() {
                            System.out.println("成功发送消息Id:"+msgId);
                        }
    
                        public void onException(JMSException e) {
                            System.out.println("失败发送消息Id:"+msgId);
                        }
                    });
                }
                System.out.println("消息发送完成");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                activeMQMessageProducer.close();
                session.close();
                connection.close();
            }
        }
    }
    
    • 控制台观察发送消息的信息:

      在对应的Queues或Topic页面点击操作栏Browse按钮即可查看到发送的消息的信息

延迟投递和定时投递

  • 介绍

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

  • 修改配置文件并重启

    <!-- 在activemq.xml添加如下位置添加  schedulerSupport="true" 之后重启activemq--> 
    </bean>
    
        <broker xmlns="http://activemq.apache.org/schema/core" brokerName="localhost" dataDirectory="${activemq.data}"  schedulerSupport="true" >
            
    <destinationPolicy>
    
  • 代码实现

    // ...
    long delay =  10*1000;
    long period = 5*1000;
    int repeat = 3 ;
    // ...
    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);
    // ...
    // 消费者和以前代码一样
    

消息消费的重试机制

  • 是什么

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

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

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

    • Client用了transactions且在session中调用了rollback
    • Client用了transactions且在调用commit之前关闭或者没有commit
    • Client再CLIENT_ACKNOWLEDGE的传递模式下,session中调用了recover
  • 请说说消息重发时间间隔和重发次数

    间隔:1

    次数:6

    每秒发6次

  • 有毒消息Poison ACK

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

  • 属性说明

    1. collisionAvoidanceFactor: 设置防止冲突范围的正负百分比,只有启动useCollisionAvoidance参数时才生效。也就是在延迟时间上再加一个时间波动范围。默认值为0.15
    2. maximumRedeliveries:最大重传次数,达到最大重连次数后抛出异常。为-1时不限次数,为0时表示不进行重传。默认值为6.
    3. maximumRedeliveryDelay:最大传送延迟,只有再useExponentialBackOff为true时才有效(V5.5),假设首次连接间隔为10ms,倍数为2,那么第二次重连时间间隔为20ms,第三次重连时间间隔为40ms,当重连时间间隔大于最大重连时间间隔时,以后每次重连时间间隔都为重连最大时间间隔。默认为-1。
    4. initialRedeliveryDelay:初始重发延迟时间,默认为1000L
    5. redeliveryDelay:重发延迟时间,当initialRedeliveryDelay=0时生效,默认为1000L
    6. useCollisionAvoidance:启动防止冲突功能,默认false
    7. useExponentialBackOff:启用指数倍数递增的方式增加延迟时间,默认为false
    8. backOffMultiplier:重连时间间隔递增倍数,只有值大于1和启用useExponentialBackOff参数时才生效,默认是5
  • 代码实现

    // 生产者。发送3条数据。代码省略.....
    // 消费者。开启事务,却没有commit。重启消费者,前6次都能收到消息,到第7次,不会再收到消息。代码:
    public class Jms_TX_Consumer {
        private static final String ACTIVEMQ_URL = "tcp://118.24.20.3:61626";
        private static final String ACTIVEMQ_QUEUE_NAME = "dead01";
    
        public static void main(String[] args) throws JMSException, IOException {
            ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
            Connection connection = activeMQConnectionFactory.createConnection();
            connection.start();
            final Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
            Queue queue = session.createQueue(ACTIVEMQ_QUEUE_NAME);
            MessageConsumer messageConsumer = session.createConsumer(queue);
            messageConsumer.setMessageListener(new MessageListener() {
                public void onMessage(Message message) {
                    if (message instanceof TextMessage) {
                        TextMessage textMessage = (TextMessage) message;
                        try {
                            System.out.println("***消费者接收到的消息:   " + textMessage.getText());
                            //session.commit();
                        }catch (Exception e){
                            e.printStackTrace();
                        }
                    }
                }
            });
            //关闭资源
            System.in.read();
            messageConsumer.close();
            session.close();
            connection.close();
        }
    }
    

    activemq管理后台。多了一个名为ActiveMQ.DLQ队列,里面多了3条消息。

  • 代码修改默认参数

    // 消费者。除灰色背景外,其他代码都和之前一样。修改重试次数为3。更多的设置请参考官网文档
    // ...
    ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
    // 修改默认参数,设置消息消费重试3次
    RedeliveryPolicy redeliveryPolicy = new RedeliveryPolicy();
    redeliveryPolicy.setMaximumRedeliveries(3);
    activeMQConnectionFactory.setRedeliveryPolicy(redeliveryPolicy);
    // ...
    
  • 整合spring

    <!-- 整合Spring后如何使用,假如工作需要 -->
    <!-- 定义ReDelivery(重发机制) -->
    <bean id="activeMQRedeliveryPolicy" class="org.apache.activemq.RedeliveryPolicy">
        <!-- 是否在每次尝试重新发送失败后,增加这个等待时间 -->
        <property name="useExponentialBackOff" value="true"></property>
        <!-- 重发次数,默认6次,这里设置3次 -->
        <property name="maximumRedeliveries" value="3"></property>
        <!-- 重发时间间隔,默认为1秒 -->
        <property name="initialRedeliveryDelay" value="1000"></property>
        <!-- 第一次失败后重新发送之前等待500ms,第二次失败再等待500*2毫秒,这里的2就是value -->
        <property name="backOffMultiplier" value="2"></property>
        <!-- 最大传送延迟,只有再useExponentialBackOff为true时才有效(V5.5),假设首次连接间隔为10ms,倍数为2,那么第二次重连		   时间间隔为20ms,第三次重连时间间隔为40ms,当重连时间间隔大于最大重连时间间隔时,以后每次重连时间间隔都为重连最大时          间间隔。默认为-1 -->
        <property name="maximumRedeliveryDelay" value="1000"></property>
    </bean>
    
    <!-- 创建连接工厂 -->
    <bean id="connectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
        <property name="brokerURL" value="tcp://localost:61616"></property>
        <!-- 引用重发机制 -->
        <property name="redeliveryPolicy" ref="activeMQRedeliveryPolicy"></property>
    </bean>
    

死信队列

  • 是什么

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

    死信队列:异常消息规避处理的集合,主要处理失败的消息。

    • ActiveMQ中引入了"死信队列"(Dead Letter Queue)的概念。即一条消息再被重发了多次后(默认是重发6次redeliveryCounter==6),将会被ActiveMQ移入"死信队列"。开发人员可在这个Queue中查看处理出错的消息,进行人工干预。

    • 死信队列的使用:处理失败的消息

      一般的生产环境中在使用MQ的时候设计两个队列:一个是核心业务队列,一个是死信队列。
      
      核心业务队列,就是比如订单系统核心业务队列用来发送订单消息的,然后另外一个死信队列就是用来处理消息情况的。
      
      假如第三方物流系统故障了,此时无法请求,那么仓库系统每次消费到一条订单消息,尝试通知发货和配送都会遇到对方的接口报错。此时仓库系统就可以看作把这条消息拒绝或标记为处理失败。一旦标志这条消息处理失败之后,MQ就会把这条消息转入提前设置好的一个死信队列中。
      
      然后你会看到就是,在第三方物流系统故障期间,所有订单消息全部处理失败,全部会转入死信队列。然后你的仓储系统得专门有一个后台线程,监控第三方物流系统是否正常,能否请求得,不停得监视。一旦发现对方恢复正常,这个后台线程就从死信队列消费出来处理失败的订单,重新执行发货和配送的通知逻辑。
      
  • 死信队列的配置(一般采用默认)

    1. sharedDeadLetterStrategy

      不管是queue还是topic,失败的消息都放到这个队列中。下面修改activemq.xml的配置,可以达到修改队列的名字。

      将所有的DeadLetter保存在一个共享的队列中,这是ActiveMQ broker端默认的策略。共享队列默认为“ActiveMQ.DLQ”,可以通过“deadLetterQueue”属性来设定。
      
      <deadLetterStrategy>
      	<sharedDeadLetterStrategy deadLetterQueue="DLQ-QUEUE"/>
      </deadLetterStrategy>
      
    2. individualDeadLetterStrategy

      可以为queue和topic单独指定两个死信队列。还可以为某个话题,单独指定一个死信队列

      把DeadLetter放入各自的死信通道中
      
      对于Queue而言,死信通道的前缀默认为“ActiveMQ.DLQ.Queue.”;
      
      对于Topic而言,死信通道的前缀默认为“ActiveMQ.DLQ.Topic.”;
      
      比如队列Order,那么它对应的死信通道为“ActiveMQ.DLQ.Queue.Order”。我们使用“queuePrefix”“topicPrefix”来指定上述前缀。
      
      默认情况下,无论是Topic还是Queue,broker将使用Quue来保存DeadLeader,即死信通道通常为Queue;不过开发者也可以指定为Topic。
      
      <policyEntry queue="order">
      	<deadLetterStrategy>
      		<individualDeadLetterStrategy queuePrefix="DLQ." useQueueForQueueMessages="false" />			</deadLetterStrategy>
      </policyEntry>
      
      将队列Order中出现的DeadLetter保存在DLQ.Order中,不过此时DLQ.Order为Topic。
      
      属性“useQueueForTopicMessages”,此值表示是否将Topic的DeadLetter保存在Queue中,默认为true。
      
    3. 自动删除过期消息

      过期消息是值生产者指定的过期时间,超过这个时间的消息

      有时需要直接删除过期的消息而不需要发送到死队列中,“processExpired”表示是否将过期消息放入死信队列,默认为true;
      
      <policyEntry queue= ">" >
      	<deadLetterStrategy>
      		<sharedDeadLetterStrategy processExpired= "false" />
      	</deadLetterStrategy>
      </policyEntry>
      
    4. 存放非持久消息到死信队列中

      默认情况下斤,Activemq不会把非持久的死消息发送到死信队列中。
      processNonPersistent”表示是否将“非持久化”消息放入死信队列,默认为false。
      
      非持久性如果你想把非持久的消息发送到死队列中,需要设置属性processNonPersistent=“true”
      
      <policyEntry queue= ">">
      	<deadLetterStrategy>
      		<sharedDeadLetterStrategy processNonPersistent= "true" />
      	</deadLetterStrategy>
      </policyEntry>
      

消息不被重复消费,幂等性

如何保证消息不被重复消费呢?幕等性问题你谈谈

之前我们学习Web框架阶段的,防止表单重复提交
这里表单 就可以类比为 message

网络延迟传输中,会造成进行MQ重试中,在重试过程中,可能会造成重复消费。

如果消息是做数据库的插入操作,给这个消息做一个唯一主键,那么就算出现重复消费的情况,就会导致主键冲突,避免数据库出现脏数据。

如果上面两种情况还不行,准备一个第三服务方来做消费记录。以redis为例,给消息分配一个全局id,只要消费过该消息,将<id,message>以
K-V形式写入redis。那消费者开始消费前,先去redis中查询有没消费记录即可。

幂等性如何解决,根据messageid去查这个消息是否被消费了

扩展

设置账号密码且通过账号密码登陆

1、第一步:,true:需要认证; false:不需要认证。

<bean id="securityConstraint" class="org.eclipse.jetty.util.security.Constraint">
   <property name="name" value="BASIC" />
   <property name="roles" value="user,admin" />
   <!-- set authenticate=false to disable login -->
   <property name="authenticate" value="true" />
</bean>

2、第二步:修改jetty-realm.properties文件

admin: admin123, admin
user: user, user

备注:第一个为用户名 第二个是密码 第三个是角色

activemq的API文档

http://activemq.apache.org/maven/apidocs/index.html

容错连接的问题

posted @ 2022-03-12 21:01  Zzzy君不见  阅读(379)  评论(0)    收藏  举报