java消息队列ActiveMQ

安装

前置条件

  • activemq的运行依赖于jdk,需要提前安装jdk
  • 如果已经安装了jdk,需要根据jdk的版本来选择对应的版本进行安装activemq
  • 版本对应在官网上,使用java -version 看jdk的版本
  • 注意:jdk和mq的版本不一致会报错,电脑的命名不能用中文

启动

  • 有的版本bin目录会有2个目录win32和win64,有的只有win64
  • 打开bin目录
  • 点击activemq.bat,访问网址 http://127.0.0.1:8161/admin/

在这里插入图片描述

使用服务进行启动

  • 上述方式使用bat文件启动,只要cmd窗口一关,就会关闭
  • 自带安装服务和卸载服务的bat文件(使用管理员权限运行)

在这里插入图片描述

关闭或开启验证

  • 更改完配置之后记得重启一下服务

在这里插入图片描述
在这里插入图片描述

消息中间件的应用场景

  • 异步处理
  • 应用解耦
  • 流量削峰

场景

用户注册需要三个功能:写数据库、发送邮件、注册短信

异步处理

串行

在这里插入图片描述

并行

在这里插入图片描述

消息中间件

在这里插入图片描述

对比

在这里插入图片描述

应用解耦

在这里插入图片描述

流量削峰

在这里插入图片描述

中间件对比

  • activemq:java、万级吞吐量、毫秒级响应速度、可用性高(主从架构),很多公司在用
  • rabbitmq:erlang,其他同上,管理界面丰富
  • rocketmq:java,10万级别,可用性很高(分布式)
  • kafka:消息查询和追溯没有提供,大数据应用广泛

jms消息模型

  • 两种模型
  • P2P(point to point):点对点模型(queue模型)
  • P/S(publish/subscribe):发布订阅模型(topic模型)

点对点

生产者消费者之间的消息往来

在这里插入图片描述

特点

  • 每个消息只能被一个消费者消费,消费之后就直接消失了
  • 生产者和消费者没有直接依赖关系,不管消费者有没有运行,都不妨碍生产者将消息发给队列
  • 消费者成功接受消息之后需要向队列回应应答成功

在这里插入图片描述

发布订阅模型

三个角色

  • 发布者
  • 订阅者
  • 主题
  • 发布者将消息发布到主题,只有订阅了主题的订阅者才能接收到消息
  • topic来实现消息的发布
  • 当一个消息被发布,n个订阅者都可以得到消息的拷贝

在这里插入图片描述

特点

  • 一个主题可以被多个订阅者订阅
  • 存在时间的先后顺序,消费者需要先订阅,生产者才能发布消息
  • 订阅者必须保持持续运行状态,才能接收到生产者的消息

JMS API

在这里插入图片描述

在这里插入图片描述

访问地址

  • http协议:http://127.0.0.1:8161/(网页监控)
  • tcp协议:tcp://127.0.0.1:61616(java后端访问)

JMS原始方式

点对点queue

引入依赖

  • log4j的依赖也需要引入一下
    <dependencies>
        <!--   引入原生的依赖  5.18版本不受activemq版本的影响   -->
        <dependency>
            <groupId>org.apache.activemq</groupId>
            <artifactId>activemq-all</artifactId>
            <version>6.1.0</version>
        </dependency>
       <!--   引入log4j的依赖,不引入会报错     -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.17.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.17.1</version>
        </dependency>
    </dependencies>

生产者

  • 后端使用的端口是tcp://localhost:61616
import jakarta.jms.*;
import org.apache.activemq.ActiveMQConnectionFactory;

// 队列生产者 点对点
public class Producer {
    public static void main(String[] args) throws JMSException {
        // 1. 创建连接工厂 默认使用tcp协议
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory("tcp://localhost:61616");
        // 2. 创建连接
        Connection connection = activeMQConnectionFactory.createConnection();
        // 3. 启动连接
        connection.start();
        // 4. 创建会话
        // 第一个参数:是否开启事务
        // 第二个参数:消息确认机制
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        // 5. 创建队列
        Queue queue = session.createQueue("queue01");
        // 6. 创建消息生产者
        MessageProducer producer = session.createProducer(queue);
        // 7. 创建消息 createTextMessage创建文本消息
        TextMessage textMessage = session.createTextMessage("Hello, ActiveMQ!");
        // 8. 发送消息
        producer.send(textMessage);
        // 9. 关闭资源 从后往前关闭
        session.close();
        connection.close();
    }
}

下图表示发送消息成功

在这里插入图片描述

消费者

一般方式(不常用)
  • 步骤和生产者如出一辙,就是最后变成了receive方法
  • 使用while true,使得消费者一直处在等待
// 点对点消费者
public class Consumer {
    public static void main(String[] args) throws JMSException {
        //1. 创建连接工厂
        ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://localhost:61616");
        //2. 创建连接
        Connection connection = connectionFactory.createConnection();
        //3. 启动连接
        connection.start();
        //4. 创建会话
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        //5. 指定队列
        Queue queue = session.createQueue("queue01");
        //6. 创建消息消费者
        MessageConsumer consumer = session.createConsumer(queue);
        //7. 接受并且消费消息
        //receive()是阻塞方法,如果没有消息会一直等待
        // 一般不会关闭消费者,因为一旦关闭就不能接收消息了
        while (true) {
            Message textMessage = consumer.receive();
            if (textMessage == null) {
                break;
            }
            //判断消息类型
            if (textMessage instanceof TextMessage) {
                TextMessage message = (TextMessage) textMessage;
                System.out.println("接收到消息:" + message.getText());
            }
        }

    }
}
监听器(推荐使用)

使用监听器,更加优雅

public class Listener {
    public static void main(String[] args) throws JMSException {
        //1. 创建连接工厂
        ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://localhost:61616");
        //2. 创建连接
        Connection connection = connectionFactory.createConnection();
        //3. 启动连接
        connection.start();
        //4. 创建会话
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        //5. 指定队列
        Queue queue = session.createQueue("queue01");
        //6. 创建消息消费者
        MessageConsumer consumer = session.createConsumer(queue);
        //7. 设置监听器 匿名内部类
        consumer.setMessageListener(new MessageListener() {
            // 重写onMessage方法 接收消息
            @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();
                    }
                }
            }
        });
    }
}

发布订阅模式

生产者

  • 和点对点十分相似,唯一不同使用的是createTopic
// 主题生产者
public class Producer {
    public static void main(String[] args) throws JMSException {
        // 1. 创建连接工厂 默认使用tcp协议
         ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory("tcp://localhost:61616");
        // 2. 创建连接
        Connection connection = activeMQConnectionFactory.createConnection();
        // 3. 启动连接
        connection.start();
        // 4. 创建会话
        // 第一个参数:是否开启事务
        // 第二个参数:消息确认机制
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        // 5. 创建主题
        Topic topic = session.createTopic("topic01");
        // 6. 创建消息生产者
        MessageProducer producer = session.createProducer(topic);
        // 7. 创建消息 createTextMessage创建文本消息
        session.createTextMessage("Hello, ActiveMQ!");
        // 8. 发送消息
        producer.send(topic, session.createTextMessage("Hello, ActiveMQ!"));
        // 9. 关闭资源 从后往前关闭
        session.close();
        connection.close();
    }
}

消费者

这里是引用

// 主题消费者
public class Consumer {
    public static void main(String[] args) throws JMSException {
        // 1. 创建连接工厂 默认使用tcp协议
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory("tcp://localhost:61616");
        // 2. 创建连接
        Connection connection = activeMQConnectionFactory.createConnection();
        // 3. 启动连接
        connection.start();
        // 4. 创建会话
        // 第一个参数:是否开启事务
        // 第二个参数:消息确认机制
        // 一般使用自动确认
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        // 5. 创建主题
        Topic topic = session.createTopic("topic01");
        // 6. 创建消息消费者
        MessageConsumer consumer = session.createConsumer(topic);
        // 7. 设置监听器 匿名内部类
        consumer.setMessageListener(new MessageListener() {
            // 重写onMessage方法 接收消息
            @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项目

  • 新建项目,导入依赖
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-activemq</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

配置application.yml

# 服务端口
server:
  port: 9001

spring:
  application:
    name: spring-active-mq-producer
  activemq:
    broker-url: tcp://localhost:61616
    user: admin
    password: admin
  jms:
    # 是否是发布订阅模式
    pub-sub-domain: false

aaa:
  bbb: ccc23

生产者

发送消息

测试


@SpringBootTest(classes = ActiveMqBootApplication.class)
class ActiveMqBootApplicationTests {

    @Autowired
    private JmsMessagingTemplate jmsMessagingTemplate;

    @Value("${aaa.bbb}")
    private String name;



    @Test
    void contextLoads() {
        System.out.println("Test");
        System.out.println(name);
    }

    @Test
    void testSend() {
        //参数一:目的地名称(队列或者主题名称)
        //参数二:消息
        jmsMessagingTemplate.convertAndSend(name, "hello, activemq");
    }

}

消费者

监听类位置

  • 把监听类放到启动类的同一目录下,或者开启包扫描模式

在这里插入图片描述

监听类

这个类被扫描到之后,会自动启动监听

// 加入到spring容器中
@Component
public class MyListener implements InitializingBean {

    @Value("#'{aaa.bbb}'")
    private String name;

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("name: " + name);
    }


    // 监听消息 监听的队列或者是主题
   @JmsListener(destination = "${aaa.bbb:ccc}")
   public void receiveMessage(Message message){
         if(message instanceof TextMessage){
              TextMessage textMessage = (TextMessage) message;
              try {
                System.out.println("接收到消息:"+textMessage.getText());
              } catch (Exception e) {
                e.printStackTrace();
              }
         }
   }
}

JMS消息组成

JMS协议组成如下

在这里插入图片描述

JMS Message 由三个部分组成

  • 消息头
  • 消息体
  • 消息属性

消息头

红色是重要的消息头

在这里插入图片描述

上述的属性:大部分是不可以修改的,只有少部分可以修改

  • JMSCorrelationID
  • JMSReplyTo
  • JMSType

红色的是修改成功的,绿色的是修改失败的

在这里插入图片描述

发送方更改属性

@Autowired
private JmsTemplate jmsTemplate;


@Test
void testSend2() {
    //参数一:目的地名称(队列或者主题名称)
    //参数二:消息
    jmsTemplate.send(name, session -> {
        Message message = session.createTextMessage("hello activemq");
        // 这些值都没法设置
        message.setJMSMessageID("CustomMessageID");
        message.setJMSExpiration(10000);
        message.setJMSPriority(9);

        // 设置消息属性
        message.setJMSCorrelationID("CustomCorrelationID");
        message.setJMSType("CustomType");
        message.setJMSReplyTo(session.createQueue("CustomReplyTo"));
        return message;
    });
}

接收方获取已经更改完毕的属性

    // 监听消息 监听的队列或者是主题
    @JmsListener(destination = "${aaa.bbb:ccc}")
    public void receiveMessage(Message message) {
        if (message instanceof TextMessage) {
            TextMessage textMessage = (TextMessage) message;
            try {
                //获取更改过的消息头信息
                System.out.println("消息ID:" + textMessage.getJMSMessageID());
                System.out.println("消息过期时间:" + textMessage.getJMSExpiration());
                System.out.println("消息优先级:" + textMessage.getJMSPriority());

                //获取消息属性
                System.out.println("消息属性:" + textMessage.getJMSCorrelationID());
                System.out.println("消息属性:" + textMessage.getJMSType());
                System.out.println("消息属性:" + textMessage.getJMSReplyTo());


                System.out.println("接收到消息:" + textMessage.getText());

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

有些属性已经更改完毕,有些没有

在这里插入图片描述

消息体

消息体有五中类型,其中三种比较常用

在这里插入图片描述

TextMessage类型

文本是最常见的消息类型了

    // 发送TextMessage
    @Test
    void testSendTextMessage() {
        jmsTemplate.send(name, session -> {
            Message message = session.createTextMessage("发送的是TextMessage");
            return message;
        });
    }

接收方

    @JmsListener(destination = "${aaa.bbb:ccc}")
    public void receiveMessage(Message message) {
        if (message instanceof TextMessage) {
            TextMessage textMessage = (TextMessage) message;
                System.out.println("接收到消息:" + textMessage.getText());
        }

    }

map类型(不常用)

发送方

// 发送mapMessage
@Test
void testSendMapMessage() {
    jmsTemplate.send(name, session -> {
        MapMessage message = session.createMapMessage();
        try {
            message.setString("name", "张三");
            message.setInt("age", 20);
        } catch (JMSException e) {
            e.printStackTrace();
        }
        return message;
    });
}

接收方

@JmsListener(destination = "${aaa.bbb:ccc}")
public void receiveMapMessage(Message message) {

    // 如果是MapMessage
    if (message instanceof MapMessage) {
        MapMessage mapMessage = (MapMessage) message;
        try {
            System.out.println("第一个参数:" + mapMessage.getString("name"));
            System.out.println("第二个参数:" + mapMessage.getInt("age"));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

ObjectMessage类型

object类型是不被信任的,发送之前先增加如下配置,否则会报错

spring:
  application:
    name: spring-active-mq-producer
  activemq:
    broker-url: tcp://localhost:61616
    user: admin
    password: admin
    packages:
      trust-all: true # 是否信任所有包

在这里插入图片描述

实体类(必须可序列化)

@Data
public class User implements Serializable {
    private String name;
    private Integer age;
}

发送方

    // 发送ObjectMessage
    @Test
    void testSendObjectMessage() {
        jmsTemplate.send(name, session -> {
            User user = new User();
            user.setName("张三");
            user.setAge(206);
            return session.createObjectMessage(user);
        });
    }

接收方
如果是ObjectMessage 需要强转 才能使用getObject方法

    // 监听消息 objectMessage
    @JmsListener(destination = "${aaa.bbb:ccc}")
    public void receiveObjectMessage(Object message) {
        if (message instanceof ObjectMessage) {
            // 如果是ObjectMessage 需要强转 才能使用getObject方法
            ObjectMessage objectMessage = (ObjectMessage) message;
            try {
                User user = (User) objectMessage.getObject();
                System.out.println("接收到消息:" + user);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

BytesMessage类型

  • 读取文件
  • 创建输入流
  • 使用输入流将文件写入到字节数组中
  • 将字节数组写入到 bytesMessage
// 发送bytesMessage
@Test
void testSendBytesMessage() {
    jmsTemplate.send(name, session -> {
         BytesMessage bytesMessage =   session.createBytesMessage();

         //1. 读取文件 获取当前项目根目录的text1.txt文件
        File file = new File("text1.txt");
        FileInputStream fileInputStream = null;

        //2 创建输入流
        try {
            fileInputStream = new FileInputStream(file);
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }

        //3. 读取文件内容
        byte[] bytes = new byte[(int) file.length()];
        try {
            fileInputStream.read(bytes);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        //4. 将字节流数组写入到bytesMessage
        try {
            bytesMessage.writeBytes(bytes);
        } catch (JMSException e) {
            throw new RuntimeException(e);
        }

        return bytesMessage;
    });
}

接受方的操作和发送方法反着来就行

  • getBodyLength获取长度创建字节数组
  • readBytes 将消息读取到字节数组
  // 监听消息 ByteMessage
  @JmsListener(destination = "${aaa.bbb:ccc}")
  public void receiveByteMessage(Message message) {
      if (message instanceof BytesMessage) {
          BytesMessage bytesMessage = (BytesMessage) message;
          try {
              // 1. 获取消息的长度 字节数组
              byte[] bytes = new byte[(int) bytesMessage.getBodyLength()];
              // 2. 读取消息
              bytesMessage.readBytes(bytes);
              // 3. 文件输出流
              FileOutputStream fileOutputStream = new FileOutputStream("text2.txt");
              // 4. 写入文件
              fileOutputStream.write(bytes);

          } catch (Exception e) {
              e.printStackTrace();
          }
      }

  }

StreamMessage类型(不常用)

TextMessage不同,能发送任何类型的消息

发送方

    // 发送StreamMessage
    @Test
    void testSendStreamMessage() {
        jmsTemplate.send(name, session -> {
            StreamMessage streamMessage = session.createStreamMessage();
            try {
                streamMessage.writeBoolean(true);
                streamMessage.writeLong(4L);
            } catch (JMSException e) {
                throw new RuntimeException(e);
            }
            return streamMessage;
        });
    }

接收方

    // 监听消息 streamMessage
    @JmsListener(destination = "${aaa.bbb:ccc}")
    public void receiveStreamMessage(Message message) {
        if (message instanceof StreamMessage) {
            StreamMessage streamMessage = (StreamMessage) message;
            try {
                System.out.println("第一个消息:" + streamMessage.readBoolean());
                System.out.println("第二个消息:" + streamMessage.readLong());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

消息属性

给消息增加一些标记

发送方

  • 方法 setStringProperty
// 消息属性
@Test
void testSendProperty() {
    jmsTemplate.send(name, session -> {
        TextMessage message = session.createTextMessage("发送的是TextMessage");
        // 设置消息属性
        message.setStringProperty("orderId", "1102");
        return message;
    });
}

接收方
-方法 getStringProperty

// 消息属性
@JmsListener(destination = "${aaa.bbb:ccc}")
public void receiveMessage(Message message) throws JMSException {
    if (message instanceof TextMessage) {
        TextMessage textMessage = (TextMessage) message;
        String orderId = message.getStringProperty("orderId");
        System.out.println("接收到消息:" + textMessage.getText());
        System.out.println("orderId: " + orderId);
    }
}

持久化方案

作用:保证消息不丢失
类型:有三种

  • 存储在内存中(效率高,重启服务器数据就没了)
  • 存储在日志文件中(khaDB是默认的存储方式)
  • 存储到数据库中(基于jdbc的存储方式)

生产者持久化的流程图

在这里插入图片描述

消费者持久化流程图

在这里插入图片描述

存储在日志文件中(默认存储方式)

delivery-mode有两个值

  • persistent :默认存储在日志文件中
  • non-persistent :存储在内存
spring:
  jms:
    # 是否是发布订阅模式
    pub-sub-domain: false
    template:
      delivery-mode: persistent # 持久化模式  非持久化(将消息保存在内存中)  持久化(将消息保存在磁盘中)

日志文件

在这里插入图片描述

存储在数据库中

开启持久化

spring:
  jms:
    # 是否是发布订阅模式
    pub-sub-domain: false
    template:
      delivery-mode: persistent # 持久化模式  非持久化(将消息保存在内存中)  持久化(将消息保存在磁盘中)

修改activemq.xml的配置

  • 新增bean
    <bean id="mysql-mq" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
            <!--如果是mysql5.7版本就写这个jdbc路径的url路径-->
            <!-- <property name="url" value="jdbc:mysql://127.0.0.1:3306/activemq?characterEncoding=utf-8&amp;serverTimezone=Asia/Shanghai" /> -->
            <!--如果是mysql8.0的版本就写这个jdbc的url路径-->
        <property name="url" value="jdbc:mysql://127.0.0.1:3306/activemq?useSSL=false&amp;serverTimezone=UTC" />
        <property name="username" value="root"/>
        <property name="password" value="root"/>
        <property name="initialSize" value="5"/>
        <property name="maxTotal" value="100"/>
        <property name="maxIdle" value="30"/>
        <property name="maxWaitMillis" value="10000"/>
        <property name="minIdle" value="1"/>
    </bean>

修改配置

<persistenceAdapter>
    <!-- <kahaDB directory="${activemq.data}/kahadb"/> -->
    <jdbcPersistenceAdapter dataSource="#mysql-mq" createTablesOnStartup="true" />
</persistenceAdapter>

引入mysql的jar包

在这里插入图片描述

新建activemq,数据库,重启activemq,就会多三个表

在这里插入图片描述

事务

  • 事务特性就是保持原子性,要么全部成功,要么全部失败
  • 这里的事务指的是生产者,要么信息全部发送到服务器,要不全部都不发送到服务器

在这里插入图片描述

新增配置类(生产者)

此配置类和启动类在一个目录下

// 配置类
@Configuration
public class ActiveMQConfig {

    // JMS事务管理器
    @Bean
    public JmsTransactionManager jmsTransactionManager(ConnectionFactory connectionFactory) {
        return new JmsTransactionManager(connectionFactory);
    }

}

注解使用

  • 在对应方法上使用注解@Transactional即可
  • 原生的写法是使用session.commit();session.rollback();
// 测试事务
// 不加事务的情况下,前面的消息发送成功,后面的消息发送失败
// 加了事务的情况下,前后的消息都不会发送成功,保持了原子性
// 如果是原生的JMS,需要手动提交事务 session.commit(); session.rollback();
// 如果是springboot的JMS,不需要手动提交事务,使用@Transactional注解即可
@Test
@Transactional
void testTransaction() {
    for (int i = 0; i < 10; i++) {
        if (i == 5) {
            throw new RuntimeException("出错了");
        }
        jmsTemplate.send(name, session -> {
            TextMessage message = session.createTextMessage("发送的是TextMessage");
            return message;
        });
    }



}

在消费方的使用

  • 消费方只能使用session.commit();session.rollback();
  • 回滚之后会再次从服务器发送给消费方,总共5次,5次之后,此消息加入死信队列

消息确认机制

事务确认机制只能二选一

在这里插入图片描述

  • 如果开启了事务,那么消息是自动确认
  • 如果没有开启事务,有三个值可以选择

在这里插入图片描述

配置类(接收方)

先关闭事务,开启手动确认

// 配置类
@Configuration
public class ActiveMQConfig {


    // 关闭事务
    @Bean("jmsListenerContainerFactory")
    public DefaultJmsListenerContainerFactory defaultJmsListenerContainerFactory(ConnectionFactory connectionFactory) {
        DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        // 关闭事务
        factory.setSessionTransacted(false);
        // 修改消息确认机制
//        factory.setSessionAcknowledgeMode(Session.CLIENT_ACKNOWLEDGE); // CLIENT_ACKNOWLED在原生的JMS中是有用的,但是在springboot中是没有用的,被修改过了
        factory.setSessionAcknowledgeMode(4); // 在springboot中,4代表的是手动确认 不要在使用Session.CLIENT_ACKNOWLED
        return factory;
    }


}

在监听器中指定工厂类

    // 确认机制 指向我们配置的配置类
    @JmsListener(destination = "${aaa.bbb:ccc}",containerFactory = "jmsListenerContainerFactory")
    public void receiveMessage(Message message, Session session) throws JMSException {
        if (message instanceof TextMessage) {
            TextMessage textMessage = (TextMessage) message;
            System.out.println("接收到消息:" + textMessage.getText());
            // 手动确认
            textMessage.acknowledge();

        }
    }

不进行手动确认的话,等待的消息会一直变多,不会在第二框中出列,会随着下一次发送消息再次发送

在这里插入图片描述

消息投递的方式

除了同步投递之外三种方式:异步投递、延时投递、定时投递

同步发送和异步发送

在这里插入图片描述

两个值的设定

在这里插入图片描述

在这里插入图片描述

在配置类中配置(发送方)

两者不要同时配置,只允许有一个JmsTemplate 的bean注入

// 配置类
@Configuration
public class ActiveMQConfig {


    // 配置用於異步發送的非持久化的jmstempalte(事务中)
    @Bean
    public JmsTemplate asynJmsTemplate(ConnectionFactory connectionFactory) {
        JmsTemplate jmsTemplate = new JmsTemplate();
        jmsTemplate.setConnectionFactory(connectionFactory);
        // 设置为非持久化
        jmsTemplate.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
        // 设置为异步发送
        jmsTemplate.setExplicitQosEnabled(true);
        return jmsTemplate;
    }


    // 配置用於同步發送的持久化的jmstempalte(默认的)
    @Bean
    public JmsTemplate synJmsTemplate(ConnectionFactory connectionFactory) {
        JmsTemplate jmsTemplate = new JmsTemplate();
        jmsTemplate.setConnectionFactory(connectionFactory);
        return jmsTemplate;
    }

}

延时投递

修改配置文件activemq.xml,新增 schedulerSupport=“true”

<broker 。。。。。。。dataDirectory="${activemq.data}"  schedulerSupport="true">

延迟投递

    // 延时投递
    @Test
    void testDelay() {
        jmsTemplate.send(name, session -> {
            TextMessage message = session.createTextMessage("发送的是TextMessage");
            // 设置延时投递 20秒之后消息才会到达服务器
            message.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY, 20 * 1000);
            return message;
        });
    }

定时投递

  • 这个地方直接使用 定时任务做吧

死信队列

死信队列,又名DLQ,全称:Dead Letter Queue,保存两种类型的信息:

  • 处理失败的信息
  • 过期的信息

有三种情况会出现死信队列:

  • 事务性的回话调用了rollbck方法
  • 事务性的回话在commit之前,被关闭
  • 回话被自定义成 CLIENT_ACKNOWLEDGE ,调用了recover函数

缺省情况下:

  • 持久化的消息才会被发送到DLQ,非持久化的消息不会发送到DLQ
  • 除非自定义,没有特别指定的话,所有的消息都会发送到 官方预定的死信队列:ACTIVEMQ.DLQ
  • 消息被重发的次数是6

在接受方 出错,服务器就会重发6次消息,间隔时间1s

演示死信队列

一定要开启事务,死信队列 就是为了重启activemq之后,能够对这部分数据进行处理

在接受方的配置类中进行事务管理类的配置

@Configuration
public class ActiveMQConfig {
//    // JMS事务管理器
    @Bean
    public JmsTransactionManager jmsTransactionManager(ConnectionFactory connectionFactory) {
        return new JmsTransactionManager(connectionFactory);
    }
}

// 测试死信队列
@JmsListener(destination = "${aaa.bbb:ccc}")
public void receiveMessage(Message message, Session session) throws JMSException {
    try {
        int i = 1 / 0;
        System.out.println("被监听到了");
        if (message instanceof TextMessage) {
            TextMessage textMessage = (TextMessage) message;
            System.out.println("接收到消息:" + textMessage.getText());
        }
    } catch (Exception e) {
        session.rollback();
        throw e; // 重新抛出异常以便上层处理
    }
}

因为调用了rollback方法,所以多了一个死信队列,里面存储了服务器发送给接收方失败的消息

在这里插入图片描述

为每一个队列单独配置死信队列

  • 所有队列共用一个死信队列,不容易找到对应队列发送失败的消息
  • 可以通过配置activemq.xml配置文件,为每一个队列都配置一个死信队列

新增如下配置:

<policyEntry queue=">" >
  <deadLetterStrategy>
    <individualDeadLetterStrategy queuePrefix="DLQ." useQueueForQueueMessages="true"/>
  </deadLetterStrategy>
</policyEntry>

在这里插入图片描述

重启activemq之后,重新触发死信队列就会看到一个基于 原队列的的新的死信队列

在这里插入图片描述

重发的策略也可以在配置类中记性配置

  • 这是接受方的配置类
  • 一定要开启事务
  • 配置RedeliveryPolicy 配置失效的话,将ActiveMQConnectionFactory 和JmsTemplate 也配置一下,确保真的使用的是我们定义的redeliveryPolicy

在这里插入图片描述

一般设置:重发间隔(默认1s),时间是否递增(默认不递增),次数(默认次数6)

// 配置类
@Configuration
public class ActiveMQConfig {

//    // JMS事务管理器
    @Bean
    public JmsTransactionManager jmsTransactionManager(ConnectionFactory connectionFactory) {
        return new JmsTransactionManager(connectionFactory);
    }

    @Bean
    public RedeliveryPolicy redeliveryPolicy() {
        RedeliveryPolicy redeliveryPolicy = new RedeliveryPolicy();
        // 设置是否在每次尝试重新发送失败后,增长重发延迟
        redeliveryPolicy.setUseExponentialBackOff(true);
        // 设置重发次数 10次
        redeliveryPolicy.setMaximumRedeliveries(10);
        // 设置重发间隔时间 1s
        redeliveryPolicy.setInitialRedeliveryDelay(1000);
        // 设置重发时间间隔递增 2倍
        redeliveryPolicy.setBackOffMultiplier(2);
        // 是否避免消息碰撞
        redeliveryPolicy.setUseCollisionAvoidance(false);
        //设置最大拖延时间是-1,表示没有最大拖延时间
        return redeliveryPolicy;
    }

    @Bean
    public ActiveMQConnectionFactory connectionFactory(RedeliveryPolicy redeliveryPolicy) {
        ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory();
        connectionFactory.setRedeliveryPolicy(redeliveryPolicy);
        return connectionFactory;
    }

    @Bean
    public JmsTemplate jmsTemplate(ActiveMQConnectionFactory connectionFactory) {
        return new JmsTemplate(connectionFactory);
    }
}

面试题

activemq服务器宕机问题如何解决

  • 使用activemq 主从集群方案:zookeeper集群 + replicated leveldb + activemq集群

在这里插入图片描述

如何防止消费方的消息被重复消费

因为网路延迟等原因,MQ无法及时接受消费方的确认消息,导致MQ重试,重试过程总就会导致消息被重复消费

解决思路

  • 如果是基于数据库的操作,将消息的id作为表的唯一主键进行存储,重复的的时候会触发冲突,避免脏数据的产生。
  • 如果不是基于数据库的的操作,可以借助第三方来判别消息是否已经被消费:
  • 使用redis,每次消息被消费完毕的时候,将当前消息的id作为key存储到redis
  • 消费前,使用消息的id查询redis中是否存在记录,记录存在,则说明已经被消费过了

如何防止消息丢失

  • 在消息的生产方和消费方都使用事务
  • 在消费方设置为手动确认(ACK)
  • 使用事务的基础上,对消息进行持久化:JDBC或者默认的日志文件

死信队列

  • 回头看
  • 死信队列的定义
  • 配置
  • 重发配置
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值