springboot2.x +rabbitmq使用和源码分析二(生产者配置)

1:手动构建RabbitmqQueueExchangeAutoConfiguration

该类用于初始化 queue exchange 并进行Binding绑定

package com.fc.rabbitmq_demo.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import static com.fc.rabbitmq_demo.config.CommonConstant.*;

/**
 *初始化 queue exchange 并进行Binding绑定
 * @author fanyuan
 */
@Configuration
public class RabbitmqQueueExchangeAutoConfiguration {


    /**
     * 默认有四种bean 对于rabbitmq四种交换器
     * DirectExchange: 直连型 和queue一一对应
     * FanoutExchange: 广播型 广播exchange下多有queue
     * TopicExchange: 通过routeKey和 Binding Key 进行模糊匹配 进行路由发送queue
     * HeadersExchange:而根据消息中的 Headers 和创建绑定关系时指定的 Arguments 来匹配决定路由到哪些 Queue基本不用
     */

    /*
     * 设置交换器 exchange
     */
    @Bean
    public Exchange directExchange(){
        return new DirectExchange(exchange_direct);
    }

    @Bean
    public Exchange fanoutExchange(){

        return new FanoutExchange(exchange_fanout);
    }

    @Bean
    public Exchange topicExchange(){

        return new TopicExchange(exchange_topic);
    }

    /**
     * Queue创建有4个属性
     * @param name 队列名称
     * @param durable 设置是否为持久性队列 如果为持久队列,则为true(该队列将在服务器重新启动后继续有效,否则重启后失效) 默认为true
     * @param exclusive 设置是否为独占队列(该队列将仅由声明者的连接使用) 如果声明独占队列,则为true 默认为false
     * @param autoDelete 设置是否自动删除 当该队列不再使用了是否自动删除
     */

    /*创建queue*/

    /**
     * 声明创建一个queue
     * @return
     */
    @Bean
    public Queue directQueue1(){
        return  new Queue(queue_direct1,true,false,false);
    }

    @Bean
    public Queue directQueue2(){
        return  new Queue(queue_direct2,true,false,false);
    }

    @Bean
    public Queue fanoutQueue1(){
        return  new Queue(queue_fanoutQueue1,true,false,false);
    }

    @Bean
    public Queue fanoutQueue2(){
        return  new Queue(queue_fanoutQueue2,true,false,false);
    }

    @Bean
    public Queue topicQueue1(){
        return  new Queue(queue_topic1,true,false,false);
    }

    @Bean
    public Queue topicQueue2(){
        return  new Queue(queue_topic2,true,false,false);
    }


    /* 将exchange与topic进行绑定*/

    @Bean
    public Binding directBinding1(){

        return BindingBuilder.bind(directQueue1()).to(directExchange()).with(routingKey_direct1).noargs();
    }

    @Bean
    public Binding directBinding2(){

        return BindingBuilder.bind(directQueue2()).to(directExchange()).with(routingKey_direct2).noargs();
    }

    @Bean
    public Binding fanoutBinding1(){

        return BindingBuilder.bind(fanoutQueue1()).to(fanoutExchange()).with("").noargs();
    }

    @Bean
    public Binding fanoutBinding2(){

        return BindingBuilder.bind(fanoutQueue2()).to(fanoutExchange()).with("").noargs();
    }

    @Bean
    public Binding topicBinding1(){
        return BindingBuilder.bind(topicQueue1()).to(topicExchange()).with(routingKey_topic1).noargs();
    }

    @Bean
    public Binding topicBinding2(){

        return BindingBuilder.bind(topicQueue2()).to(topicExchange()).with(routingKey_topic2).noargs();
    }


}

在项目中将需要创建的exchange和queue进行 binding完成之后 ,此时我们去构建生产者相关配置,

2:RabbitmqProducerAutoConfiguration

该类用于对生产者进行配置,包含对公共的异步ack消息处理

/**
 * 对生产者进行设置
 * @author fangyuan
 */
@Configuration
@Slf4j
public class RabbitmqProducerAutoConfiguration {

    /**
     *
     * 若需要 使用到returnCallback 与confirmCallback 生产者确认机制
     * 其中confirmCallback:消息从生产者到达exchange时返回ack,消息未到达exchange返回nack(ack为false);
     * returnCallBack:消息进入exchange但未进入queue时会被调用  这个时候说明路由失败了 需要进行 设置
     * 需要在配置中使用以下配置:
     * publisher-returns: true
     * publisher-confirm-type: correlated
     *
     */

    /**
     * 设置生产者的生产消息的ack信息回调(公共处理)
     * @return
     */
    @Bean
    public RabbitTemplate.ConfirmCallback confirmCallback(){

        //返回
        return (correlationData, ack, cause)->{
            //我们可以通过correlationData原始数据 来对消息进行后续处理,当时这是有个要求在于发送必须使用CorrelationData类
            //成功
            if(ack){
                log.info("消息发送成功!!!!!,消息data:{},时间:{}",correlationData,System.currentTimeMillis());
            }else {
                log.error("消息发送失败!!!!,原因是:{}",cause);
            }

        };
    }

    /**
     * 发送者失败通知
     */
    @Bean
    public RabbitTemplate.ReturnCallback returnCallback(){

        //构建一个
        return (Message message, int replyCode, String replyText, String exchange, String routingKey)->{
            log.error("发送者路由失败,请检查路由 Returned replyCode:{} Returned replyText:{} Returned routingKey:{} Returned message:{}"
                    ,  replyCode,replyText,routingKey,new String(message.getBody()));
        };
    }

    //设置RabbitTemplate
    @Bean
    public RabbitTemplate rabbitTemplate(RabbitTemplateConfigurer configurer, ConnectionFactory connectionFactory) {
        RabbitTemplate template = new RabbitTemplate();
        configurer.configure(template, connectionFactory);
        //若同步阻塞该ConfirmCallback 不用设置
//        template.setConfirmCallback(confirmCallback());
        template.setReturnCallback(returnCallback());
        return template;
    }

}

 

3: application.yml 基础设置

spring:
  rabbitmq:
    addresses: localhost:5672
    username: admin
    password: admin
    virtual-host: my_vhost
    ##通过returnCallback
    publisher-returns: true
    ##通过confirmCallback
    publisher-confirm-type: simple
    template:
      mandatory: true

4: 生产者API测试

4.1:测试生产者同步与异步ACK确认机制

同步阻塞ACK确认发送,需要将    publisher-confirm-type: simple 并且template中不需要设置ConfirmCallback。

 //测试发送阻塞消息
    @Test
    public void testSendMessageSync(){

        RabbitTemplate rabbitTemplate = rabbitMessagingTemplate.getRabbitTemplate();

        Boolean flag = rabbitTemplate.invoke(operations -> {
            rabbitTemplate.convertAndSend(CommonConstant.exchange_direct, CommonConstant.routingKey_direct2, "测试数据direct2");
            log.info("发送message==============》");
            //同步阻塞
            return rabbitTemplate.waitForConfirms(3 * 1000);
        });
        //同步ack确认
        if(flag){
            log.info("消息发送成功");
        }else {
            //消息消费失败 后续处理 重发(消费者需要进行幂等控制)或其它
            log.error("消息发送失败");
        }
    }

可以在rabbitmq控制台可以看到该条消息被创建但未消费

异步ACK确认发送, publisher-confirm-type: correlated 并且在设置ConfirmCallback,可以统一设置ConfirmCallback,也每种发送定制一种ConfirmCallback。

公共设置可参考RabbitmqProducerAutoConfiguration中。send代码如下:

 //定制
@Test
    public void testSendMessageASyncCustomized(){

        RabbitTemplate rabbitTemplate = rabbitMessagingTemplate.getRabbitTemplate();

        rabbitTemplate.convertAndSend(CommonConstant.exchange_direct, CommonConstant.routingKey_direct2, "测试数据direct2");

}

运行测试代码可以查看到日志,得到:

从上述可以发现correlationData为null,我们需要在send时创建correlationData被写入。代码如下所示:

@Test
    public void testSendMessageASyncCustomized(){

        RabbitTemplate rabbitTemplate = rabbitMessagingTemplate.getRabbitTemplate();
        
        CorrelationData correlationData = new CorrelationData();
        correlationData.setId("1");

        rabbitTemplate.convertAndSend(CommonConstant.exchange_direct, CommonConstant.routingKey_direct2, "测试数据direct2",correlationData);
}

最终结果如下所示,correlationData不会为空,并且携带的id包含在呢。

springboot2.x +rabbitmq使用和源码分析二(生产者配置)_第1张图片

查看CorrelationData源码发现,携带的数据很少,若回调ack中携带更加丰富消息,例如exchange,queue,retry次数,我们可以重写correlationData。


//这里需要对CorrelationData进行序列化 后续说到消息转换器说到
//默认支持 string 实现了Serializable的java类 byte[]
@Data
public class CorrelationData extends org.springframework.amqp.rabbit.connection.CorrelationData implements Serializable {

    //消息体
    private volatile Object message;
    //交换机
    private String exchange;
    //路由键
    private String routingKey;
    //重试次数
    private int retryCount = 0;

}

再次发送,最终的结果如下所示:

springboot2.x +rabbitmq使用和源码分析二(生产者配置)_第2张图片

 

定制发送可以通过在send时候指定如下所示:

4.3:测试ReturnCallback作用

指定发送一个消息到不存在的queue中,发现执行了以下代码

springboot2.x +rabbitmq使用和源码分析二(生产者配置)_第3张图片

日志打印结果:

发送者路由失败,请检查路由 Returned replyCode:312 Returned replyText:NO_ROUTE Returned routingKey:exchange:com.fc.exchange.direct Returned message:routingKey:com.fc.routingKey.direct1

发现 ConfirmCallback中得到的结果ack结果为成功,所以这也证明了rabbitmq的一个机制,数据先到exchange,exchange这时候发送一个ack机制,但并不代表这条消息就真的能落地了,若exchange路由queue发生了异常,若我们不设置ReturnCallback机制,就会出现丢数据的情况。在实际工作中,若我们面对的是消息一致性要求高,不要丢失数据,我们需要对ReturnCallback与ConfirmCallback要同时设置并就行补偿流程设置

4.3:测试不同的exchange与queue的实际效果

测试代码如下:

 @Test
    public void testSendMessageBatchASync(){

        //convertAndSend 使用此方法,交换机会马上把所有的信息都交给所有的消费者,消费者再自行处理,不会因为消费者处理慢而阻塞线程。
        //convertSendAndReceive 使用此方法,当确认了所有的消费者都接收成功之后,才触发另一个convertSendAndReceive
        //发送direct类型数据
        log.info("开始发送!!!!");
        RabbitTemplate rabbitTemplate = rabbitMessagingTemplate.getRabbitTemplate();

        //发送direct
        rabbitTemplate.convertAndSend(CommonConstant.exchange_direct, CommonConstant.routingKey_direct1,"测试数据direct1");
        rabbitTemplate.convertAndSend(CommonConstant.exchange_direct, CommonConstant.routingKey_direct2, "测试数据direct2");
        //发送fanout类型数据
        rabbitTemplate.convertAndSend(CommonConstant.exchange_fanout, "", "测试数据fanout1");
        rabbitTemplate.convertAndSend(CommonConstant.exchange_fanout, "", "测试数据fanout2");
        //发送topic类型数据
        rabbitTemplate.convertAndSend(CommonConstant.exchange_topic, CommonConstant.routingKey_c_topic1, "测试数据topic1");
        rabbitTemplate.convertAndSend(CommonConstant.exchange_topic, CommonConstant.routingKey_c_topic2, "测试数据topic2");


    }

我们通过对exchange 3种常用进行测试,期盼的上述顺序队列种的数据条数为为1,1,2,2,1,1

注:在执行多条写入的过程中会出现这样的情况,后面几条中ack为false,异常信息为clean channel shutdown; protocol method: #method(reply-code=200, reply-text=OK, class-id=0, method-id=0),但是发现queue队列中却写入成功了。这是因为上述代表运行在单元测试环境中,有因为是异步回调,所以存在主方法执行完成,rabbitmq通道被关闭,最终ack正确消息无法被回调,就出现如下情况。我们只需要对主方法sleep即可

最终执行结果为:

springboot2.x +rabbitmq使用和源码分析二(生产者配置)_第4张图片

5:批量发送

AMQP在协议上规定每次只能传送一条数据,因此做批量数据操作,需要在应用层上定义,Spring 目前通BatchingRabbitTemplate在上层应用中提供了批量使用( 所谓批量, 就是spring 将多条message重新组成一条message, 发送到mq, 从mq接受到这条message后,在重新解析成多条message),BatchingRabbitTemplate定义如下所示:

@Bean
    public BatchingRabbitTemplate batchingRabbitTemplate(ConnectionFactory connectionFactory){

        //可自定义对该pool设置
        ThreadPoolTaskScheduler taskScheduler=new ThreadPoolTaskScheduler();
        taskScheduler.setPoolSize(5);
        taskScheduler.setDaemon(true);
        taskScheduler.setThreadNamePrefix("rabbitmq-");
        taskScheduler.setAwaitTerminationSeconds(60);
        taskScheduler.setWaitForTasksToCompleteOnShutdown(true);
        // 初始化
        taskScheduler.initialize();
        //一次批量的数量
        int batchSize=10;
        // 缓存大小限制,单位字节,
        // simpleBatchingStrategy的策略,是判断message数量是否超过batchSize限制或者message的大小是否超过缓存限制,
        // 缓存限制,主要用于限制"组装后的一条消息的大小"
        // 如果主要通过数量来做批量("打包"成一条消息), 缓存设置大点
        // 详细逻辑请看simpleBatchingStrategy#addToBatch()
        int bufferLimit=1024; //1 K
        long timeout=10000;

        //注意,该策略只支持一个exchange/routingKey
        BatchingStrategy batchingStrategy=new SimpleBatchingStrategy(batchSize,bufferLimit,timeout);

        return  new BatchingRabbitTemplate(connectionFactory,batchingStrategy,taskScheduler);
    }

测试发送代码:

 @Test
    public void testBatchSendMessage(){
        // 除了send(String exchange, String routingKey, Message message, CorrelationData correlationData)方法是发送单条数据
        // 其他send都是批量
        String msg;
        Message message;
        MessageProperties messageProperties=new MessageProperties();
        for(int i=0;i<1000;i++){
            msg="测试批量数据"+i;
            message=new Message(msg.getBytes(), messageProperties);
            batchingRabbitTemplate.send(CommonConstant.exchange_direct,CommonConstant.routingKey_direct2,message);
        }

        System.out.println("发送数据完毕");
    }

//ack确认与非ack确认最终实现效果一致 写入mq都为100条
@Test
    public void testBatchSendMessageAck(){
        // 除了send(String exchange, String routingKey, Message message, CorrelationData correlationData)方法是发送单条数据
        // 其他send都是批量
        String msg;
        Message message;
        MessageProperties messageProperties=new MessageProperties();
        for(int i=0;i<1000;i++){
            msg="测试批量数据"+i;
            message=new Message(msg.getBytes(), messageProperties);
//            batchingRabbitTemplate.send(CommonConstant.exchange_direct,CommonConstant.routingKey_direct2,message);
            Message finalMessage = message;
            Boolean flag = batchingRabbitTemplate.invoke(operations -> {
                batchingRabbitTemplate.convertAndSend(CommonConstant.exchange_direct, CommonConstant.routingKey_direct2, finalMessage);
                log.info("发送message==============》");
                //同步阻塞
                return batchingRabbitTemplate.waitForConfirms(3 * 1000);
            });
            //同步ack确认
            if(flag){
                log.info("消息发送成功");
            }else {
                //消息消费失败 后续处理 重发(消费者需要进行幂等控制)或其它
                log.error("消息发送失败");
            }

        }

        System.out.println("发送数据完毕");
    }

我们共发送了1000条数据, 通过控制台查看最终结果:

 

springboot2.x +rabbitmq使用和源码分析二(生产者配置)_第5张图片

从上面数据存储来看,spring将10(batchSize设置)条 数据压缩成一条发送给mq(为何1000变化为100),一般我们通过batch发送的数据(为何这么处理原因在于 batchlisteren看到headers头中包含amqp_batchSize,会讲数据进行解压还原为10条),关于批量消费详情查看后续对消费者描述

6:设置消息的TTL

在某些mq的应用场景中,消息是有过期时间,例如:我们一般对于发送验证码短信这个动作都会通过mq来进行异步推送,而每一个验证码都有属于自己的过期时间(短的1min长的有半个小时),如果我们mq因为某些原因阻塞了,本该马上发出的消息,却在1min之后发出,那么这时候发出的短信毫无意义。所以我们需要进行设置消息的TTL,一旦超时,就写入到死信队列(关于此可以参考后文的消费者),这时候监控消息队列的cousumer就会获取该动作,触发重新获取验证码的动作。

目前rabbitmq有两个地方可以设置ttl:

  1. 第一种方式是通过队列属性设置,队列中所有消息都有相同的过期时间
  2. 第二种方式是对消息本身进行单独的设置,每条消息的TTL可以不同,在发送时指定

测试代码演示:

第一种:构建一个ttl的队列并设置ttl=10s,并绑定死信队列,在发送20条message,在超时时间内不进行消费

构建队列代码: 

 @Bean
    public Queue ttlQueue(){

        Map arguments  = new HashMap<>();

        arguments.put("x-message-ttl",10*1000);

        //声明当前死信的 Exchange 10s超时

        arguments.put("x-dead-letter-exchange",exchange_deadLetter_direct);

        arguments.put("x-dead-letter-routing-key",routingKey_deadLetter);

        return  new Queue(queue_ttl,true,false,false,arguments);
    }

    @Bean
    public Binding ttlBinding(){

        return BindingBuilder.bind(ttlQueue()).to(directExchange()).with(routingKey_ttl).noargs();
    }

发送端:

 @Test
    public void testSendMessageTTL(){

        for (int i=0;i<20;i++){
            rabbitTemplate.convertAndSend(CommonConstant.exchange_direct,CommonConstant.routingKey_ttl,"这是一条测试数据"+i);
        }


    
        try {
            Thread.sleep(60*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

发送之前:

ttl队列: 

死信队列: 

 第二种 指定message的过期时间,不用设置队列TTL

@Test
    public void testSendMessageTTL(){
        //

        for (int i=0;i<20;i++){
            rabbitTemplate.convertAndSend(CommonConstant.exchange_direct,CommonConstant.routingKey_ttl,"这是一条测试数据"+i);
        }


        for (int i=0;i<20;i++){
            rabbitTemplate.convertAndSend(CommonConstant.exchange_direct,CommonConstant.routingKey_ttl,"这是一条测试数据"+i,(message->{
                //设置过期时间时间
                message.getMessageProperties().setExpiration("10000");
                return message;
            }));
        }

        try {
            Thread.sleep(60*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

这里20条不设置ttl,20条设置10sttl

最终结果:

ttl开始包含40条

经过10s后,有20条消息进入死信队列中,剩下20条仍在队列中

 

通过上述测试发现,在无消费者消费的情况,一旦设置超过了过期时间数据被清除,如果设置了死信则转储死信队列。

6:设置消息优先级

还是拿短信发送来描述该功能的作用,在实际中我们会将短信发送进行异步处理,而短信发送来源很多,例如有登陆验证码短信,注册验证码短信,交易通知短信,营销类的短信。这些短信我们不可能根据类型来构建不同的queue,通常都会存放到一个队列中,那么在大量消息到来之后,消费者消费能力不足就会产生消息堆积。那么对于交易通知短信这种很重要的短信就会因为营销类短信先到而无法发送。如果队列能提供一种level的概念,那么对于等级高的,在消费能力不足的情况下,等级高的先被消费,等级低的后消费。Rabbitmq就通过对队列设置x-max-priority(设置当前队列最大的level等级)参数,对消息设置Priority参数来做到。

代码如下所示:

queue构建:

  @Bean
    public Exchange priorityQExchange(){
        return new DirectExchange(exchange_priority);
    }

    /**
     *
     * @return
     */
    @Bean
    public Queue priorityQueue(){

        Map arguments  = new HashMap<>();
        arguments.put("x-max-priority",200);

        return  new Queue(queue_priority,true,false,false,arguments);
    }


    @Bean
    public Binding priorityBinding(){
        return BindingBuilder.bind(priorityQueue()).to(priorityQExchange()).with(routingKey_priority).noargs();
    }

发送者:

  @Test
    public void testSendMessagePriority(){

        for (int i=1;i<200;i++){
            final  int j = i;
            rabbitTemplate.convertAndSend(CommonConstant.exchange_priority,CommonConstant.routingKey_priority,"这是一条测试数据"+i,message -> {
                //设置
                message.getMessageProperties().setPriority(j);
                return message;
            });
        }
        
        try {
            Thread.sleep(600*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

最终结果:

在Rabbitmq控制台优先级队列中都有Pri标示

 消费者:

springboot2.x +rabbitmq使用和源码分析二(生产者配置)_第6张图片

注意:spring.ribbitmq.simple.prefetch的设置,该值代表每个customer会在MQ预取一些消息放入内存的LinkedBlockingQueue中,这个值越高,消息传递的越快,但非顺序处理消息的风险更高,如果ack模式为none,则忽略。如有必要,将增加此值以匹配txSize或messagePerAck。从2.0开始默认为250;设置为1将还原为以前的行为。prefetch默认值以前是1,会导致高效cousumer的利用率不足(频繁和服务端进行交互)。从spring-amqp 2.0版开始,默认的prefetch值是250,这将使消费者在大多数常见场景中保持忙碌,从而提高吞吐量。

不过在有些情况下,尤其是处理速度比较慢的大消息,消息可能在内存中大量堆积,消耗大量内存;以及对于一些严格要求顺序的消息,prefetch的值应当设置为1,上述中如果要使用到消息优先级改值的也不易设置过大。

对于低容量消息和多个消费者的情况(也包括单listener容器的concurrency配置)希望在多个使用者之间实现更均匀的消息分布,建议在手动ack下并设置prefetch=1

对于消费端更加详细的介绍。可阅读springboot官方文档:https://docs.spring.io/spring-amqp/reference/html/#amqp

Demo地址:https://github.com/fangyuan94/rabbitmq_demo.git 

你可能感兴趣的:(rabbitmq,SpringBoot,rabbitmq)