消息中间件 JMS 规范

  • 这篇笔记,主要是基于尚硅谷的ActiveMQ课程,以及这位大佬的笔记
  • 本人初学,在一边学的基础上,一边加上自己的理解写一些笔记,如果有写得不好的地方,请多担待。

1. 基本概念

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

2. 消息头

  • 消息头包含一些消息的属性,比如说消息的id、消息是否持久化、消息的过期时间、消息的优先级等等。
    • JMSDestination:消息目的地
    • JMSDeliveryMode:消息持久化模式
    • JMSExpiration:消息过期时间
    • JMSPriority:消息的优先级
    • JMSMessageID:消息的唯一标识符
  • 按理来说,是可以自定义每个message的这些属性的,但是经过本人的多次测试以及网上的查证,的确没办法修改message的这些头属性。
    TextMessage textMessage = session.createTextMessage("topic_name--" + i);
    // 这里可以指定每个消息的目的地
    textMessage.setJMSDestination(topic);
    /*
    持久模式和非持久模式。
    一条持久性的消息:应该被传送“一次仅仅一次”,这就意味着如果JMS提供者出现故障,该消息并不会丢失,它会在服务器恢复之后再次传递。
    一条非持久的消息:最多会传递一次,这意味着服务器出现故障,该消息将会永远丢失。
     */
    message.setJMSDeliveryMode(DeliveryMode.PERSISTENT);  // 默认情况,持久化
    message.setJMSDeliveryMode(DeliveryMode.PERSISTENT); // 非持久化
    /*
    可以设置消息在一定时间后过期,默认是永不过期。
    消息过期时间,等于Destination的send方法中的timeToLive值加上发送时刻的GMT时间值。
    如果timeToLive值等于0,则JMSExpiration被设为0,表示该消息永不过期。
    如果发送后,在消息过期时间之后还没有被发送到目的地,则该消息被清除。
     */
    textMessage.setJMSExpiration(1000);
    /*  消息优先级,从0-9十个级别,0-4是普通消息5-9是加急消息。
    JMS不要求MQ严格按照这十个优先级发送消息但必须保证加急消息要先于普通消息到达。默认是4级。
     */
    textMessage.setJMSPriority(10);
    // 唯一标识每个消息的标识。MQ会给我们默认生成一个,我们也可以自己指定。
    textMessage.setJMSMessageID("ABCD");
    // 上面有些属性在send方法里也能设置
    messageProducer.send(textMessage);
    
  • 然而,在发送消息的时候,可以顺带修改几个属性。且本人查证,修改有效。
    在这里插入图片描述
    producer.send(message, DeliveryMode.NON_PERSISTENT, 9, 10000);// 生产者负责将消息对象发送出去
    

3. 消息体

  • 也就是消息的正文部分,真正需要被传递的内容。
  • 一共有五种消息体格式
    在这里插入图片描述

3.1 例子

  • TextMessage 之前一直在使用,如今以MapMessage 为例做一个简单的测试。
  • 生产者
      @Test
      public void test2() throws JMSException {
        Queue queue = session.createQueue(DESTINATION_NAME);
        producer = session.createProducer(queue);
    
        MapMessage mapMessage = session.createMapMessage();
        mapMessage.setInt("int", 10);
        mapMessage.setBoolean("boolean", true);
        mapMessage.setString("string", "string");
        producer.send(mapMessage);
        System.out.println("发送完毕");
      }
    
  • 消费者
      @Test
      public void test2() throws JMSException, InterruptedException {
        Queue queue = session.createQueue(DESTINATION_NAME);
        consumer = session.createConsumer(queue);
        consumer.setMessageListener((message) -> {
          if (message instanceof MapMessage) {
            MapMessage mapMessage = (MapMessage) message;
            try {
              System.out.println("int:" + mapMessage.getInt("int"));
              System.out.println("boolean:" + mapMessage.getBoolean("boolean"));
              System.out.println("string:" + mapMessage.getString("string"));
            } catch (JMSException e) {
              e.printStackTrace();
            }
          }
        });
        Thread.sleep(1000L);
      }
    

4. 消息属性

  • 其实就是可以自定义一些属性,通过类似于kv键值对的方式传递过去。
  • 他是识别/去重/重点标注等操作,非常有用的方法。
  • 基本上涵盖了所有的基本数据类型
    在这里插入图片描述

4.1 例子

  • 下面是一个简单的例子
  • 生产者
      @Test
      public void test3() throws JMSException {
        Queue queue = session.createQueue(DESTINATION_NAME);
        producer = session.createProducer(queue);
        TextMessage textMessage = session.createTextMessage("textMessage");
        textMessage.setIntProperty("int_property", 10);
        textMessage.setBooleanProperty("boolean_property", false);
        textMessage.setStringProperty("string_property", "string");
        producer.send(textMessage);
        System.out.println("发送完毕");
      }
    
  • 消费者
      @Test
      public void test3() throws JMSException, InterruptedException {
        Queue queue = session.createQueue(DESTINATION_NAME);
        consumer = session.createConsumer(queue);
        consumer.setMessageListener((message) -> {
          if (message instanceof TextMessage) {
            TextMessage textMessage = (TextMessage) message;
            try {
              System.out.println("getText: " + textMessage.getText());
              System.out.println("getBooleanProperty: " + textMessage.getBooleanProperty("boolean_property"));
              System.out.println("getIntProperty: " + textMessage.getIntProperty("int_property"));
              System.out.println("getStringProperty: " + textMessage.getStringProperty("string_property"));
            } catch (JMSException e) {
              e.printStackTrace();
            }
          }
        });
        Thread.sleep(1000L);
      }
    

5. 消息的持久化

  • 消息持久化,当消息生产者将消息发送给消息中间件之后,消息中间件将它保存在磁盘或者其他可以永久保存消息的地方。即使服务器宕机、关机或者其他意外,也能从磁盘中将消息恢复回来的手段。
  • 队列 queue 和主题 topic 的持久化方式又有点不一样,下面各自展示例子。

5.1 queue消息非持久和持久

  • queue默认就是持久化的,就是说,当queue消息传给消息中间件之后,就会将queue进行持久化。即使关机之后再开机,消费者依旧能够取出之前没有取的消息。
  • 可以这么理解,因为queue消息是点对点的,对于可靠性要求会比较高,因此保险起见,Active MQ选择queue默认持久化的。
  • 而设置queue是否为持久化,很简单,只需要设置生产者MessageProducer即可
    producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);  // 将生产者设置为非持久的
    producer.setDeliveryMode(DeliveryMode.PERSISTENT);  // 将生产者设置为持久的
    

在这里插入图片描述

5.2 topic消息的非持久和持久

  • 而topic的持久化就比较复杂了,默认topic是非持久化的。
  • 由之前的实验可以知道,需要订阅者首先在线,发布者再发布消息,订阅者才可以收到消息。否则,订阅者后面再订阅是无法收到消息的。
  • 而topic的持久化模式,与非持久化很类似,也需要订阅者提前先订阅好主题。但是,订阅者订阅好主题之后,可以下线,因为该主题的订阅已经被持久化了,谁订阅的也被持久化了。因此,等待发布者发布的时候,无论订阅者在不在,发布者的发布topic消息都可以保存在该主题下,并且持久化。等待订阅者再次上线的时候,就可以接收到该消息了。
  • 经本人实验证明,即使是服务器重启,该topic消息依旧存在。

5.2.1 发布者代码

  • 发布者和上面queue消息的发布者设置一样,就是将生产者MessageProducer设置为持久化的。
    ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
    Connection connection = activeMQConnectionFactory.createConnection();
    connection.start();   // 开始连接
    Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
    Topic topic = session.createTopic(DESTINATION_NAME);
    MessageProducer producer = session.createProducer(topic);
    
    // 以往都是直接开启连接,这次要拿了producer,设置为持久化,才能开启连接?
    producer.setDeliveryMode(DeliveryMode.PERSISTENT);     // 设置为持久化的。重点!!!
    // connection.start();   // 开始连接
    

    有的老师说要将connection.start()放到下面的位置,但本人测试发现,不需要放到下面与往常一样直接开启连接也能有效果。

5.2.2 订阅者代码

  • 最主要的两个操作是,首先在要给自己的连接设置一个客户端id,用于设置连接;还有就是,这次不生成Consumer了,而是生成TopicSubscriber订阅者,并且给订阅的主题取一个名字。
    ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
    Connection connection = activeMQConnectionFactory.createConnection();
    connection.setClientID("Marry"); // 给自己的客户端起一个名字,用于标示
    connection.start();  // 开启连接
    
    Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
    Topic topic = session.createTopic(DESTINATION_NAME);
    TopicSubscriber subscriber = session.createDurableSubscriber(topic, "subscriber");  // 这次不是注册消费者了
    // connection.start();  // 开启连接
    

    有的老师说要将connection.start()放到下面的位置,但本人测试发现,不需要放到下面与往常一样直接开启连接也能有效果。

  • 要首先让订阅者启动一次。订阅成功之后,就会在订阅者列表中,找到刚刚订阅的内容。如果下线了,就会跑到offline那个列表里面。
    在这里插入图片描述
  • 在发布者发布之后,就可以获取到消息了。
  • 至此,完成了topic方面的持久化操作。

6. 消息的事务性

  • 在获取会话的时候就会设置本会话时候采用事务。而之前的设置都是设置为false,默认每次都是自动提交。
    session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
    
  • 生产者开启事务后,执行commit方法后,这批消息才算真正被提交。不执行commit方法,这批消息不会提交。如果执行rollback方法,之前的消息会被回滚掉。
  • 生产者的事务机制要高于签收机制,当生产者开启事务,签收机制就不重要了。
  • 消费者开启事务之后,执行commit方法,这批消息才算是真正被消费。不执行commit,下次还能消费。使用rollback就更加离谱了,会不断地给客户端推送同样的消息,直到让消息成为死信。(也就是说,消费者事实上可以白嫖几次同一条消息哈哈)反正最好不要给消费者rollback,不然很容易出事。
  • 事实上,事务的使用更偏向于生产者,签收机制的使用更偏向于消费者。

6.1 生产者代码

  @Test
  public void test1() throws JMSException {
    session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
    Queue queue = session.createQueue(DESTINATION_NAME);
    producer = session.createProducer(queue);
    try {
      for (int i = 0; i < 4; i++) {
        TextMessage textMessage = session.createTextMessage(String.valueOf(i));
        producer.send(textMessage);
        if (i == 2) throw new RuntimeException("中间报一些异常");  // 如果中间发生一些异常,事务就没办法提交,那就没办法将所有消息发送
      }
      session.commit();
      System.out.println("消息发送正常");
    } catch (Exception e) {
      System.out.println("发送失败");
      session.rollback();
      e.printStackTrace();
    }
  }
  • 上面的代码,当执行到中间出现了异常,就会直接跳到catch里面,不会执行commit方法,因此所有消息都不会发送成功。

6.2 消费者代码

  @Test
  public void test2() throws Exception {
    session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
    Queue queue = session.createQueue(DESTINATION_NAME);
    consumer = session.createConsumer(queue);
    while (true) {
      Message message = consumer.receive();
      if (message instanceof TextMessage) {
        TextMessage textMessage = (TextMessage) message;
        String text = textMessage.getText();
        System.out.println("获得消息" + text);
        session.commit();
      } else {
        break;
      }
    }
  • 上面的代码,每次消费都有提交,因此可以真正消费。

7. 消息的签收机制

  • 签收就像是签收快递一样,可以让快递小哥将快递放到前台自动签收,也可以让快递小哥当面给你确认之后再手动签收
  • 签收一般来说有几种方式:
    • 自动签收(Session.AUTO_ACKNOWLEDGE):该方式是默认的。该种方式,无需我们程序做任何操作,框架会帮我们自动签收收到的消息。
    • 手动签收(Session.CLIENT_ACKNOWLEDGE):手动签收。该种方式,需要我们手动调用Message.acknowledge(),来签收消息。如果不签收消息,该消息会被我们反复消费,只到被签收。
    • 允许重复消息(Session.DUPS_OK_ACKNOWLEDGE):多线程或多个消费者同时消费到一个消息,因为线程不安全,可能会重复消费。该种方式很少使用到。
    • 事务下的签收(Session.SESSION_TRANSACTED):开始事务的情况下,可以使用该方式。该种方式很少使用到。

7.1 非事务下的手动签收

  • 生产者代码不变
  • 消费者,在createSession的时候,选择手动签收。在接收到每条消息的是时候,通过textMessage.acknowledge()手动签收。
      @Test
      public void test3() throws JMSException {
        session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE); // 改成客户端手动签收
        Queue queue = session.createQueue(DESTINATION_NAME);
        consumer = session.createConsumer(queue);
        while (true) {
          Message message = consumer.receive();
          if (message instanceof TextMessage) {
            TextMessage textMessage = (TextMessage) message;
            String text = textMessage.getText();
            System.out.println("获得消息" + text);
            textMessage.acknowledge();  // 确认签收
          } else {
            break;
          }
        }
      }
    

7.2 事务下的手动签收

  • 在开启事务的情况下,也开启手动签收,即使没有每条消息都签收,只要事务提交了,在此期间的所有消息都会一起签收。
      @Test
      public void test3() throws JMSException {
        session = connection.createSession(true, Session.CLIENT_ACKNOWLEDGE); // 改成客户端手动签收
        Queue queue = session.createQueue(DESTINATION_NAME);
        consumer = session.createConsumer(queue);
        while (true) {
          Message message = consumer.receive(3000);
          if (message instanceof TextMessage) {
            TextMessage textMessage = (TextMessage) message;
            String text = textMessage.getText();
            System.out.println("获得消息" + text);
          } else {
            break;
          }
        }
        session.commit();
      }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值