学习笔记:ActiveMQ + SpringBoot & 事务问题 & 序列化问题

一、docker 安装 ActiveMQ

1.在docker环境中执行

// 搜索activemq镜像
docker search activemq
// 拉取activemq镜像
docker pull webcenter/activemq
// 查看拉取后的activemq镜像
docker images
// 创建数据文件夹和日志文件夹
mkdir -p ./activemq/soft/activemq
mkdir -p ./activemq/soft/activemq/log
// docker执行命令,名称,后台启动,绑定端口,开机启动,数据卷绑定
docker run --name=activemq -itd -p 8161:8161 -p 61616:61616 --restart=always -v /home/docker/activemq/soft/activemq:/data/activemq -v /home/docker/activemq/soft/activemq/log:/var/log/activemq  webcenter/activemq:latest
// 默认登陆用户名密码
用户名密码admin/admin

2.访问页面
访问路径:http://ip:8161。

二、ActiveMQ介绍

1.ActiveMQ基于JMS协议,组成部分:

JMS Provider:生产者,支持事务来保证发送可靠性。
JMS Message:JMS 的消息,主要类型有Text,Object,Map,Bytes,Stream五种类型。由消息头,消息属性,消息体组成。
JMS Consumer:消费者,支持事务和确认机制来保证消费可靠性。同步:使用recive()方法阻塞接受消息(客户端拉)。异步:使用监听方式接受消息(服务器推)。
JMS Domains:消息传递域,支持P2P(点对点传输),只存在一个队列,生产者发送到队列,消费者从队列中消费;支持pub/sub订阅消费模式,生产者发送到Topic,生产者订阅topic进行消费(消费者在订阅之前是收不到消息的。在订阅之后在线的状态可以收到消息,如果想离线后依然能接收到消息,需要设置成持久订阅)。

Connection Factory:连接工厂,创建连接,通过连接可以创建session对话,再创建生产者和消费者。(springboot中整合为jmstemplate模板,可以直接生产和消费消息);
JMS Connection:封装了客户与 JMS 提供者之间的一个虚拟的连接。
JMS Session:是生产者和消费者的一个单线程上下文。会话用于创建消息生产者(Producer)、消息消费者(Consumer),和消息(Message)等。会话提供了一个事务性的上下文,一组发送和接收被组合到了一个原子操作中。

2.消息结构

1.消息头

属性含义
Destination目的地,主要是queue和topic
DeliveryMode传递模式,在send或者jmstemplate中设置。分为持久模式和非持久模式
Expiration消息过期/到期时间,在send或者jmstemplate中设置
Priority消息优先级,有 0-9 十个级别,0-4是普通消息,5-9是加急消息。JMS 不要求 JMS Provider 严格按着十个优先级发送消息,但必须保证加急消息要先于普通消息到达。默认是第4级
MessageID由生产者自动分配,唯一的ID,以ID:开头
Timestamp生产者发送消息到消息call或者return返回的时间差
JMSTypeJMS 消息类型的识别符
CorrelationIDJMS 相关性 id,由客户端设置。用来连接到另外一个消息,典型的应用是在回复消息中连接到原消息
ReplyTo回复,由客户端设置。提供本信息回复消息的目的地址
Redelivered重发,由JMS Provider(供应商)设置

2.消息体:JMS API 定义了 5 种消息体格式,也叫消息类型,可以使用不同的形式发送接收数据,并可以兼容现有的消息格式。包括:TextMessage、MapMessage、BytesMessage、StreamMessage 和 ObjectMessage。它们都是 Message 接口的子类。

3.消息属性:自定义属性,JMS定义的属性,供应商特点的属性。

三、ActiveMQ + SpringBoot的使用

ActiveMQ配置

spring:
  activemq:
    broker-url: tcp://192.168.99.100:61616
    user: admin
    password: admin
    in-memory: false # 基于外部mq模式
    pool:
      enable: true #开启链接池
      max-connections: 10 #最大链接数
package com.zwfw.framework.activemq.config;

import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.ActiveMQSession;
import org.apache.activemq.RedeliveryPolicy;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
import org.springframework.jms.config.JmsListenerContainerFactory;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.support.converter.MappingJackson2MessageConverter;
import org.springframework.jms.support.converter.MessageConverter;
import org.springframework.jms.support.converter.MessageType;

import javax.jms.DeliveryMode;
import javax.jms.Session;


@Configuration
public class ActivemqConfig {
    @Value("${spring.activemq.broker-url}")
    private String brokerUrl;

    @Value("${spring.activemq.user}")
    private String username;

    @Value("${spring.activemq.password}")
    private String password;

    /**
     * 消息重发策略配置
     */
    @Bean
    public RedeliveryPolicy redeliveryPolicy() {
        RedeliveryPolicy redeliveryPolicy = new RedeliveryPolicy();
        //是否在每次尝试重新发送失败后,增长这个等待时间
        redeliveryPolicy.setUseExponentialBackOff(true);
        //重发次数,默认为6次-设置为3次
        redeliveryPolicy.setMaximumRedeliveries(3);
        //重发时间间隔单位毫秒,默认为1秒
        redeliveryPolicy.setInitialRedeliveryDelay(1000L);
        //第一次失败后重新发送之前等待500毫秒,第二次失败再等待500 * 2毫秒
        redeliveryPolicy.setBackOffMultiplier(2);
        // 是否避免消息碰撞
        redeliveryPolicy.setUseCollisionAvoidance(false);
        // 设置重发最大拖延时间-1表示无延迟限制
        redeliveryPolicy.setMaximumRedeliveryDelay(-1);
        return redeliveryPolicy;
    }

    /**
     * 消息工厂配置
     */
    @Bean
    public ActiveMQConnectionFactory activeMqConnectionFactory() {
        ActiveMQConnectionFactory activeMqConnectionFactory = new ActiveMQConnectionFactory(username, password, brokerUrl);
        activeMqConnectionFactory.setRedeliveryPolicy(redeliveryPolicy());
        return activeMqConnectionFactory;
    }

    @Bean(name = "jmsTemplate")
    public JmsTemplate jmsTemplate() {
        JmsTemplate jmsTemplate = new JmsTemplate();
        // 设置连接工厂
        jmsTemplate.setConnectionFactory(activeMqConnectionFactory());
        //deliveryMode, priority, timeToLive 的开关,要生效,必须配置为true,默认false
        jmsTemplate.setExplicitQosEnabled(true);
        //定义持久化后节点挂掉以后,重启可以继续消费 1表示非持久化,2表示持久化
        jmsTemplate.setDeliveryMode(DeliveryMode.PERSISTENT);
        /**
         * 如果不启用事务,则会导致XA事务失效;
         * 作为生产者如果需要支持事务,则需要配置SessionTransacted为true
         */
        jmsTemplate.setSessionTransacted(false);
        //消息的应答方式,需要手动确认,此时SessionTransacted必须被设置为false,且为Session.CLIENT_ACKNOWLEDGE模式
        /**
         * 当关闭事务时候,下面设置才有效
         * Session.AUTO_ACKNOWLEDGE  消息自动签收
         * Session.CLIENT_ACKNOWLEDGE  客户端调用acknowledge方法手动签收
         * Session.DUPS_OK_ACKNOWLEDGE 不必必须签收,消息可能会重复发送
         */
        jmsTemplate.setSessionAcknowledgeMode(Session.CLIENT_ACKNOWLEDGE);
        jmsTemplate.setMessageConverter(jacksonJmsMessageConverter());
        return jmsTemplate;
    }

    /**
     * topic模式的ListenerContainer
     * topic下没有消息回执一说,确认消息之存在queue模式
     * 浏览只是针对 Queue 的概念,Topic 没有浏览。浏览是指获取消息而消息依然保持在 broker 中,而消息的接收会把消息从 broker 中移除。
     */
    @Bean
    public JmsListenerContainerFactory<?> jmsListenerContainerTopic() {
        DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
        factory.setPubSubDomain(true);
        factory.setConnectionFactory(activeMqConnectionFactory());
        factory.setMessageConverter(jacksonJmsMessageConverter());
        return factory;
    }

    /**
     * queue模式的ListenerContainer
     * 监听容器配置,使用jackson的消息转换器
     * 1 不开启事务,手动确认,自动确认
     * 2 开启事务,是自动应答,当客户端消费有异常抛出,会进行重试模式,按照上面重试配置次数重试后,如果还是失败,则会进入死信队列
     * @return
     */
    @Bean
    public JmsListenerContainerFactory<?> jmsListenerContainerQueue() {
        DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
        // 关闭事务
        factory.setSessionTransacted(false);
        // 设置手动确认,默认配置中Session是开启了事务的,事务优先级大于客户端确认,即使我们设置了手动Ack也是无效的
        factory.setSessionAcknowledgeMode(ActiveMQSession.INDIVIDUAL_ACKNOWLEDGE);
        factory.setConnectionFactory(activeMqConnectionFactory());
        factory.setMessageConverter(jacksonJmsMessageConverter());
        return factory;
    }

    /**
     * queue模式的ListenerContainer
     * 监听容器配置,使用自带的消息转换器
     */
    @Bean
    public JmsListenerContainerFactory<?> jmsListenerContainerQueueNoConver() {
        DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
        // 关闭事务
        factory.setSessionTransacted(false);
        // 设置手动确认,默认配置中Session是开启了事物的,即使我们设置了手动Ack也是无效的
        factory.setSessionAcknowledgeMode(ActiveMQSession.INDIVIDUAL_ACKNOWLEDGE);
        factory.setConnectionFactory(activeMqConnectionFactory());
        return factory;
    }

    /**
     * 自定义消息转换器
     * @return
     */
    @Bean
    public MessageConverter jacksonJmsMessageConverter() {
        MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
        // MappingJackson2MessageConverter只支持TEXT和byte类型的转换,
        // 见org.springframework.jms.support.converter.MappingJackson2MessageConverter.toMessage
        converter.setTargetType(MessageType.TEXT);
        // 可以为任何字符,但必需要配置,在下文中的setTypeIdOnMessage方法中会用上
        converter.setTypeIdPropertyName("_type");
        return converter;
    }
}

queue和topic配置

package com.zwfw.framework.activemq.queue;

import org.apache.activemq.command.ActiveMQQueue;
import org.apache.activemq.command.ActiveMQTopic;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.jms.Queue;
import javax.jms.Topic;

@Configuration
public class QueueConfig {

    /**
     * 声明普通队列
     */
    @Bean
    public Queue conmonQueue(){
        return new ActiveMQQueue("common.queue");
    }

    /**
     * 声明延时队列
     */
    @Bean
    public Queue delayQueue(){
        return new ActiveMQQueue("delay.queue");
    }

    /**
     * 声明广播类型队列
     */
    @Bean
    public Topic topicQueue(){
        return new ActiveMQTopic("topic.queue");
    }
}

上面是mq的基本配置,配置了几个监听容器:queue模式下的带事务和不带事务手动确认的容器和topic监听的容器,这些容器在后续监听类中配置用得上。

P2P模式(图是借鉴来的):
P2P模式

前言:该模式,点对点传输。生产者传输消息到队列,消费者从队列中消费。生产者可配置事务,如果开启事务发送,只有在commit之后,队列中才会有消息入列,如果rollback则不会进入队列;客户端一般有两种方式,一种是事务,第二种是确认机制:开启事务,不需要设置确认机制(事务优先级大于确认机制,设置了手动确认等也无效),默认就是自动确认,当事务方法中抛出异常,则消息不会被消费,会进入重试,重试次数到了之后,会进入activemq的DLQ队列(死信)。


生产者代码和说明:

@RestController
public class SendController {
    
    @Autowired
    private Queue conmonQueue;

    @Autowired
    private JmsTemplate jmsTemplate;

    /**
     * 单条数据发送,事务模式
     */
    @RequestMapping("/commonQueue")
    public void commonQueue() throws InterruptedException {
        for (int i = 0; i < 20; i++) {
            Book book = new Book();
            book.setName("三体" + i).setAuthor("刘慈欣" + i).setType("科幻" + i);
            // 设置事务模式发送
            jmsTemplate.setSessionTransacted(true);
            jmsTemplate.convertAndSend(conmonQueue,book);
            if (i == 12) {
                throw new RuntimeException();
            }
        }
    }

    /**
     * 批量发送,事务模式
     */
    @RequestMapping("/commonQueue/{num}")
    public void commonQueueTranscate(@PathVariable("num") Integer num) throws Exception {
        MessageProducer pd = null;
        Session session = null;
        Connection connection = null;
        try {
            ConnectionFactory connectionFactory = jmsTemplate.getConnectionFactory();
            connection = connectionFactory.createConnection();
            connection.start();
            // 开启事务,只能设置AUTO_ACKNOWLEDGE,其他模式无效且不受控制
            session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
            pd = session.createProducer(conmonQueue);

            for (int i = 0; i < 20; i++) {
                Book book = new Book();
                book.setName("三体" + i).setAuthor("刘慈欣" + i).setType("科幻" + i);
                // 用内置的消息转换器
                TextMessage message = session.createTextMessage(JSON.toJSONString(book));
                // 用jackson的消息转化器
//                Message message = jmsTemplate.getMessageConverter().toMessage(book, session);
                pd.send(message);
                System.out.println("send book" + i + " to queue");
            }
            // 测试判断,偶数提交,奇数回滚
            if (num % 2 == 0) {
                session.commit();
            }else {
                session.rollback();
            }
        } catch (JMSException e) {
            throw new RuntimeException(e);
        }finally {
            pd.close();
            session.close();
            connection.close();
        }
    }

}

事务:使用springboot整合后,基本上是使用jmstemplate模版进行消息的发送。在事务环境下,好像只能发送单条并且发送成功后确认单条数据。这样数据量大的话感觉会影响效率。如果在批量发送环境下,用jmstemplate发送我还没有找到合适的方法 ………所以用session会话的模式,设置事务进行批量发送。
序列化:在配置文件中,定义了jackson的序列化方式,如果不定义,就是使用默认的org.springframework.jms.support.converter.SimpleMessageConverter.toMessage序列化。在里面根据你发送消息的类型来序列化。

在这里插入图片描述

如果使用的是自定义的jackson序列化,发送的jmstemplate模板也需要注入jackson的序列化配置,在监听容器配置中,也需要注入jackson的序列化配置。否则,如果发送的jmstemplate没有注入,或者用的会话模式session.createTextMessage来发送的消息(自带的序列化),在监听收到消息后会序列化失败,原因是,使用了jackson配置发送的消息,在内部会调用setTypeIdOnMessage,里面会塞入设置过的typeIdPropertyName。当消费者反序列化的时候,则会调用getJavaTypeForMessage方法,里面会判断有没有这个属性,如果没有则抛出异常。
总而言之,发送端和接收端的序列化配置必需同步。

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

当客户端监听配置的反序列化是jackson后,jmstemplate也要注入jackson配置。如果想要批量发送消息,可以使用下面的模版来构造一个消息对象,通过配置jackson后的jmstemplate方法是调用了setTypeIdOnMessage方法,在反序列化的时候不会出现上面异常问题:

Message message = jmsTemplate.getMessageConverter().toMessage(book, session);

消费者代码和说明:

@Component
public class ActiveListener {

    /**
     * 将开启事务时候,方法内有异常则不会确认消费
     * 发生异常会进入重试模式,服务器按重试配置数推送,默认6次
     * 重试还是失败,消息会进入死信队列
     */
    @JmsListener(destination = "common.queue", containerFactory = "jmsListenerContainerQueue")
    public void commonQueueListen(Book book, ActiveMQMessage message) throws Exception {
        System.out.println(book);
        // 手动确认消息,当开启事务时,此设置无效
        message.acknowledge();
    }

    /**
     * 使用内置序列化的配置的监听容器
     */
    @JmsListener(destination = "common.queue", containerFactory = "jmsListenerContainerQueueNoConver")
    public void commonQueueListen1(String book, ActiveMQMessage message) throws Exception {
        System.out.println(book);
        // 手动确认消息,当开启事务时,此设置无效
        message.acknowledge();
    }
}

当监听容器开启事务后, message.acknowledge()方法并没有作用,事务对应的是自动确认,不受控制。当监听容器关闭事务,应使用确认机制,一般手动确认,如果没有确认,则消息不会被消费。

分组和并发消费

并发消费:如果想在发送消息并且由多个消费者一起并发消费,可以通过设置配置文件中的concurrency属性,或者在@JmsListener中给注解属性concurrency设置数量,如下图。
分组消费:用于队列模式。分组消费需要在生产者发送的消息中设置消息头,使用setStringProperty来设置消息的头属性。在消费者端,在@JmsListener注解上添加消息头过滤 selector =“JMSXGroupID=‘groupB’”,就可以完成分组消费。
在这里插入图片描述

生产者:

@RequestMapping("/groupQueue")
    public void groupQueue() throws InterruptedException {
        for (int i = 0; i < 20; i++) {
            Book book = new Book();
            book.setName("三体" + i).setAuthor("刘慈欣" + i).setType("科幻" + i);
            jmsTemplate.setSessionTransacted(true);
            // 可以用session会话模式生成的message来设置消息头,我这里用模版发送,在生成message同时塞入属性
            jmsTemplate.send(conmonQueue, session -> {
                Message message = jmsTemplate.getMessageConverter().toMessage(book, session);
                message.setStringProperty("JMSXGroupID","groupA");
                return message;
            });
        }
    }

消费者:

    /**
     * 分组
     * selector,过滤头属性
     * concurrency,并发数量,可以生成多个消费者
     */
    @JmsListener(destination = "common.queue", containerFactory = "jmsListenerContainerQueue", selector ="JMSXGroupID='groupA'")
    public void commonQueueListenGroupA(Book book, ActiveMQMessage message) throws Exception {
        System.out.println("groupA: " + book);
        // 手动确认消息,当开启事务时,此设置无效
        message.acknowledge();
    }
    /**
     * 分组
     * selector,过滤头属性
     * concurrency,并发数量,可以生成多个消费者
     */
    @JmsListener(concurrency = "10", destination = "common.queue", containerFactory = "jmsListenerContainerQueue", selector ="JMSXGroupID='groupB'")
    public void commonQueueListenGroupB(Book book, ActiveMQMessage message) throws Exception {
        System.out.println("groupB: " + book);
        // 手动确认消息,当开启事务时,此设置无效
        message.acknowledge();
    }

pub/sub模式(图是借鉴来的):
发布订阅模式
topic模式待完善。。。。

四、解决的问题

此文主要是提供ActiveMQ基本的配置,生产者事务,消费者事务和确认机制,生产者和消费者的序列化问题。更详细的介绍网上有很多资料可以查阅。主要是自己学习的时候,网上的代码都没有很好的解决问题,所以在这里记录一下。

  1. 序列化和反序列化必需同步:如果使用原生的session来发送消息是没有配置序列化的(应该也是可以设置自定义的序列化配置的),监听类也需要用自带的默认的序列化方式来接受对象。如果使用了jackson序列化方式,在配置类中,jmstemplate和监听容器都要注入此配置,并且发送的时候需要用带有jackson配置的jmstemplate来发送消息,否则消息会序列化失败。
  2. 生产者的事务:如果用jmstemplate来发送消息,跟读源码会发现,如果设置setSessionTransacted为true,则会代理生成一个事务,并且发送了会commit。在网上用模版批量发送事务消息的例子。于是就用的session会话来控制批量消息事务的控制。如果要用自定义序列化,则可以用“Message message = jmsTemplate.getMessageConverter().toMessage(book, session);”来构造一个含有jackson序列化配置的消息。
  3. 消费者的事务和确认机制:当事务设置为true的时候,和生产者一样,在配置确认机制就不生效了。因为默认就是自动确认。当发生异常事务回滚,会进行重试阶段,重试后如果失败,则会进入死信队列。当事务设置为false的时候,一般设置的是手动确认,只有确认后的数据才会出队,否则数据会一直存在队列里面(排除定时的消息)。
  4. 批量事务:用模版方法发送貌似只能一条条的来,批量的话得用session来发送,并commit;
  5. topic的代码还没有实现,还在学习中,后续补上。。。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值