java客户端操作RabbitMQ

上一篇:RabbitMQ安装与原理详解

官方文档:http://next.rabbitmq.com/api-guide.html
API文档:https://rabbitmq.github.io/rabbitmq-java-client/api/current/

一、Java操作RabbitMQ(未使用SpringBoot)

1. 添加依赖

<!--rabbitMQ java客户端依赖-->
<dependency>
    <groupId>com.rabbitmq</groupId>
    <artifactId>amqp-client</artifactId>
    <version>5.1.1</version>
</dependency>

2. 与RabbitMQ建立连接

  • 先创建连接工厂ConnectionFactory ,并指定连接RabbitMQ的四要素(ip,端口,账号,密码)
  • 通过连接工厂创建连接对象Connection,再通过连接对象获取到信道对象Channel
  • 通过信道对象实现对 RabbitMQ 的操作
  • 关闭接连和信道

注意:有些类名与其它包中的重名了(比如java.sql.Connection,java.nio.channels.Channel等),导包的时候,一定是com.rabbitmq.client包下的

private static void send() {
    //创建连接工厂
    ConnectionFactory factory = new ConnectionFactory();
    //设置连接信息
    factory.setHost("192.168.245.128");//设置RabbitMQ所在机器的IP地址
    factory.setPort(5672);//指定端口
    factory.setUsername("root");//指定连接账号
    factory.setPassword("123");//指定连接密码
    Connection connection = null;
    final Channel channel;  //后面可能会在匿名内部类中使用,故设为常量
    try {
        //创建连接对象,用于连接到RabbitMQ
        connection=factory.newConnection();
        //创建通道对象
        channel=connection.createChannel();

        /**
         * 在这里实现对RabbitMQ的操作,之后的代码,如果没有特殊说明,默认都是写在这里的
         */

    } catch (IOException e) {
        e.printStackTrace();
    } catch (TimeoutException e) {
        e.printStackTrace();
    }finally {
        if(channel != null){
            try {
                channel.close();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (TimeoutException e) {
                e.printStackTrace();
            }
        }
        if(connection != null){
            try {
                connection.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

3. Channel操作RabbitMQ

在获取到 Channel 对象后(channel),通过该对象的方法来操作 RabbitMQ,常用的方法有:

(1)创建队列

队列是单例的,如果多次创建同一个名字的队列,仍是原来的那个队列

创建一个指定名字的队列:

channel.queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete,
                     Map<String, Object> arguments) 
  • queue:队列名
  • durable:是否持久化,true表示开启持久化
  • exclusive:是否排外,true表示排外,一个队列只允许一个消费者连接
  • autoDelete:如果没有消费者连接是否自动删除队列
  • arguments:指定参数,通常为null

创建一个随机的队列:

//创建一个随机的默认的队列,队列名是随机的,当然也可以自己指定队列名
String queueName=channel.queueDeclare().getQueue();
  • 返回 String:队列的名字

(2)创建交换机

交换机是单例的,如果多次创建同一个名字的交换机,并不会改变原来的那个交换机

channel.exchangeDeclare(String exchange, String type, boolean durable)
  • exchange:交换机的名称
  • type:交换机类型
  • durable:是否是持久化的消息

(3)将队列和交换机绑定到到某个RoutingKey中

无论是接收消息还是发送消息,必须保证交换机已经创建和队列已经创建并实现绑定

因此这个3个步骤一般是在项目启动时直接创建好,例如交给Spring在启动容器时就可以创建

注意:

  1. 无论是交换机还是队列都不会因为重复的创建而给覆盖(单例)
  2. 如果不能在项目启动时就创建好交换机和队列,以及绑定,那么建议在消息消费者中完成这些操作,如果这么做了就必须要先启动消费者(一般也是先启动消费者)
channel.queueBind(String queue, String exchange, String routingKey)
  • queue:队列名,必须已经存在
  • exchange:需要绑定的交换机名称,必须已经存在
  • routionKey:RoutingKey 这个值取值任意但必须要与发送时完全一致

(4)发送消息

channel.basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body)
  • exchange:将消息发送的指定的队列中,必须已经存在
  • routingKey:routingKey的名称,如果不指定exchange,则routingKey表示队列名字,直接往队列里发送
  • props:消息属性
  • body:具体的消息数据

注意:

  1. 使用direct消息模式时,必须要指定routingKey(路由键),将指定的消息绑定到指定的路由键上
  2. fanout模式的消息需要将一个消息同时绑定到多个队列中,因此这里不能创建并指定某个队列,即不绑定队列和交换机,方法中的routingKey为""
  3. 在topic模式中必须要指定routingkey,并且可以同时指定多层的routingKey,每个层次之间使用点(".")分隔即可 例如:aa.bb.cc

(5)接收消息

channel.basicConsume(String queue, boolean autoAck, String consumerTag, Consumer callback)
  • queue:队列名称,必须已经存在
  • autoAck:是否自动确认消息 true表示自动确认 false表示手动确认
  • consumerTag:消费的标签,用于区分不同的消费者
  • callback:消息接收后的回调方法,新建一个DefaultConsumer(channel)对象,构造方法的参数为信道对象,并重写handleDelivery方法,在该方法中对消息进行处理
    • handleDelivery方法的参数:
      • consumerTag:标识信道中投递的消息,每个信道中,每条消息的 consumerTag 从 1 开始递增
      • body:表示取到的消息的字节数组

注意:

  1. 消息消费者消费完成消息以后可以不关闭通道和链接,如果不关闭通道和链接那么消费者会不间断的接收消息,因为我们的消息接收底层会启动一个子线程,异步实现接收
  2. 使用Exchange的direct模式时接收者的RoutingKey必须要与发送时的RoutingKey完全一致否则无法获取消息,接收消息时队列名也必须要发送消息时的完全一致
  3. 使用fanout模式获取消息时不需要绑定特定的队列名称,只需使用channel.queueDeclare().getQueue();获取一个随机的队列名称,然后绑定到指定的Exchange即可获取消息。这种模式中,可以同时启动多个接收者,只要都绑定到同一个Exchang上,即可让所有接收者同时接收同一个消息,是一种广播的消息机制
  4. Topic模式的消息接收时必须要指定RoutingKey并且可以使用#*来做统配符号,#表示通配任意一个单词,*表示通配任意多个单词,例如aa.*.*或者aa.#都可以接收到 routingKey 为 aa.bb.cc 的发送者发送的消息

(6)举例

接收消息(先启动接收消息进行监听,再启动发送消息):

  1. 不经过交换机,直接接收名字为 myQueue 的队列中的消息:

    //不经过交换机,直接接收名字为 myQueue 的队列中的消息
    channel.basicConsume("myQueue",true,"",new DefaultConsumer(channel){
        @Override
        public void handleDelivery(String consumerTag, 
                                   Envelope envelope, 
                                   AMQP.BasicProperties properties, 
                                   byte[] body) throws IOException {
            
            //获取到队列中的消息
            String message=new String(body,"UTF-8");
    	}
    });
    
  2. 接收交换机类型为 direct 的交换机绑定的队列中的数据

    //接收与类型为 fanout 的交换机绑定的队列中的数据
    channel.queueDeclare("myDirectQueue", true, false, false, null);
    channel.exchangeDeclare("directExchange", "fanout", true);
    channel.queueBind("myDirectQueue", "directExchange", "");
    
    channel.basicConsume("myDirectQueue", true, "", new DefaultConsumer(channel){
        @Override
        public void handleDelivery(String consumerTag, 
                                   Envelope envelope, 
                                   AMQP.BasicProperties properties, 
                                   byte[] body) throws IOException {
            
            //获取到队列中的消息
            String message=new String(body,"UTF-8");
        }
    });
    
  3. 接收交换机类型为 fanout 的交换机绑定的队列中的数据

    //接收与类型为 fanout 的交换机绑定的队列中的数据
    String queueName=channel.queueDeclare().getQueue();
    channel.exchangeDeclare("fanoutExchange", "fanout", true);
    channel.queueBind(queueName, "fanoutExchange", "");
    
    channel.basicConsume(queueName, true, "", new DefaultConsumer(channel){
        @Override
        public void handleDelivery(String consumerTag, 
                                   Envelope envelope, 
                                   AMQP.BasicProperties properties, 
                                   byte[] body) throws IOException {
            
            //获取到队列中的消息
            String message=new String(body,"UTF-8");
        }
    });
    
  4. 接收交换机类型为 topic 的交换机绑定的队列中的数据

    String queueName = channel.queueDeclare().getQueue();
    
    //创建一个交换机
    channel.exchangeDeclare("topicExchange", "topic", true);
    
    //将队列和交换机绑定到到某个RoutingKey中
    channel.queueBind(queueName, "topicExchange", "aa.*");
    
    //接收消息
    channel.basicConsume(queueName, true, "", new DefaultConsumer(channel) {
        @Override
        public void handleDelivery(String consumerTag,
                                   Envelope envelope,
                                   AMQP.BasicProperties properties,
                                   byte[] body) throws IOException {
            //获取消息
            String message = new String(body, "UTF-8");
            /*
                这里对消息进行处理
            */
        }
    });
    
  5. 给上面的消息消费者发送消息:

    //定义消息数据
    String message="这是发送的消息数据";
    
    //不经过交换机,直接发送到名字为 myQueue 的队列中
    channel.basicPublish("", "myQueue", null, message.getBytes("UTF-8"));
    
    //将消息发送到类型为 direct 的交换机中 
    channel.basicPublish("directExchange", "directRoutingKey", null, message.getBytes("UTF-8"));
    
    //将消息发送到类型为 fanout 的交换机中 
    channel.basicPublish("fanoutExchange", "", null, message.getBytes("UTF-8"));
    
    //将消息发送到类型为 topic 的交换机中 
    channel.basicPublish("topicExchange", "aa.bb", null, message.getBytes("UTF-8"));
    

4. 事务与消息确认模式

事务消息与数据库的事务类似,只是MQ中的消息是要保证消息是否会全部发送成功,防止丢失消息的一种策略。

RabbitMQ有两种方式来解决这个问题:

  1. 通过AMQP提供的事务机制实现;
  2. 使用Confirm发送方和接收方确认模式实现;

由于事务机制的性能很差,故使用较多的是Confirm发送方确认模式

(1)事务机制

事务的实现主要是对信道(Channel)的设置,主要的方法有三个:

  1. channel.txSelect():声明启动事务模式;
  2. channel.txComment():提交事务;
  3. channel.txRollback():回滚事务;

注意:要在消息发送之前启动信道的事务模式,发送完毕后要提交事务,否则不会发送成功

(2)发送者确认模式

Confirm发送方确认模式使用和事务类似,也是通过设置Channel进行发送方确认的,最终达到确保所有的消息全部发送成功

Confirm的三种实现方式:
开启发送方确认模式:channel.confirmSelect();
方式一:channel.waitForConfirms():普通发送方确认模式;
方式二:channel.waitForConfirmsOrDie():批量确认模式;
方式三:channel.addConfirmListener():异步监听发送方确认模式

使用方式:在发送消息前,开启发送方确认模式,在发送完毕后,进行消息的确认

方式一:
在推送消息之前,channel.confirmSelect()声明开启发送方确认模式,再使用channel.waitForConfirms()等待消息被服务器确认即可。

//开启消息确认模式
channel.confirmSelect();

//发送消息到指定队列
channel.basicPublish("", "directRoutingKey", null, message.getBytes("UTF-8"));

if (channel.waitForConfirms()) {
    System.out.println("消息发送成功");
}

方式二:
channel.waitForConfirmsOrDie()使用同步方式等所有的消息发送之后才会执行后面代码,只要有一个消息未被确认就会抛出IOException异常。

//开启消息确认模式
channel.confirmSelect();

for (int i = 0; i < 10000; i++) {
    channel.basicPublish("", "directRoutingKey", null, String.valueOf(i).getBytes("UTF-8"));
}
channel.waitForConfirmsOrDie(); //直到所有信息都发布,只要有一个未确认就会IOException
System.out.println("全部执行完成");

方式三:
异步模式的优点,就是执行效率高,不需要等待消息执行完,只需要监听消息即可

//开启消息确认模式
channel.confirmSelect();


//发送消息到指定队列
for (int i = 0; i < 10000; i++) {
    channel.basicPublish("", "directRoutingKey", null, String.valueOf(i).getBytes("UTF-8"));
}

//异步监听确认和未确认的消息
channel.addConfirmListener(new ConfirmListener() {
    public void handleAck(long deliveryTag, boolean multiple) throws IOException {
    	//这里是确认的消息
        System.out.println("成功确认的消息" + deliveryTag + "==> " + multiple);
    }

    public void handleNack(long deliveryTag, boolean multiple) throws IOException {
    	//这里是未确认的消息
        System.out.println("未确认的消息");
    }
});

handleAck()方法与handleNack()方法的参数:

  • deliveryTag:表示第几条消息
  • multiple:boolean 类型,表示是否批量处理了消息,true表示批量执行了deliveryTag这个值的消息和它之前的所有消息,false的话表示单条确认

(3)消费者确认模式

为了保证消息从队列可靠地到达消费者,RabbitMQ提供消息确认机制(message acknowledgment)。接收者消息确认指的是否要将数据从队列中进行移除,如果确认消息则是将这条消息从队列中彻底移除掉。如果这条消息被成功处理(例如完成数据库的插入等等),这条消息才能被确认删除,如果没有被成功处理(例如服务崩溃),我们在队列中的消息不应该被确认移除

在声明接收消息时(channel.basicConsume),可以指定 autoAck 参数,当 autoAckfalse时,RabbitMQ会等待消费者显式发回ack信号后才从内存(和磁盘,如果是持久化消息的话)中移去消息。否则(autoAck=true),消息被消费后会在队列中立即删除它,不管消息是否被接收到。

在Consumer中Confirm模式中分为手动确认和自动确认(autoAck=true)。

手动确认主要并使用以下方法:

  • basicAck:用于肯定确认

    //deliveryTage:消息的编号,由RabbitMQ提供
    //multiple:true时用于多个消息确认,确认deliveryTage对应的消息和之前的消息,false为单条消息确认。
    channel.basicAck(long deliveryTag, boolean multiple);
    
  • basicRecover:路由不成功的消息可以使用recover重新发送到队列中。

    //requeue:true时将确认不成功的消息重新发送到队列中,false 直接丢弃
    channel.basicRecover(boolean requeue);
    
  • basicReject:是接收端告诉服务器这个消息我拒绝接收,不处理,可以设置是否放回到队列中还是丢掉,而且只能一次拒绝一个消息,官网中有明确说明不能批量拒绝消息,为解决批量拒绝消息才有了basicNack。

    //deliveryTage:消息的编号,由RabbitMQ提供
    //requeue:true时将确认不成功的消息重新发送到队列中,false 直接丢弃
    channel.basicReject(long deliveryTag, boolean requeue);
    
  • basicNack:可以一次拒绝N条消息,客户端可以设置basicNack方法的multiple参数为true。

    //deliveryTage:消息的编号,由RabbitMQ提供
    //multiple:true表示开启批量处理
    //requeue:true时将确认不成功的消息重新发送到队列中,false 直接丢弃
    channel.basicNack(long deliveryTag, boolean multiple, boolean requeue);
    
  • 当程序执行中断或者因为网络原因,RabbitMQ 没有收到 ack,则也会将消息重新入队。

完整的程序:

channel.basicConsume("confirmDirectQueue", false, "", new DefaultConsumer(channel) {
    @Override
    public void handleDelivery(String consumerTag, 
                               Envelope envelope, 
                               AMQP.BasicProperties properties, 
                               byte[] body) throws IOException {
        String message = new String(body, "UTF-8");
        System.out.println(message);
        //获取消息在队列中的唯一标识
        long messageNo = envelope.getDeliveryTag();
        //根据消息的编号来确认消息,确认以后则表示这个消息已经全部完成处理
        //进行消息确认,需要将这个消息从队列中移除掉
        channel.basicAck(messageNo, true);
    }
});

(4)事务与确认模式混用

强烈建议只使用消息确认模式,因为事务开销太大,假设消费者模式中使用了事务,并且在消息确认之后进行了事务回滚,那么RabbitMQ会产生什么样的变化?

结果分为两种情况:

  • autoAck=false手动确认的时候是支持事务的,也就是说即使你已经手动确认了消息已经收到,但在确认消息会等到事务提交之后,如果你手动确认现在之后,又回滚了事务,那么会以事务回滚为主,此条消息会重新放回队列;
  • autoAck=true如果自定确认为true的情况是不支持事务的,也就是说你即使在收到消息之后在回滚事务也是于事无补的,队列已经把消息移除了;

注意:如果两者都使用了的话,如果确认模式中使用的是异步的方法,则事务提交不能放在主线程中,因为主线程运行完后,确认模式的子线程可能还在运行,如果事务提交放在主线程中的话,则主线程执行完后,子线程中确认模式就无法进行事务提交了。故事务提交应放在模式确认的子线程中

二、SpringBoot集成RabbitMQ

1. 添加依赖

<!--spring集成amqp的起步依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

<!--这个是测试的-->
<dependency>
    <groupId>org.springframework.amqp</groupId>
    <artifactId>spring-rabbit-test</artifactId>
    <scope>test</scope>
</dependency>

2. 配置RabbitMQ

(1)通用配置

# 配置 rabbitmq 的ip
spring.rabbitmq.host=192.168.29.128
# 配置 rabbitmq 的端口
spring.rabbitmq.port=5672
# 配置 rabbitmq 的用户名
spring.rabbitmq.username=root
# 配置 rabbitmq 的密码
spring.rabbitmq.password=123
# 配置虚拟主机
spring.rabbitmq.virtual-host=/
# 配置连接超时时间
spring.rabbitmq.connection-timeout=15000

如果rabbitmq是集群的,则使用 addresses 来替换 host 和 port 配置如下:

#配置RabbitMQ的集群访问地址
spring.rabbitmq.addresses=192.168.222.129:5672,192.168.222.130:5672

(2)配置Producer

#开启发布者确认确认机制,snone为不启用,correlated是发布消息成功到交换器后会触发回调方法
spring.rabbitmq.publisher-confirm-type=correlated
#消息从交换机抵达队列确认,当消息没有路由从交换机到队列时,会进行回调
spring.rabbitmq.publisher-returns=true
#设置为 true 后 消费者在消息没有被路由到合适队列情况下会被return监听,而不会自动删除
spring.rabbitmq.template.mandatory=true

(3)配置Consumer

首先配置手工确认模式(默认是自动的),用于 ACK 的手工处理,这样我们可以保证消息的可靠性送达,或者在消费端消费失败的时候可以做到重回队列、根据业务记录日志等处理。我们也可以设置消费端的监听个数和最大个数,用于控制消费端的并发情况。我们要开启限流,指定每次处理消息最多只能处理两条消息。

#设置消费端手动 ack
spring.rabbitmq.listener.simple.acknowledge-mode=manual
#消费者最小数量
spring.rabbitmq.listener.simple.concurrency=1
#消费之最大数量
spring.rabbitmq.listener.simple.max-concurrency=10

#在单个请求中处理的消息个数,他应该大于等于事务数量(unack的最大数量)
spring.rabbitmq.listener.simple.prefetch=2

3. 创建队列与交换机

  • 创建交换机(Exchange):
    • 直接new对应类型的交换机:DirectExchange,FanoutExchange,TopicExchange,并指定交换机的名字
    • 通过交换机构造器对象创建各种类型的交换机:ExchangeBuilder.directExchange().build() 等
    • 通过 AmqpAdmin(在 RabbitAutoConfiguration 中已经自动注入) 来创建
  • 创建队列(Queue):
    • 直接new一个Queue对象(注意包名是:org.springframework.amqp.core.Queue),并指定队列的名字
    • 通过 AmqpAdmin(在RabbitAutoConfiguration` 中已经自动注入) 来创建
  • 创建队列与交换机的绑定对象(Binding)
    • 通过BindingBuilder对象来进行绑定并指定RoutingKey

(1)直接创建

注意

  1. 如果 RabbitMQ 中已经存在了相应的队列,交换机等,则需要先删除。例如,当第一次运行创建了某个 Queue 后,发现其中某个配置错了,改了相应的参数后,直接启动并不会覆盖原来的参数,需要在 RabbitMQ 中将之前创建的 Queue 删除后重新创建
  2. 并不是项目启动时就会直接创建配置中的队列,而是当第一次连接该队列时才会进行创建,例如有消费者监听这个端口时,交换机也一样。
//@Configuration 标记当前类是Spring的一个配置类,用于模拟Spring的xml配置文件
@Configuration
public class AmqpConfig {
    //标记当前方法是一个Spring的Bean标签配置,方法名相当于bean标签的id 返回值相当于bean标签的class
    //作用是用于创建一个对象到Spring的容器中
    @Bean
    public DirectExchange directExchange(){
        /*
         *   String name,       交换机名字
         *   boolean durable,   是否持久化
         *   boolean autoDelete,    是否自动删除
         *   Map<String, Object> arguments  参数
         */
        return new DirectExchange("BootDirectExchange", true, false, null);
    }
    @Bean
    public Queue directQueue(){
        /*
            Queue(String name,  队列名字
            boolean durable,  是否持久化
            boolean exclusive,  是否排他
            boolean autoDelete, 是否自动删除
            Map<String, Object> arguments) 属性
         */
        return new Queue("BootDirectQueue", true, false, false);
    }
    
    //将队列与交换机进行绑定,并指定RoutingKey
    //参数 1 为需要绑定的队列对象,参数名必须要与某个标记了@Bean的方法名完全一致,Spring就会将这个方法的返回值注入到当前方法参数中
    //参数 2 为需要绑定的交换机对象,参数名必须要与某个标记了@Bean的方法名完全一致,Spring就会将这个方法的返回值注入到当前方法参数中
    @Bean
    public Binding directBinding(Queue directQueue, DirectExchange directExchange){
        return BindingBuilder.bind(directQueue).to(directExchange).with("BootDirectRoutingKey");
    }

	//第二种创建 Binding 方式,与上一个方法结果相同
	@Bean
    public Binding directBinding2() {
        /*
         * String destination, 目的地(队列名或者交换机名字)
         * DestinationType destinationType, 目的地类型(Queue、Exhcange)
         * String exchange,     交换机
         * String routingKey,   路由键
         * Map<String, Object> arguments    参数
         */
        return new Binding("BootDirectQueue",
                Binding.DestinationType.QUEUE,
                "BootDirectExchange",
                "BootDirectRoutingKey",
                null);
    }

	/**
     * 创建死信交换机,跟普通交换机一样,只是死信交换机只用来接收过期的消息
     */
    @Bean
    public DirectExchange deadExchange() {
        return new DirectExchange("deadExchange", true, false);
    }

    /**
     * 创建死信队列,该队列没有消费者,消息会设置过期时间,消息过期后会发送到死信交换机,在由死信交换机转发至处理该消息的队列中
     */
    @Bean
    public Queue DeadQueue() {
        Map<String, Object> arguments = new HashMap<>();
        // 死信路由到死信交换器DLX
        arguments.put("x-dead-letter-exchange", "deadExchange");
        arguments.put("x-dead-letter-routing-key", "deadRoutingKey"); //路由键
        arguments.put("x-message-ttl", 60000); // 消息过期时间 1分钟

		/*
            Queue(String name,  队列名字
            boolean durable,  是否持久化
            boolean exclusive,  是否排他
            boolean autoDelete, 是否自动删除
            Map<String, Object> arguments) 属性
         */
        return new Queue("deadQueue", true, false, false, arguments);

    }
}

(2)使用 AmqpAdmin 创建

@SpringBootTest
class GulimallOrderApplicationTests {

    @Autowired
    private AmqpAdmin amqpAdmin;

    @Test
    void contextLoads() {
        //创建交换机
        amqpAdmin.declareExchange(new DirectExchange("hello-java-exchange", //名字
                true, //是否持久化存储
                false)); //是否自动删除
        //创建队列,注意这里的 Queue 是org.springframework.amqp.core包下的
        amqpAdmin.declareQueue(new Queue("hello-java-queue", //队列名
                true, //是否持久化存储
                false, //是否独占
                false)); //是否自动删除
        //创建绑定关系
        amqpAdmin.declareBinding(new Binding("hello-java-queue", //目的地
                Binding.DestinationType.QUEUE, //目的地类型(队列或者交换机)
                "hello-java-exchange", //交换机
                "hello.java", //路由键
                null)); //自定义参数
    }
}

4. AmqpTemplate发送消息

AmqpTemplate 它提供了通用的操作基于Amqp开发的消息队列的方法。同样我们需要进行注入到 Spring 容器中,然后直接使用。AmqpTemplate 在 Spring 整合时需要实例化,但是在 Springboot 整合时,在配置文件里添加配置即可。

  • 获取AmqpTemplate对象,在Springboot中,在需要使用的类中直接获取:

    @Autowired
    private AmqpTemplate amqpTemplate;
    
  • 将java对象转换为Message对象,并发送到RabbitMQ

    amqpTemplate.convertAndSend(String exchange, String routingKey, Object message)
    
    • exchange:交换机名称
    • routingKey:路由键
    • message:消息
  • 将Message消息转换为java对象

    amqpTemplate.receiveAndConvert(String queueName)
    
    • queueName:队列名字
    • 返回值为Object,需要类型强转

5. RabbitTemplate发送消息

RabbitTemplate 即消息模板,RabbitTemplate 是 AmqpTemplate 接口的一个实现类,它除了提供了 AmqpTemplate 通用的方法外,还提供了针对RabbitMQ操作的方法,比如回调监听消息接口 ConfirmCallback、返回值确认接口 ReturnCallback 等等。

(1)发送消息

@SpringBootTest
class GulimallOrderApplicationTests {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    void sendMessage() {
        Map<String, Object> map = new HashMap<>();
        map.put("name", "zhangsan");
        map.put("age", 18);
        rabbitTemplate.convertAndSend("hello-java-exchange", //交换机
                "hello.java", //路由键
                map, //消息
                new CorrelationData(UUID.randomUUID().toString())); //唯一Id
    }

}

(2)设置发送回调

发送者确认模式,需要在发送消息之前进行设置:

@Configuration
public class MyRabbitConfig {

    @Autowired
    private RabbitTemplate rabbitTemplate;

	//使用JSON作为消息的序列化方式
    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }

    /**
     * 定制rabbitTemplate
     * 配置文件中需配置 spring.rabbitmq.publisher-confirm-type=correlated
     */
    @PostConstruct //MyRabbitConfig 对象创建完成以后,执行这个方法
    public void initRabbitTemplate() {
        //设置消息抵达交换机确认回调
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {

            /**
             *
             * @param correlationData 当前消息的唯一关联数据(消息的唯一id)
             * @param ack 消息是否成功收到,只要消息抵达broker,就是true
             * @param cause 失败的原因
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                System.out.println("ConfirmCallback ==> correlationData[" + correlationData + "]==>ack[" + ack + "]==>cause[" + cause);
                if (ack) {
                    System.out.println("消息发送确认成功");
                } else {
                    System.out.println("消息发送失败:" + cause);
                }
            }
        });

        /**
         * 设置消息抵达队列确认回调,配置文件中需要配:
         * spring.rabbitmq.publisher-returns=true
         * spring.rabbitmq.template.mandatory=true
         */
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {

            /**
             * 当消息没有投递给指定的队列,就会触发这个失败回调
             * @param message   投递失败的消息详细信息
             * @param replyCode 回复的状态码
             * @param replyText 回复的文本内容
             * @param exchange  消息发送给哪个交换机
             * @param routingKey 消息发送的路由键
             */
            @Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                System.out.println("ReturnCallback ==> message[" + message + "]==>replyCode[" + replyCode + "]==>replyText[" + replyText + "]==>exchange[" + exchange + "]==>routingKey[" + routingKey);
                //在这里可以对消息进行重发
            }
        });
    }
}

关于确认与回调:

  • 如果消息没有到 exchange,则 ConfirmCallback 回调,ack=false
  • 如果消息到达 exchange,则 ConfirmCallback 回调,ack=true
  • exchangequeue 成功,则不回调 return
  • exchangequeue 失败,则回调 return

6. Consumer接收消息(@RabbitListener)

(1)使用@RabbitListener监听消息

@RabbitListener:可以标在类上(要和 @RabbitHandler 配合使用)或者方法上,参数 queues 可以指定一个 String[],来监听多个队列。(类必须注入到 Spring 容器中)

  • 方法的参数列表可以写的类型:
    1. Message message:原生消息详细信息,头 + 体
    2. T:发送的消息的类型,比如发送消息时发了个 Map,则接收该消息时也可以使用 Map,
    3. Channel channel:当前传输数据的管道
@Service
public class TestServiceImpl{
	@RabbitListener(queues = "hello-java-queue")
	public void directReceive(Message message, Map<String, Object> body, Channel channel) {
	    System.out.println("收到消息:" + body);
	}
	
	//也可以直接在注解中创建队列,交换机,然后指定routingKey进行绑定,监听
	@RabbitListener(
	       	bindings = @QueueBinding(
	           	value = @Queue(value = "queue2", durable = "true"),
	           	exchange = @Exchange(value = "exchange2", 
		            	type = "direct", 
		            	durable = "true", i
		            	gnoreDeclarationExceptions = "true"
		        ),
	           	key = "routingKey2"
	       	)
	)
	public void directReceive2(Message message, Channel channel) {
	    //这里是对取出来的message进行处理
	}
}

(2)使用 @RabbitHandler 重载消息类型

@RabbitHandler:当 @RabbitListener 标在类上时,要使用 @RabbitHandler 标在方法上。这样的好处是,当一个队列中传入了不同类型的对象时,可以通过重载的方式在方法形参中直接接收不同类型的消息

@Component
@RabbitListener(queues = "hello-java-queue")
public class ConsumerListener {

    //消息类型为 Map
    @RabbitHandler
    public void directReceive(Map<String, Object> body) {
        System.out.println("收到消息Map:" + body);
    }

    //消息类型为 String
    @RabbitHandler
    public void directReceive(String body) {
        System.out.println("收到消息String:" + body);
    }
}

(3)消息确认机制

要在配置文件中配置手动 ack:spring.rabbitmq.listener.simple.acknowledge-mode=manual

@Component
@RabbitListener(queues = "hello-java-queue")
public class ConsumerListener {

    //消息类型为 Map
    @RabbitHandler
    public void directReceive(Message message, Map<String, Object> body, Channel channel) {
        System.out.println("收到消息Map:" + body);
        //channel中按顺序自增,所以是唯一的
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            if (deliveryTag % 2 == 0) {
                /*
                 * 肯定确认,表明已经成功消费消息
                 * deliveryTage:消息的编号,由RabbitMQ提供
                 * multiple:true时用于多个消息确认,确认deliveryTage对应的消息和之前的消息,false为单条消息确认。
                 */
                channel.basicAck(deliveryTag, false);
            } else {
                /**
                 * 拒绝消息,表明消息消费失败
                 *deliveryTage:消息的编号,由RabbitMQ提供
                 *multiple:true时用于多个消息确认,确认deliveryTage对应的消息和之前的消息,false为单条消息确认。
                 * requeuq:true 表示重新入队;false 表示丢弃
                 */
                channel.basicNack(deliveryTag, false, true);
            }

        } catch (IOException e) {
            //网络中断
            e.printStackTrace();
        }
    }
}

7. 使用JSON的序列化方式

所有的序列化方式都实现了 MessageConverter 接口,默认使用的是 SimpleMessageConverter,其使用 ObjectOutputStream 进行序列化,需要对象实现 Serializable 接口。

若将消息序列化为 json,则只需将自带的 Jackson2JsonMessageConverter 注入容器即可

@Configuration
public class MyRabbitConfig {

    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }
}

三、常见问题及解决思路

1. 消息丢失

(1)消息发送出去,由于网络问题没有抵达服务器
  1. 做好容错方法(try - catch),发送消息可能会网络失败,失败后要有重试机制,可记录到数据库,采用定期扫描重发的方式
  2. 做好日志记录,每个消息状态是否都被服务器收到都应该记录
  3. 做好定期重发,如果消息没有发送成功,定期去数据库扫描未成功的消息进行重发

数据库消息记录样表:

CREATE TABLE `mq_message` (
`message_id` char(32) NOT NULL,
`content` text,
`to_exchane` varchar(255) DEFAULT NULL,
`routing_key` varchar(255) DEFAULT NULL,
`class_type` varchar(255) DEFAULT NULL,
`message_status` int(1) DEFAULT '0' COMMENT '0-新建 1-已发送 2-错误抵达 3-已抵达',
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`message_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
(2)消息抵达 Broker,Broker 要将消息写入磁盘(持久化)才算成功,此时 Broker 尚未持久化完成,宕机

消息生产者也必须加入确认回调机制,确认成功的消息,修改数据库消息状态

(3)自动 ACK 的状态下,消费者收到消息,但没来得及消费然后宕机

一定开启手动 ACK,消费成功才移除,失败或者没来得及处理就 noAck 并重新入队

2. 消息积压

  1. 消费者宕机积压
  2. 消费者消费能力不足积压
  3. 发送者发送流量太大
    1. 上线更多的消费者,进行正常消费
    2. 上线专门的队列消费服务,将消息先批量取出来,记录数据库,离线慢慢处理

3. 消息重复

  1. 消息消费成功,事务已经提交,ack 时,机器宕机,导致没有 ack 成功,Broker 的消息重新由 unack 变为 ready,并发送给其他消费者
  2. 消息消费失败,由于重试机制,自动又将消息发送出去
  3. 成功消费,ack 时宕机,消息由 unack 变为 ready,Broker 又重新发送
    1. 消费者的业务消费接口应该设计为幂等性的。比如扣库存有工作单的状态标志

    2. 使用防重表(redis/mysql),发送消息每一个都有业务的唯一标识,处理过就不用处理

    3. rabbitMQ 的每一个消息都有 redelivered 字段,可以获取是否是被重新投递过来的,而不是第一次投递过来的。

      //当前消息是否被第二次及以后(重新)派发过来了
      Boolean redelivered = message.getMessageProperties().getRedelivered();
      

讲解比较深,讲的也非常棒:Java SpringBoot集成RabbitMq实战和总结
参考了这一篇博客:https://www.cnblogs.com/haixiang/p/10959551.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值