sheng的学习笔记-activeMQ框架原理

目录

搭建环境

如果需要修改访问端口的话

如果需要修改用户名和密码的话

重启 ActiveMQ

配置文件 activemq.xml

基础理论:

什么是 JMS

JMS基本开发步骤:

点对点模型(Point To Point)

发布订阅模型(Publish/Subscribe)

JMS结构:

消息结构

消息头

消息体

消息属性

代码:

QUEUE

生产者代码:

QUEUE消费者代码(同步阻塞监听):

QUEUE消费者代码(监听者方式)

注意

TOPIC

注意,发布订阅模式有以下特点:

生产者代码

消费者代码(监听者方式,同步阻塞见上面QUEUE的代码)

注意:

QUEUE和TOPIC对比

Spring和activeMQ整合代码

配置applicationContext.xml:

生产者代码:

消费者代码

监听器的消费者代码:

注意:

可靠性

消息持久化:

代码:

QUEUE 持久化代码

TOPIC 订阅模式的持久化:生产者代码

TOPIC 订阅模式的持久化:消费者代码

注意:

消息持久化机制:

KahaDB持久化

JDBC持久化

将activemq.xml的持久化配置改为:

JDBC With Journal持久化

JDBC持久化坑:

事务

开启事务代码:

生产者事务:

消费者事务:

完整生产者代码如下:

完整消费者代码如下:

签收

自动签收 Session.AUTO_ACKNOWLEDGE

手动签收 Session.CLIENT_ACKNOWLEDGE

允许重复消息签收 Session.DUPS_OK_ACKNOWLEDGE

特性

broker

使用指定的配置文件进行启动

用activemq来构建java应用---不依赖于ActiveMQ应用,只需要jar包即可实现

协议:

ActiveMQ支持哪些协议

默认协议:OpenWire 协议

NIO协议

AUTO协议

集群架构

主从集群架构

Pure Master Slave(纯主从)

Shared File System master/slaves

JDBC Master Slave

Replicated LevelDB Store (推荐的主从架构)

network


搭建环境

MQ的官网地址:ActiveMQ

下载后解压缩,开启命令:

bin\activemq start

 打开MQ的页面控制台 http://127.0.0.1:8161/admin/

用户名和密码都是admin,看到以下页面为启动成功:

 MQ启动完成

如果需要修改访问端口的话

修改 ActiveMQ 配置文件: /usr/local/activemq/conf/jetty.xml

配置文件修改完毕,保存并重新启动 ActiveMQ 服务。

如果需要修改用户名和密码的话

修改 conf/users.properties 配置文件.内容为: 用户名=密码

保存并重启 ActiveMQ 服务即可.

重启 ActiveMQ

/usr/local/activemq/bin/activemq restart

配置文件 activemq.xml

配置文件中,配置的是 ActiveMQ 的核心配置信息. 是提供服务时使用的配置. 可以修改启动的访问端口. 即 java 编程中访问ActiveMQ 的访问端口.

默认端口为 61616.

使用协议是: tcp 协议.

修改端口后, 保存并重启 ActiveMQ 服务即可.

ActiveMQ 目录介绍 :

从它的目录来说,还是很简单的:

* bin 存放的是脚本文件

* conf 存放的是基本配置文件

* data 存放的是日志文件

* docs 存放的是说明文档

* examples 存放的是简单的实例

* lib 存放的是 activemq 所需 jar 包

* webapps 用于存放项目的目录
 

基础理论:

什么是 JMS

JMS(Java Messaging Service)是 Java 平台上有关面向消息中间件的技术规范,它便于消息系统中的 Java 应用程序进行消息交换,并且通过提供标准的产生、发送、接收消息的接口,简化企业应用的开发。

在J2EE中,当两个应用程序使用JMS进行通信时,它们之间并不是直接相连的,而是通过一个共同的消息收发服务连接起来,可以达到解耦,异步,消峰的效果

active mq是实现了JMS标准的消息中间件,其中JMS技术况下如下图

JMS基本开发步骤:

MQ包含点对点模式(Queue)和发布订阅模式(Topic),如下图,

点对点模型(Point To Point)

生产者发送一条消息到 queue,只有一个消费者能收到。

发布订阅模型(Publish/Subscribe)

发布者发送到 topic 的消息,只有订阅了 topic 的订阅者才会收到消息。

JMS结构:

  • jms provider    消息中间件/消息服务器
  • jms producer   消息生产者
  • jms consumer  消息消费者
  • jms message   消息

消息结构

JMS客户端使用JMS消息与系统通讯,JMS消息虽然格式简单但是非常灵活, JMS消息由三部分组成:

消息头

JMS消息头预定义了若干字段用于客户端与JMS提供者之间识别和发送消息,预编译头如下:

– JMSDestination
– JMSDeliveryMode
– JMSMessageID
– JMSTimestamp
– JMSCorrelationID
– JMSReplyTo
– JMSRedelivered
– JMSType
– JMSExpiration
– JMSPriority

消息体

在消息体中,JMS API定义了五种类型的消息格式,让我们可以以不同的形式发送和接受消息,并提供了对已有消息格式的兼容。不同的消息类型如下:

TextMessage : javax.jms.TextMessage,表示一个文本对象。
ObjectMessage : javax.jms.ObjectMessage,表示一个JAVA对象。
BytesMessage : javax.jms.BytesMessage,表示字节数据。
StreamMessage :javax.jms.StreamMessage,表示java原始值数据流。
MapMessage : javax.jms.MapMessage,表示键值对。

消息属性

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

message.setStringProperty("Property",Property);    //自定义属性

代码:

QUEUE

生产者代码:

package com.example;

import org.apache.activemq.ActiveMQConnectionFactory;

import javax.jms.*;

public class JmsProduce {

    public static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61616";
    public static final String QUEUE_NAME = "queue01";

    public static void main(String[] args) throws JMSException {
        // 创建连接工厂,采用默认的用户名和密码
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);

        // 通过连接工厂,获取连接connection并启动访问
        Connection connection = activeMQConnectionFactory.createConnection();
        connection.start();

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

        //创建目的地(具体是队列还是主题topic)
        Destination destination = session.createQueue(QUEUE_NAME);

        // 得到消息生成者【发送者】
        MessageProducer messageProducer = session.createProducer(destination);

        // 通过message producer生产消息并发送到MQ队列中
        for (int i = 1; i <= 3; i++) {
            // 创建消息
            TextMessage message = session.createTextMessage("activemq message---" + i);
            // 发送消息到目的地方
            messageProducer.send(message);
        }

        // 关闭资源
        messageProducer.close();
        session.close();
        connection.close();
        System.out.println("*********消息发布到MQ完成");
    }
}

运行结果:

*********消息发布到MQ完成

到MQ的控制台,点击queue菜单看到3个待消费的消息:

 

QUEUE消费者代码(同步阻塞监听):

package com.example;

import org.apache.activemq.ActiveMQConnectionFactory;

import javax.jms.*;

public class JmsConsumer {

    public static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61616";
    public static final String QUEUE_NAME = "queue01";

    public static void main(String[] args) throws JMSException {
        // 创建连接工厂,采用默认的用户名和密码
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);

        // 通过连接工厂,获取连接connection并启动访问
        Connection connection = activeMQConnectionFactory.createConnection();
        connection.start();

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

        //创建目的地(具体是队列还是主题topic)
        Destination destination = session.createQueue(QUEUE_NAME);

        // 得到消息生成者【发送者】
        MessageConsumer messageConsumer = session.createConsumer(destination);
        // 同步阻塞方式
        while (true) {
            // 其中receive的参数如果没有设置值,一直等待,如果设置值,超过时间后退出
            TextMessage message = (TextMessage) messageConsumer.receive(4000L);
            if (null != message) {
                System.out.println("收到消息:" + message.getText());
            } else {
                break;
            }
        }

        // 关闭资源
        messageConsumer.close();
        session.close();
        connection.close();
    }
}

结果:

收到消息:activemq message---1
收到消息:activemq message---2
收到消息:activemq message---3

MQ的控制台:

表示有0个待消费消息,1个消费者正在连接,3个进队列数据,3个出队列数据

注意,1个消费者正在连接,是因为我消费者的receive函数没有设置结束时间,上述代码改为:

TextMessage message = (TextMessage) messageConsumer.receive(4000L);

表示4秒后没收到消息,退出

QUEUE消费者代码(监听者方式)

package com.example;

import org.apache.activemq.ActiveMQConnectionFactory;

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

public class JmsConsumerListener {

    public static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61616";
    public static final String QUEUE_NAME = "queue01";

    public static void main(String[] args) throws JMSException,IOException {
        // 创建连接工厂,采用默认的用户名和密码
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);

        // 通过连接工厂,获取连接connection并启动访问
        Connection connection = activeMQConnectionFactory.createConnection();
        connection.start();

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

        //创建目的地(具体是队列还是主题topic)
        Destination destination = session.createQueue(QUEUE_NAME);

        // 得到消息生成者【发送者】
        MessageConsumer messageConsumer = session.createConsumer(destination);

        messageConsumer.setMessageListener(new MessageListener() {
            @Override
            public void onMessage(Message message) {
                if (null != message && message instanceof TextMessage){
                    TextMessage textMessage = (TextMessage)message;
                    try {
                        System.out.println("收到消息:" + textMessage.getText());
                    }catch (JMSException e){
                        e.printStackTrace();
                    }
                }
            }
        });
        // 此处需要阻塞,否则监听器还没收到消息,就退出程序了
        System.in.read();

        // 关闭资源
        messageConsumer.close();
        session.close();
        connection.close();
    }
}

运行结果:

收到消息:activemq message---1
收到消息:activemq message---2
收到消息:activemq message---3
 

MQ的控制台:

注意

  • 如果将下面代码注释,会出现无法消费的现象,因为没等到监听器有作用,程序直接退出了,可以将下面代码注释后再跑一次
System.in.read();

  •  如果先启动2个消费者,再启动1个生产者,生产者发送的消息会被2个消费者平均消费(但每个消息只能被1个消费者消费),说明默认是负载均衡机制分发消息

TOPIC

注意,发布订阅模式有以下特点:

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

JMS规范允许客户创建持久订阅,即允许消费者消费它在未处于激活时发送的消息

生产者代码

package com.example.topic;

import org.apache.activemq.ActiveMQConnectionFactory;

import javax.jms.*;

public class JmsProduce_Topic {

    public static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61616";
    public static final String TOPIC_NAME = "topic01";

    public static void main(String[] args) throws JMSException {
        // 创建连接工厂,采用默认的用户名和密码
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);

        // 通过连接工厂,获取连接connection并启动访问
        Connection connection = activeMQConnectionFactory.createConnection();
        connection.start();

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

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

        // 得到消息生成者【发送者】
        MessageProducer messageProducer = session.createProducer(destination);

        // 通过message producer生产消息并发送到MQ队列中
        for (int i = 1; i <= 3; i++) {
            // 创建消息
            TextMessage message = session.createTextMessage("activemq topic message---" + i);
            // 发送消息到目的地方
            messageProducer.send(message);
        }

        // 关闭资源
        messageProducer.close();
        session.close();
        connection.close();
        System.out.println("*********消息发布到MQ完成");
    }
}

消费者代码(监听者方式,同步阻塞见上面QUEUE的代码)

package com.example.topic;

import org.apache.activemq.ActiveMQConnectionFactory;

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

public class JmsConsumerListener_Topic {

    public static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61616";
    public static final String TOPIC_NAME = "topic01";

    public static void main(String[] args) throws JMSException,IOException {
        // 创建连接工厂,采用默认的用户名和密码
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);

        // 通过连接工厂,获取连接connection并启动访问
        Connection connection = activeMQConnectionFactory.createConnection();
        connection.start();

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

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

        // 得到消息生成者【发送者】
        MessageConsumer messageConsumer = session.createConsumer(destination);

        messageConsumer.setMessageListener(new MessageListener() {
            @Override
            public void onMessage(Message message) {
                if (null != message && message instanceof TextMessage){
                    TextMessage textMessage = (TextMessage)message;
                    try {
                        System.out.println("收到topic消息:" + textMessage.getText());
                    }catch (JMSException e){
                        e.printStackTrace();
                    }
                }
            }
        });
        // 此处需要阻塞,否则监听器还没收到消息,就退出程序了
        System.in.read();

        // 关闭资源
        messageConsumer.close();
        session.close();
        connection.close();
    }
}

先启动消费者,再启动生产者,结果如下:

收到topic消息:activemq topic message---1
收到topic消息:activemq topic message---2
收到topic消息:activemq topic message---3

MQ的控制台结果:

注意:

  • 启动3个消费者,再启动生产者,会发现3个消费者都会收到所有的消息,MQ的控制台如下:

3个消费者连接,3个入队列,9个出队列(因为3个监听*3个消息=9个出队列)

  • 启动生产者,再启动3个消费者,会发现3个消费者都没有收到任何消息,MQ的控制台如下:

3个消费者连接,3个入队列,没有消息出队列

QUEUE和TOPIC对比

Spring和activeMQ整合代码

需要的JAR包找别的文章吧,上代码和配置

配置applicationContext.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:p="http://www.springframework.org/schema/p"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:amq="http://activemq.apache.org/schema/core"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsd
        http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.1.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
        http://activemq.apache.org/schema/core http://activemq.apache.org/schema/core/activemq-core.xsd">

    <context:component-scan base-package="com.example.springMQ">

    </context:component-scan>

    <!-- 配置ActiveMQConnectionFactory连接工厂对象 -->
    <amq:connectionFactory id="amqConnectionFactory" brokerURL="tcp://127.0.0.1:61616" userName="admin" password="admin"/>
    <!-- 配置connectionFactory的连接池信息 -->
    <bean id="pooledConnectionFactory" class="org.apache.activemq.pool.PooledConnectionFactory">
        <property name="connectionFactory" ref="amqConnectionFactory"></property>
        <property name="maxConnections" value="10"></property>
    </bean>
    <!-- 带有缓存功能的连接工厂,Session缓存大小可配置 -->
    <bean id="connectionFactory" class="org.springframework.jms.connection.CachingConnectionFactory">
        <property name="targetConnectionFactory" ref="pooledConnectionFactory"></property>
        <property name="sessionCacheSize" value="100"></property>
    </bean>

    <bean id="destinationTopic" class="org.apache.activemq.command.ActiveMQTopic">
        <constructor-arg index="0" value="srping-active-topic"/>
    </bean>
    <bean id="destinationQueue" class="org.apache.activemq.command.ActiveMQQueue">
        <constructor-arg index="0" value="srping-active-queue"/>
    </bean>

    <!-- 配置JmsTemplate -->
    <bean id="template" class="org.springframework.jms.core.JmsTemplate">
        <!-- 给定连接工厂, 必须是spring创建的连接工厂. -->
        <property name="connectionFactory" ref="connectionFactory"></property>
        <property name="defaultDestination" ref="destinationTopic"></property>
        <!-- 可选 - 默认目的地命名 -->
        <!--<property name="defaultDestinationName" value="tp_simple_queue"></property>-->
    </bean>
    <!-- 配置生产者Producer -->
    <!--<bean id="springProducer" class="com.cfang.amq.SpringProducer"/>-->

    <!-- 配置消费listener -->
    <bean  class="org.springframework.jms.listener.DefaultMessageListenerContainer">
        <property name="connectionFactory" ref="connectionFactory"></property>
        <!--<property name="destinationName" value="tp_simple_queue"/>-->
        <property name="destination" ref="destinationTopic"></property>
        <property name="messageListener" ref="springConsumer"></property>
        <property name="concurrentConsumers" value="1"/>
    </bean>
    <!-- 消费者 -->
    <bean id="springConsumer" class="com.example.springMQ.SpringMQ_Listener"/>
</beans>

生产者代码:

package com.example.springMQ;

import org.apache.xbean.spring.context.ClassPathXmlApplicationContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
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 SpringMQ_Produce {
    @Autowired
    private JmsTemplate jmsTemplate;

    public static void main(String[] args) {

        ApplicationContext ctx = new ClassPathXmlApplicationContext("/com/example/applicationContext.xml");
        SpringMQ_Produce produce = (SpringMQ_Produce)ctx.getBean("springMQ_Produce");
        produce.jmsTemplate.send(new MessageCreator() {
            @Override
            public Message createMessage(Session session) throws JMSException {
                TextMessage textMessage = session.createTextMessage("******spring和activemq整合case");
                return textMessage;
            }
        });

        System.out.println("************send task over");
    }
}

消费者代码

package com.example.springMQ;

import org.apache.xbean.spring.context.ClassPathXmlApplicationContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Service;

@Service
public class SpringMQ_Consumer {
    @Autowired
    private JmsTemplate jmsTemplate;

    public static void main(String[] args) {

        ApplicationContext ctx = new ClassPathXmlApplicationContext("/com/example/applicationContext.xml");
        SpringMQ_Consumer consumer = (SpringMQ_Consumer)ctx.getBean("springMQ_Consumer");

        String retValue = (String)consumer.jmsTemplate.receiveAndConvert();

        System.out.println("************消费者收到消息" + retValue);
    }
}

监听器的消费者代码:

package com.example.springMQ;

import org.springframework.stereotype.Service;

import javax.jms.*;

@Service
public class SpringMQ_Listener implements MessageListener{

    @Override
    public void onMessage(Message message) {
        if (null != message && message instanceof TextMessage){
            TextMessage textMessage = (TextMessage)message;
            try {
                System.out.println("收到消息:" + textMessage.getText());
            }catch (JMSException e){
                e.printStackTrace();
            }
        }
    }
}

注意:

配置监听器的就好了,上述的消费者(非监听器)是用于学习用的,另外applicationContext我是存放在producer的上层目录,真正的项目需要能找到spring的配置文件

有了监听器,不需要启动消费者,在生产者发送消息后就可以接收到消息

可靠性

主要内容有持久化,事务,签收

消息持久化:

为了避免意外宕机以后丢失信息,需要做到重启后可以恢复消息队列,消息系统一般都会采用持久化机制。

ActiveMQ的消息持久化机制有JDBC,AMQ,KahaDB和LevelDB,无论使用哪种持久化方式,消息的存储逻辑都是一致的。

就是在发送者将消息发送出去后,消息中心首先将消息存储到本地数据文件、内存数据库或者远程数据库等,然后试图将消息发送给接收者,发送成功则将消息从存储中删除,失败则继续尝试。

消息中心启动以后首先要检查指定的存储位置,如果有未发送成功的消息,则需要把消息发送出去。

消息持久化分为QUEUE和TOPIC

如图:

官网地址:ActiveMQ

代码:

QUEUE 持久化代码

默认是持久化消息,生产者发送后,消费者没有消费,MQ挂掉再启动,消费者可以消费消息。可以用下面代码控制持久化,默认是持久化消息

// 非持久化设置,MQ宕机后消息不存在
messageProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);

// 持久化设置,MQ宕机后消息不存在
messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);

TOPIC 订阅模式的持久化:生产者代码

package com.example.persist.topic;

import org.apache.activemq.ActiveMQConnectionFactory;

import javax.jms.*;

public class JmsProduce_Topic_Persist {

    public static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61616";
    public static final String TOPIC_NAME = "topic_persist";

    public static void main(String[] args) throws JMSException {
        // 创建连接工厂,采用默认的用户名和密码
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);

        // 通过连接工厂,获取连接connection并启动访问
        Connection connection = activeMQConnectionFactory.createConnection();


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

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


        // 得到消息生成者【发送者】
        MessageProducer messageProducer = session.createProducer(destination);
        connection.start();
        // 通过message producer生产消息并发送到MQ队列中
        for (int i = 1; i <= 3; i++) {
            // 创建消息
            TextMessage message = session.createTextMessage("activemq topic message---" + i);
            // 发送消息到目的地方
            messageProducer.send(message);
        }

        // 关闭资源
        messageProducer.close();
        session.close();
        connection.close();
        System.out.println("*********消息发布到MQ完成");
    }
}

TOPIC 订阅模式的持久化:消费者代码

package com.example.persist.topic;

import org.apache.activemq.ActiveMQConnectionFactory;

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

public class JmsConsumer_Topic_Persist {

    public static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61616";
    public static final String TOPIC_NAME = "topic_persist";

    public static void main(String[] args) throws JMSException,IOException {
        System.out.println("zhang3");
        // 创建连接工厂,采用默认的用户名和密码
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);

        // 通过连接工厂,获取连接connection并启动访问
        Connection connection = activeMQConnectionFactory.createConnection();
        connection.setClientID("zhang3");

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

        //创建目的地(具体是队列还是主题topic)
        Topic topic = session.createTopic(TOPIC_NAME);
        TopicSubscriber topicSubscriber = session.createDurableSubscriber(topic, "remark");

        connection.start();

        Message message = topicSubscriber.receive();
        while (null != message) {
            // 其中receive的参数如果没有设置值,一直等待,如果设置值,超过时间后退出
            TextMessage textMessage = (TextMessage)message;
            System.out.println("收到持久化topic消息:" + textMessage.getText());
            message = topicSubscriber.receive(50000L);
        }

        // 关闭资源
        session.close();
        connection.close();
    }
}

先启动消费者,设置订阅,MQ的控制台如下:

启动生产者

等到消费者超时退出后,再看MQ控制台转为线下:

注意:

干掉MQ后,重新启动,再发送消息,消费者也能收得到,一定要消费者先启动,做订阅

消息持久化机制:

上述讲述持久化的生产者和消费者代码,本章如何借助外部力量存放持久化的数据。ActiveMQ的消息持久化机制有JDBC,AMQ,KahaDB和LevelDB

  1. AMQ是一种文件存储形式,它具有写入速度快和容易恢复的特定,消息存储在一个个文件中,文件的默认大小为32M,当一个存储文件中的消息已经全部被消费,那么这个文件将被标识为可删除,在下一个清除阶段这个文件被删除,AMQ适用于ActiveMQ5.3之前的版本
  2. KahaDB消息存储(默认)。KahaDB是基于日志文件的持久性数据库,是自ActiveMQ 5.4以来的默认存储机制,可用于任何场景,提高了性能和恢能力,它是基于文件的本地数据库存储形式。它已针对快速持久性进行了优化。KahaDB使用较少的文件描述符,并提供比其前身AMQ消息存储更快的恢复。

  3. JDBC就是数据库,会比kahaDB慢一点,kahaDB是本地文件,JDBC会多出网络开销,但可靠性更高

  4. LevelDB,和KahaDB很像,也是基于文件的本地数据库存储形式,但比kahaDB更快,不使用自定义B-tree实现来索引预写日志,而是基于LevelDB的索引,这是未来的趋势,但目前默认的还是KahaDB

KahaDB持久化

消息存储是默认的持久化方式,看activemq.xml的持久化配置:

<!--
            Configure message persistence for the broker. The default persistence
            mechanism is the KahaDB store (identified by the kahaDB tag).
            For more information, see:

            http://activemq.apache.org/persistence.html
        -->
        <persistenceAdapter>
            <kahaDB directory="${activemq.data}/kahadb"/>
        </persistenceAdapter>

kahaDB的数据在\data\kahadb中,包含的文件:

文件的作用:

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

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

3、db.free文件表示当前db.data文件哪些页面是空闲的,文件具体内容是所有空闲页的ID。

4、db.redo文件是用来进行消息恢复,如果KahaDB消息存储在强制退出后启动,用于恢复BTree索引。

5、lock文件表示当前获得KahaDB读写权限的broker。

JDBC持久化

将activemq.xml的持久化配置改为:

<persistenceAdapter> 
  <!--createTablesOnStartup    启动是否创建表  第一次为true 后续为false-->
  <jdbcPersistenceAdapter dataSource="#my-ds" createTablesOnStartup="true"/> 
</persistenceAdapter>

数据库配置:

<!-- MySql DataSource Sample Setup --> 
  <!-- 
  <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://localhost/activemq?relaxAutoCommit=true"/> 
    <property name="username" value="activemq"/> 
    <property name="password" value="activemq"/> 
    <property name="poolPreparedStatements" value="true"/> 
  </bean> 
  --> 

弄好JAR包后启动会生成3个表:

activemq_msgs用于存储消息,Queue和Topic都存储在这个表中:

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

activemq_acks用于存储订阅关系。如果是持久化Topic,订阅者和服务器的订阅关系在这个表保存,主要的数据库字段如下:

  • CONTAINER:消息的Destination
  • SUB_DEST:如果是使用Static集群,这个字段会有集群其他系统的信息
  • CLIENT_ID:每个订阅者都必须有一个唯一的客户端ID用以区分
  • SUB_NAME:订阅者名称
  • SELECTOR:选择器,可以选择只消费满足条件的消息。条件可以用自定义属性实现,可支持多属性AND和OR操作
  • LAST_ACKED_ID:记录消费过的消息的ID。

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

JDBC With Journal持久化

For long term persistence we recommend using JDBC coupled with our high performance journal. You can use just JDBC if you wish but its quite slow

为了在ActiveMQ V4.x中实现持久消息传递的高性能,我们强烈建议您使用我们的高性能日志 - 默认情况下已启用。这很像一个数据库消息(以及transcation提交/回滚和消息确认)以尽可能快的速度写入日志 - 然后每隔一段时间我们将日志检查到长期持久性存储(在本例中为JDBC)。

它在使用队列时很常见,例如消息在发布后很快消耗掉; 因此,您可以发布10,000条消息,并且只有一些未完成的消息 - 因此,当我们检查JDBC数据库时,我们通常只有少量消息可以实际写入JDBC。即使我们必须将所有消息写入JDBC,我们仍然可以通过日志获得性能提升,因为我们可以使用大型事务批处理将消息插入JDBC数据库以提高JDBC端的性能。

JDBC With Journal方式克服了JDBC Store的不足,使用快速的缓存写入技术,大大提高了性能

JDBC With Journal方式,发送出来的消息会在内存中告诉缓存,接收端若在没有接收情况下7~10分钟后再写入数据库,这样接收端就不用等到数据库操作完了之后再接收消息。

JDBC Store和JDBC Message Store with ActiveMQ Journal的区别

  • Jdbc with journal的性能优于jdbc
  • Jdbc用于master/slave模式的数据库分享
  • Jdbc with journal不能用于master/slave模式
  • 一般情况下,推荐使用jdbc with journal

参考配置如下

<persistenceFactory>
      <journalPersistenceAdapterFactory journalLogFiles="5" dataDirectory="${basedir}/target" /> 
      <!-- To use a different dataSource, use the following syntax : --> 
      <!-- <journalPersistenceAdapterFactory journalLogFiles="5" dataDirectory="${basedir}/activemq-data" dataSource="#mysql-ds"/> --> 
    </persistenceFactory> 

JDBC持久化坑:

  • 如果是queue模式,在没有消费者的情况下会将消息保存到activemq_msgs表中,只要有任意一个消费者已经消费过,相应的消息将会立即被删除。
  • 如果是topic模式,一般是先启动消费订阅然后再生产的情况下会将消息保存到activemq_acks表中。
  • 若报错“java.lang.lllegalStateException:BeanFactory not initialized or already closed”这是因为操作系统的机器名中有“_”符号,更改机器名并且重启后即可解决。

事务

消息事务,是保证消息传递原子性的一个重要特征,和JDBC的事务特征类似。
一个事务性发送,其中一组消息要么能够全部保证到达服务器,要么都不到达服务器
生产者、消费者与消息服务器直接都支持事务性;
ActionMQ的事务主要偏向在生产者的应用。原理如下:

image-20201016100112087

开启事务代码

Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);

messageProducer.close();

session.commit(); // 这一行,必须要事务提交

session.close();

生产者事务

  • 如果SESSION的第一个参数事务是true,必须要提交,否则会出现下图

  • 在send后MQ没有收到消息(如果是false,自动提交)
  • 如果一个批次的消息,有一个失败了,可以进行回滚rollback

消费者事务

  • 代码和生产者一样,如果消费者事务是true,但没有commit,会出现重复消费(非事务一个消息消费一次后,不会再次消费),即消费一次后,再次启动消费者,还会消费到消息,如下图

完整生产者代码如下:

package com.example.queue;

import org.apache.activemq.ActiveMQConnectionFactory;

import javax.jms.*;

public class JmsProduce {

    public static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61616";
    public static final String QUEUE_NAME = "queue01";

    public static void main(String[] args) throws JMSException {
        // 创建连接工厂,采用默认的用户名和密码
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);

        // 通过连接工厂,获取连接connection并启动访问
        Connection connection = activeMQConnectionFactory.createConnection();
        connection.start();


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

        //创建目的地(具体是队列还是主题topic)
        Destination destination = session.createQueue(QUEUE_NAME);

        // 得到消息生成者【发送者】
        MessageProducer messageProducer = session.createProducer(destination);

        // 非持久化设置,MQ宕机后消息不存在
        //messageProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);

        // 持久化设置,MQ宕机后消息不存在
        //messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);

        try {
            // 通过message producer生产消息并发送到MQ队列中
            for (int i = 1; i <= 3; i++) {
                // 创建消息
                TextMessage message = session.createTextMessage("activemq message---" + i);
                // 发送消息到目的地方
                messageProducer.send(message);
            }

            // ok,session.commit;
            session.commit();
        } catch (Exception e) {
            e.printStackTrace();
            //error
            session.rollback();
        } finally {
            // 关闭资源
            messageProducer.close();
            session.close();
            connection.close();
            System.out.println("*********消息发布到MQ完成");
        }
    }


}

完整消费者代码如下:

package com.example.queue;

import org.apache.activemq.ActiveMQConnectionFactory;

import javax.jms.*;

public class JmsConsumer_Tx {

    public static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61616";
    public static final String QUEUE_NAME = "queue01";

    public static void main(String[] args) throws JMSException {
        // 创建连接工厂,采用默认的用户名和密码
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);

        // 通过连接工厂,获取连接connection并启动访问
        Connection connection = activeMQConnectionFactory.createConnection();
        connection.start();

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

        //创建目的地(具体是队列还是主题topic)
        Destination destination = session.createQueue(QUEUE_NAME);

        // 得到消息生成者【发送者】
        MessageConsumer messageConsumer = session.createConsumer(destination);
        // 同步阻塞方式
        while (true) {
            // 其中receive的参数如果没有设置值,一直等待,如果设置值,超过时间后退出
            TextMessage message = (TextMessage) messageConsumer.receive(4000L);
            if (null != message) {
                System.out.println("收到消息:" + message.getText());
            } else {
                break;
            }
        }

        // 关闭资源
        messageConsumer.close();
        session.commit();
        session.close();
        connection.close();
    }
}

签收

消费者客户端成功接收一条消息的标志是:这条消息被签收。
         消费者客户端成功接收一条消息一般包括三个阶段:

  1. 消费者接收消息,也即从MessageConsumer的receive方法返回
  2. 消费者处理消息
  3. 消息被签收

其中,第三阶段的签收可以有ActiveMQ发起,也可以由消费者客户端发起,取决于Session是否开启事务以及签收模式的设置。

  • 在带事务的Session中,消费者客户端事务提交之时,消息自动完成签收(事务配置覆盖签收配置,如果是手动签收,不写ack也会完成消费,如果事务回滚,消息会再次消费)。
  • 在不带事务的Session中,消息何时以及如何被签收取决于Session的签收模式设置

非事务Session可以设置如下几种签收模式:

  • 自动签收 Session.AUTO_ACKNOWLEDGE

当消息从MessageConsumer的receive方法返回或者从MessageListener接口的onMessage方法返回时,会话自动确认消息签收

Session session =connection.createSession(false, Session.AUTO_ACKNOWLEDGE);

  • 手动签收 Session.CLIENT_ACKNOWLEDGE

需要消费者客户端主动调用acknowledge方法签收消息,这种模式实在Session层面进行签收的,签收一个已经消费的消息会自动的签收这个Session已消费的所有消息:
   例如一个消费者在一个Session中消费了5条消息,然后确认第3条消息,所有这5条消息都会被签收

Session session =connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);

while(true) {
             TextMessage message=(TextMessage) consumer.receive();
             if(null != message ) {
                 System.out.println("收到消息:"+message.getText());
                 message.acknowledge();
          
             }else
                 break;
         }
 

  • 允许重复消息签收 Session.DUPS_OK_ACKNOWLEDGE

这种方式允许JMS不必急于确认收到的消息,允许在收到多个消息之后一次完成确认,与Auto_AcKnowledge相比,这种确认方式在某些情况下可能更有效,因为没有确认,当系统崩溃或者网络出现故障的时候,消息可以被重新传递. 
这种方式会引起消息的重复,但是降低了Session的开销,所以只有客户端能容忍重复的消息才可使用。(如果ActiveMQ再次传送同一消息,那么消息头中的JMSRedelivered将被设置为true)

特性

broker

broker相当于一个ActiveMq的服务器实例,就是实现了用代码的形式启动了ActiveMQ将MQ嵌入到JAVA代码中,以便随时用随时启动

使用指定的配置文件进行启动

在conf文件夹下拷贝activemq.xml为新文件activemq02.xml,然后使用命令

./activemq.bat start xbean:file:../conf/activemq02.xml

注意:

如果不指定file,也就是xbean:activemq2.xml,那么activemq2.xml必须在classpath目录下

打开控制台可以看到MQ界面

用activemq来构建java应用---不依赖于ActiveMQ应用,只需要jar包即可实现

这里主要是用Activemq Broker作为独立的消息服务器来构建Java应用。简单的说,就是在java应用中启动activemq。这种方式会以进程的方式启动一个新的JVM来支持连接。

嵌入式Broker启动

  下面的启动方式都不能通过http访问连接,要想测试是否启动成功只能通过收消息和发消息来测试。

代码:

package com.example.broker;

import org.apache.activemq.broker.BrokerService;

public class EmbeBroker {

    public static void main(String[] args) throws Exception {
        BrokerService brokerService = new BrokerService();
        brokerService.setUseJmx(true);
        brokerService.addConnector("tcp://localhost:61616");
        brokerService.start();
    }
}

启动broker后,启动生产者和消费者,可以看到正常使用MQ

协议:

ActiveMQ支持哪些协议

ActiveMQ支持多种协议传输和传输方式,允许客户端使用多种协议连接
ActiveMQ支持的协议:AUTO,OpenWire,AMQP,Stomp,MQTT等
ActiveMQ支持的基础传输方式:VM,TCP,SSL,UDP,Peer,Multicast,HTTP(S)等,以及更高级的Failover,Fanout,Discovery,ZerConf方式

地址:ActiveMQ

配置:在/conf/activemq.xml中,每个协议的?后面都是key value形式的内容,用于修饰协议参数。如图

配置语法:

协议://hostname:port?key=value

默认协议:OpenWire 协议

OpenWire 是 Apache 的一种跨语言协议,允许从不同的语言和平台访问 ActiveMQ,是 4.x 版本以后默认的传输协议,它为高速消息传递提供了高效的二进制格式。

OpenWire 支持 TCP、SSL、NIO、UDP、VM 等传输方式,直接配置这些连接,默认使用的就是 OpenWire 协议

注意,上述默认的都是BIO,为了提升速度,应使用NIO

NIO协议

原始NIO传输是使用OpenWire协议的tcp传输的替代品。NIO可以提供更好的性能,下述配置,是以TCP协议为基础的NIO网络IO模型

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

 别忘了改下spring中的url配置

如果要配置NIO+多协议,用以下配置

<transportConnectors>
    <transportConnector name="nio+ssl" uri="nio+ssl://0.0.0.0:61616"/>  
  </<transportConnectors>

AUTO协议

从5.13.0版本开始,ActiveMQ支持wire format协议检测,可以自动检测OpenWire,STOMP,AMQP和MQTT,允许为这4种类型的客户端共享一个传输。要通过NIO TCP连接配置ActiveMQ自动wire format检测,使用  auto+nio传输前缀
 


<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.SelectorManager.maximumPoolSize=50" />

查看MQ控制台配置:

activemq的配置:

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

        </transportConnectors>

生产者代码,参考上面queue的代码,改下url

public class JmsProduce {

//    public static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61616";
    public static final String ACTIVEMQ_URL = "nio://127.0.0.1:61608";
    public static final String QUEUE_NAME = "queue01";
。。。
}

消费者代码,参考上面queue代码,改下url

public class JmsConsumer {

//    public static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61616";
    public static final String ACTIVEMQ_URL = "nio://127.0.0.1:61608";
    public static final String QUEUE_NAME = "queue01";
。。。
}

看MQ控制台结果,生产3条消息,成功消费3条消息

上面的url,可以直接改为:

public static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61608";

注意,那个端口是auto+nio的配置61608 ,也就是只要有一个auto+nio的协议配置,在代码中可以使用nio,tcp等多个协议

集群架构

activemq的最经典的集群架构有:主从(MasterSlave),network

主从集群架构

Pure Master Slave(纯主从)

已经不推荐用了,了解一下局限即可

最简单最典型的master-slave模式,master与slave有各自的存储系统,不共享任何数据。master接收到的所有指令(消息生产,消费,确认,事务,订阅等)都会同步发送给slave。在slave启动之前,首先启动master,在master有效时,salve将不会创建任何transportConnector,即Client不能与slave建立链接;如果master失效,slave是否接管服务是可选择的。在master与slave之间将会建立TCP链接用来数据同步,如果链接失效,那么master认为slave离线。

对于持久化消息,将会采用同步的方式(sync)在master与slave之间备份,master接受到消息之后,首先发给slave,slave存储成功后,master才会存储消息并向producer发送ACK。

    当master失效后,slave有两种选择:

    1) 关闭:如果slave检测到master失效,slave实例关闭自己。此后管理员需要手动依次启动master、slave来恢复服务。

    2) 角色转换: slave将自己提升为master,并初始化transportConnector,此后Client可以通过failover协议切换到Slave上,并与slave交互。

 “Shared nothing”模式下,有很多局限性。master只能有一个slave,而且slave不能继续挂载slave。如果slave较晚的接入master,那么master上已有的消息不会同步给slave,只会同步那些slave接入之后的新消息,那也意味着slave并没有完全持有全局的所有消息;所以如果此时master失效,slave接入之前的消息有丢失的风险。如果一个新的slave接入master,或者一个失效时间较长的旧master接入新master,通常会首先关闭master,然后把master上所有的数据Copy给slave(或旧master),然后依次启动它们。事实上,ActiveMQ没有提供任何有效的手段,能够让master与slave在故障恢复期间,自动进行数据同步。

Shared File System master/slaves

基于共享文件系统的master/slaves模式,使用日志系统作为共享数据源

JDBC Master Slave

基于数据库的master/slaves模式,JDBC Store相对于日志文件而言,通常认为是低效的,尽管数据的可见性较好,但是database的扩容能力非常的弱,无法良好的适应在高并发、大数据情况下(严格来说,单组M-S架构是无法支持大数据的),况且ActiveMQ的消息通常存储时间较短,频繁的写入,频繁的删除,都是性能的影响点

对于文件系统和数据库做主从集群,讲述原理如下:

启动

当仅使用 JDBC 作为数据源时,您可以使用主从方法,运行任意数量的代理,如图所示。在启动时,一个主服务器在代理数据库中获取一个排他锁——所有其他代理都是从服务器并暂停等待排他锁。

客户端应该使用故障转移传输来连接到可用的代理。例如,使用类似于以下内容的 URL

failover:(tcp://broker1:61616,tcp://broker2:61616,tcp://broker3:61616)

只有主代理启动其传输连接器,因此客户端只能连接到主代理。

主故障

如果 master 失去与数据库的连接或失去排他锁,则它会立即关闭。如果 master 关闭或出现故障,其他 slave 之一将抢占锁,因此拓扑切换到下图

其他从站之一立即获取数据库上的排他锁,让他们开始成为主站,启动其所有传输连接器。

客户端与已停止的 master 断开连接,然后故障转移传输尝试连接到可用的 broker - 其中唯一可用的是新的 master

主机重启

您可以随时重新启动加入集群的其他代理,并作为从服务器启动,如果主服务器关闭或发生故障,则等待成为主服务器。所以下面的拓扑是在一个旧的 master 重启后创建的……

Replicated LevelDB Store (推荐的主从架构)

官网:ActiveMQhttps://activemq.apache.org/replicated-leveldb-store

基于复制的LevelDB Store,这是ActiveMQ全力打造的HA存储引擎,也是目前比较符合“Master-slave”架构模型的存储方案

复制的 LevelDB 存储使用 Apache ZooKeeper 从一组配置为复制 LevelDB 存储的代理节点中选择一个主节点。然后将所有从属 LevelDB 存储与主服务器同步,通过从主服务器复制所有更新来使它们保持最新状态。

复制的 LevelDB 存储使用与 LevelDB 存储相同的数据文件,因此您可以随时在复制和非复制之间切换代理配置。原理图:

使用ZooKeeper集群注册所有的ActiveMQ Broker。只有其中的一个Broker可以对外提供服务,被视为master。而其他的Broker处于待机状态,被视为slave。而此时slave只是做数据的主从同步。
如果master因故障而不能提供服务,ZooKeeper集群会从slave中选举出一个Broker充当 master。slave连接 master并同步它们的存储状态,slave不接受客户端连接。所有的存储操作都将被复制到连接至master的slave上。
如果master宕机了,得到了最新更新的slave会成为master。而故障节点在恢复后会重新加入到集群中并连接 master进入slave模式。
需要同步的消息操作都将等待存储状态被复制到其他节点的操作完成后才能完成
所以,如果你配置了replicas=3,那么法定大小是(3/2)+1=2。master将会存储并更新然后等待(2-1)=1个slave存储和更新完成,才汇报success。
至于为什么是2-1,熟悉ZooKeeper集群的应该知道,有一个node要作为观擦者存在。当一个新的master被选中,你需要至少保障一个法定node在线以能够找到拥有最新状态的node。这个node可以成为新的Master。因此,推荐运行至少3个replica nodes,以防一个node失败了,服务中断。(原理与zookeeper集群的高可用实现方式类似)。

另外注意:不要用JDBC+Journal模式搭建这种集群,附上别人的错误和解决方案:(原文见:https://www.iteye.com/blog/shift-alt-ctrl-2069250

故障发生时

    1、我们的HA,基于“Journal + JDBC”方式共享存储。基于Journal的原因是,希望能够提供更高的IO性能。

    2、某日,JDBC对应的数据库出现问题,触发ActiveMQ HA集群Failover,即原slave切换为master。

    3、切换之后,发现数据库中突增大量历史旧数据;这些旧数据导致业务系统数据异常

故障发生的原因

    1、Journal + JDBC方式:消息首先写入Journal日志文件(对于非事务性,将会立即转发给消费者(可以为异步)),内部通过定时调度线程每个一段时间(默认为5分钟)对日志文件进行checkpoint,并将日志中的消息批量 + 事务的方式写入MySql,checkpoint时间点之前的日志文件即可被删除。(Journal日志文件中, 保存有Message、也包括ACK的信息,当然对于ActiveMQ而言,ACK也是一种Message,所以在checkpoint期间,如果是ACK类型的消息,也将会从数据库中删除消息)

     这意味着,如果Master异常退出,那么在Journal日志中、且处于最近一次checkpoint之后的消息,将“丢失”;因为在Slave上是没有这些Journal日志数据的。

2、在ActiveMQ实例启动之后,正常对外服务之前,broker所做的第一件事,就是recovery,即将Journal日志进行一次全量的checkpoint。这意味着,无论它是master还是slave,如果本地Journal日志文件中有历史数据,都将会会被recovery。

    当在Failover发生时,如果原slave(它曾经是master)本地的journal日志文件中有历史消息,那么slave在提升为master之后,将会进行数据恢复,这些“历史消息”将会重新“复现”。

    3、Broker在正常关闭时,即通过“activemq stop”指令关闭时,关闭之前,将会对journal日志进行全量checkpoint,不会发生上述情况。

故障解决方案

    1、如果继续基于Journal + JDBC,或者基于Journal(比如kahaDB),我们原则上,不能支持Failover。在HA架构中,Master、slave角色将不能转换,如果master失效,我们所做的就是重启;如果master物理失效而无法恢复,此时才能将slave提升为master。不过为了Client代码的透明度,Client端的链接协议中仍然使用“failover”。

    2、如果抛弃Journal,直接使用JDBC,那么HA + failover都将可以支持,因为master和slave的数据视图只有DB一个,本地不会再存储数据。它的问题就是:消息的product效率将会大大降低。因为JDBC存储,是ActiveMQ中“性能最低”、“数据可靠性最高”的模式。

3、可以使用ActiveMQ官方提供的kahaDB + SAN(共享文件系统),由SAN提供文件排它锁决定M-S角色;同时M-S的数据视图均在SAN的共享文件中。这也可以帮助我们解决问题,而且性能较高;可维护性较高,支持Failover,也不会带来数据恢复的问题。

    4、ActiveMQ高级版本,已经移除了“原生的M-S”架构模式,也移除了“Replication LevelDB”架构模式。所以我们能选择的空间已经不大了。

    5、我们要求Producer端,在发送的消息中,都应该设置TTL参数,即表示消息的有效期;此值应该合理,无论如何,Consumer都不应该接收到过期的消息。(对于Broker而言,过期的消息将会传送给Consumer)

network

Network:这里可以理解为网络通信方式,也可以说叫Network of brokers。这种方式真正解决了分布式消息存储和故障转移、broker切换的问题。可以理解消息会进行均衡;从ActiveMQ1.1版本起,ActiveMQ支持networks of brokers。它支持分布式的queues和topics。一个broker会相同对待所有的订阅(subscription):不管他们是来自本地的客户连接,还是来自远程broker,它都会递送有关的消息拷贝到每个订阅。远程broker得到这个消息拷贝后,会依次把它递送到其内部的本地连接上。

每个主从组成一个group,多个group通过通讯协议进行数据同步,达到多个group提供集群对外提供服务

在这里插入图片描述

 架构思考:Network集群模型的关键点:

  • 这种方案需要两套或多套(Master-Slave)的集群模型才可以搞定,部署非常麻烦,需要两套或多套集群直接相互交叉配置,相互间能够感知到彼此的存在
  • Network虽然解决了分布式消息队列这个难题,但是还有很多潜在的问题,最典型的就是资源浪费问题,并且也可能达不到所预期的效果;通常采用Master-Slave模型是传统型互联网公司的首选
  • 无分片功能。由于 ActiveMQ 是伟企业级开发设计的消息中间件,初衷并不是为了 处理海量消息和高并发请求。如果一台服务器不能承受更多消息,则需要横向 拆分。ActiveMQ 官方不提供分片机制,需要自己实现,而且也不支持负载均衡,所以吞吐量没有kafka大

参考文章:

尚硅谷是学习内容的主要来源

尚硅谷ActiveMQ教程(MQ消息中间件快速入门)_哔哩哔哩_bilibili

ActiveMQ集群架构与原理解析_柏~的博客-CSDN博客_activemq架构

https://segmentfault.com/a/1190000014592517

【消息队列----ActiveMQ】基本原理_Sunny-CSDN博客_activemq原理

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值