Springboot集成RabbitMQ, 注解式开发, 包含死信队列 , 消费回调介绍

RabbitMQ浅度学习

	这里介绍注解式开发RabbitMQ

RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)。RabbitMQ服务器是用Erlang语言编写的,而集群和故障转移是构建在开放电信平台框架上的。所有主要的编程语言均有与代理接口通讯的客户端库。 ---- 来自百度百科

Springboot集成RabbitMQ, 上代码

  • Springboot 2.2.2.RELEASE

  • JDK 1.8

    *准备工作:
    1: 在RabbitMQ中添加exchange, queue
    2: 将exchange和queue进行绑定
    3: 绑定规则, routingKey 设置为xxx.* 模糊后一位 Example: user.insert
    不能匹配user.insert.demo, 如要模糊多位, 使用xxx.#
    4. 路由规则也可精确匹配

也可不用手动创建exchange和queue, 以及他们的绑定关系 , 代码编写好后, 启动项目, 正常连接到RabbitMQ后相应的exchange和queue会自动创建并且绑定好

展示关键依赖:

		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

RabbitMQ部分配置

spring.application.name=spring-boot-rabbitmq
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=admin
#默认的虚拟主机
spring.rabbitmq.virtual-host=my_vhost
#连接超时时间 15s
spring.rabbitmq.connection-timeout=15000

#============rabbitmq消费者配置===========#
#并发数  主页的channel会有五个
spring.rabbitmq.listener.simple.concurrency=5
#最大并发数
#spring.rabbitmq.listener.simple.max-concurrency=10
#auto自动签收     manual手动签收, 只要谈到消息可靠, 基本都是手动签收
spring.rabbitmq.listener.simple.acknowledge-mode=manual
#限流  同一时间只允许一条消息
spring.rabbitmq.listener.simple.prefetch=5

# 回调方式消息确认模式, 消息发出后 异步等待broker回送响应
# confirm机制, 回调消息到达了exchange的消息
spring.rabbitmq.publisher-confirm-type=correlated
# return机制 , 回调消息没有到达queue的消息
spring.rabbitmq.publisher-returns=true
#配置为true,到不了queue会回调方法
spring.rabbitmq.template.mandatory=true

配置文件完全可配置类替代, 看个人爱好, 书写习惯

发送端:
**

注意:

这里RabbitTemplate用构造器注入的而没有直接用@Autowired, 是因为我没有写RabbitMQConfig配置类,
准确说是我没有用配置类将RabbitTemplate变成一个非Spring实例化单例的Bean. B装完了, 说白了,
这里RabbitTemplate不能是单例的, Spring默认创建的Bean是单例的. 单例的RabbitTemplate只能回调一次,
一个RabbitTemplate只能回调一次. (这个说法可能不太准确, 官方一点我也不知道咋说…
如果用配置类来配置RabbitTemplate可以在返回RabbitTemplate的方法上加上@Scope(“prototype”), 这样用注解方式注入的RabbitTemplate就不是单例了

这个类里两个关键:

  1. 多例的RabbitTemplate
  2. 设置回调的类
package com.sunnyfe.rabbitmq.demo.producer;

import com.sunnyfe.rabbitmq.demo.callback.RabbitmqCallback;
import com.sunnyfe.rabbitmq.demo.entity.User;
import lombok.extern.log4j.Log4j2;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;


/**
 * @author maple
 */
@Component
@Log4j2
public class InsertUserSender {

    /**
     * 非单例Bean, 构造器注入
     */
    private final RabbitTemplate rabbitTemplate;

    /**
     * 构造方法注入rabbitTemplate成为一个多例的rabbitTemplate
     * 如果是但里的rabbitmq, 只能回调一次 , 再次调用的时候会 报错: 一个rabbitmqTemplate只能回调一次
     */
    public InsertUserSender(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
        // 设置回调地址 消息到达exchange的回调
        rabbitTemplate.setConfirmCallback(new RabbitmqCallback());
        // 消息失败的回调 没有到queue的回调
        rabbitTemplate.setReturnCallback(new RabbitmqCallback());
    }

    public void sendInsertUser(User user) {
        CorrelationData correlationData = new CorrelationData();
        correlationData.setId(user.getPrimaryKey());
        try {

            rabbitTemplate.convertAndSend(
                    //exchange 交换机 相应要在rabbitmq中添加  user-exchange
                    "liveExchange",
                    //routingKey 路由key    user.insert
                    "info",
                    //消息体内容
                    user,
                    //消息唯一id
                    correlationData
            );
            log.info("消息发送成功");
        } catch (AmqpException e) {
            e.printStackTrace();
        }
    }

}

回调类

  1. 大可不必另外写一个回调类, 可直接写在发送者中, 然后在设置回调类的时候用this
  2. 要使这两个回调生效, 配置文件中的confirm机制和return机制一定得配
package com.sunnyfe.rabbitmq.demo.callback;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

/**
 * Detail 回调
 *
 * @author SunnyFon
 * @date 2021年01月18日 21:48
 */
@Slf4j
@Component
public class RabbitmqCallback  implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
    /**
     * 如果没有exchange,也会收到回调
     *
     * ConfirmCallback 只确认消息是否正确到达 Exchange 中
     * 1. 如果消息没有到exchange,则confirm回调,ack=false
     * 2. 如果消息到达exchange,则confirm回调,ack=true
     *
     * 配置参数
     *  spring.rabbitmq.publisher-confirms=true
     *
     * @param correlationData   如果发送方没有传这个对象,则为null
     * @param ack ack
     * @param cause cause
     * @see RabbitTemplate.ConfirmCallback#confirm
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        log.info(" 回调id:" + correlationData);
        if (ack) {
            log.info("消息成功到达exchange");
        } else {
            log.error("消息未成功到达exchange:" + cause);
        }
    }

    /**
     * Returned message callback. 到不了queue会回调到该方法
     *
     * @param message    the returned message.
     * @param replyCode  the reply code.
     * @param replyText  the reply text.
     * @param exchange   the exchange.
     * @param routingKey the routing key.
     */
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        System.out.println("未到queue的回调么");
    }
}

消息接收:
注解绑定队列和交换机(exchange), 还有路由.
死信队列信息就是arguments参数, 不需要死信队列可以去掉整个arguments={}

有几个关键点:

  1. 注解放的位置一定得方正确, 括号啥都不能乱, 否则脑袋找破了, 不知道为啥不创建死信队列.
  2. 配置文件签收方式一定设置为手动签收
  3. 我这里发送的是User对象, 但是接收用Message接收, 有点不讲武德可以改成public void process(@Payload User user, @Headers Map<String, Object> headers, Channel channel)
  4. 也可用message.getBody获取对象信息.
  5. 至于argument中的name定义都是来源于RabbitMQ
  6. 当消费消息出现异常时, 就会走catch代码, 就会执行 channel.basicNack(consumerTag, false, false); consumerTag是消息唯一id, 第一个false是否批量执行, 第二个false是否重新进入队列basicNack方法后会将消息放入死信队列, 就可在死信队列中对消费失败的消息进行业务逻辑执行, 消费死信队列中的消息.
  7. 死信队列 , 可以理解为一个普通的队列
package com.sunnyfe.rabbitmq.demo.rabbitmqtest;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.stereotype.Component;

import java.io.IOException;


/**
 * Detail RabbitMQ消息接收
 *
 * @author SunnyFon
 * @date 2021年01月18日 17:39
 */
@Component
public class Receiver {
    @RabbitListener(bindings = {
            @QueueBinding(value = @Queue(value = "liveQueue", arguments =
                    {@Argument(name = "x-dead-letter-exchange", value = "deadExchange"),
                            @Argument(name = "x-dead-letter-routing-key", value = "deadKey")
                            , @Argument(name = "x-message-ttl", value = "10000", type = "java.lang.Integer")
                            // ,@Argument(name = "x-max-length",value = "5",type = "java.lang.Integer")队列最大长度
                    }),
                    exchange = @Exchange(value = "liveExchange"),
                    key = {"info", "error", "warning"}
            )
    })

//            设置成manual手动确认,一定要对消息做出应答,否则rabbit认为当前队列没有消费完成,将不再继续向该队列发送消息。
//            1.channel.basicAck(long,boolean); 确认收到消息,消息将被队列移除,false只确认当前consumer一个消息收到,true确认所有consumer获得的消息。
//            2.channel.basicNack(long,boolean,boolean); 确认否定消息,第一个boolean表示一个consumer还是所有,第二个boolean表示requeue是否重新回到队列,true重新入队。
//            3.channel.basicReject(long,boolean); 拒绝消息,requeue=false 表示不再重新入队,如果配置了死信队列则进入死信队列。
//            4.当消息回滚到消息队列时,这条消息不会回到队列尾部,而是仍是在队列头部,这时消费者会又接收到这条消息,如果想消息进入队尾,须确认消息后再次发送消息。

    @RabbitHandler
    public void onMessage(Message message, Channel channel) {
        long consumerTag = message.getMessageProperties().getDeliveryTag();
        try {
            // int i = 1/0;   模拟消费消息, 回调接收ack是true 因为回调的ack根据exchange是否收到消息判断
//            int i = 1/0;
            System.out.println("收到消息");
            System.out.println(new String(message.getBody()));
            /*
             * 配置文件中配置的是手动签收
             * 如果注释ACK  也可以消费信息,  不过在控制台上消息仍然存在 消息会回到队列
             *
             * 业务逻辑完成后一定更要告诉rabbitmq服务器消息消费完了
             *  false 不使用批量ack  false可能会出现死循环 , 有消息永远消费不了 可以考虑将消息放到死信队列,  执行业务逻辑的时候try catch, 在catch中添加到死信队列
             *  true 使用批量ack  可能会导致消息丢失, 加上死信队列可解决消息丢失问题
             */
            channel.basicAck(consumerTag, true);
        } catch (Exception e) {
            try {
                // 消费异常消息会添加到死信队列
                channel.basicNack(consumerTag, false, false);
            } catch (IOException ioException) {
                ioException.printStackTrace();
            }
            e.printStackTrace();
        }
    }

}

死信队列

package com.sunnyfe.rabbitmq.demo.rabbitmqtest;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.io.IOException;

/**
 * Detail
 *
 * @author SunnyFon
 * @date 2021年01月18日 17:51
 */
@Component
public class DeadReceiver {
    @RabbitListener(bindings = {
            @QueueBinding(
                    value = @Queue(value = "deadQueue"),
                    exchange = @Exchange(value = "deadExchange"),
                    key = "deadKey"
            )
    })

    public void receive2(Message message, Channel channel) throws IOException {
        System.out.println("我是一条死信:" + message);
        long consumerTag = message.getMessageProperties().getDeliveryTag();
        System.out.println(consumerTag);
        channel.basicAck(consumerTag, true);
    }
}

个人觉得死信队列和回调没必要同时使用
甚至觉得用死信队列即可满足大部分业务场景, 看需要吧.
全部内容来自网络, 加上自己瞎🐔儿琢磨.
不足地方敬请提出.

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值