1.概述

RabbitMQ有几个重要的概念:虚拟主机,交换机,队列和绑定

  • 虚拟主机:一个虚拟主机持有一组交换机、队列和绑定,我们可以从虚拟主机层面的颗粒度进行权限控制
  • 交换机:Exchange用于转发消息,它并不存储消息,如果没有Queue队列绑定到Exchange,它会直接丢弃掉生产者发来的数据。
    交换机还有个关联的重要概念:路由键,消息转发到哪个队列根据路由键决定
  • 绑定:就是绑定交换机和队列,它是多对多的关系,也就是说多个交换机可以绑同一个队列,也可以一个交换机绑多个队列

2.交换机

交换机有四种类型的模式Direct, topic, Headers and Fanout

2.1 Direct Exchage

Direct模式使用的是RabbitMQ的默认交换机,也是最简单的模式,适合比较简单的场景

如下图所示,使用Direct模式,我们需要创建不同的队列,而默认交换机则通过Routing key路由键的值来决定转发到哪个队列,可以看到,路由键绑定队列是可以指定多个的

                        

rabbitmq 删除交换机 springboot_spring

2.2  Topic Exchange

Topic模式主要是根据通配符匹配,也就类似于模糊匹配,当这种匹配模式和路由键匹配后交换机就能转发消息到指定队列

  • 路由键为一串字符串,由句号(.)隔开,比如a.b.c
  • *)代表指定位置一个单词,(#)代表零个或者多个单词,比如a.*.b.#,表示a和b中间随意填个单词,b后面可以跟n个单词,比如a.x.b.c.d.e

Topic模式和Direct模式的区别在于交换机需要自己指定,路由键支持模糊匹配,例如:

rabbitTemplate.convertAndSend("topicExchange","a.x.b.d", " hello world!");

2.3 Headers Exchage

Headers也是根据规则匹配,但它不是根据路由键了,headers有个自定义匹配规则,它将匹配键值设在了消息的headers属性上,当这些键值对有一对或者全部匹配时,消息才会被投递到对应队列,这种模式效率相对较低,一般不推荐使用

2.4 Fanout Exchange

Fanout即为大名鼎鼎的广播模式了,它不需要管路由键,会把消息发给绑定它的全部队列,就算配置了路由键也会被忽略

3. 复杂情况

  1. 首先我们Direct模式,一个生产者一个消费者的情况,也就对应了一个发送者和一个队列A接收,这是没有疑问的,发送什么接收什么
  2. 当Direct模式,一个生产者发消息,开启多个消费者也就是多个相同queue,此时消息由多个消费者均匀分摊,不会重复消费(前提ack正常)
  3. 当Topic模式,一个交换机绑定两个队列,路由键有重叠关系,如下代码,此时指定路由键topic.message发送消息,队列queueMessagequeueMessages都能接收到相同消息,也就是说,topic模式可以实现类似于广播模式的形式,甚至更加灵活,它能否转发到消息由路由键决定。
  4. 相比于Fanout模式,我们如果要对消费者队列分组发送,我们需要指定不同的路由键;而Fanout模式则需要指定不同的交换机和队列绑定,实际使用结合实际情况

4. Demo 代码

4.1 pom

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

4.2 配置文件 

spring:
  rabbitmq:
    #    addresses: 10.111.36.4
    addresses: localhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /saas-mail
    mail-queue: mail_queue
    publisher-confirms: true
    publisher-returns: true
    template:
      mandatory: true
    listener:
      simple.acknowledge-mode: manual
    simple:
      concurrency: 5
      max: 10
  1. spring.rabbitmq.publisher-confirms为true,表示生产者消息发出后,MQ的broker接收到了消息,发送回执表示确认接收,不设置则可能导致消息丢失
  2. spring.rabbitmq.publisher-returns为true,表示当消息不能到达MQ的Broker端,,则使用监听器对不可达的消息做后续处理,这种一般是路由键没配好,或MQ宕机才可能发生
  3. spring.rabbitmq.template.mandatory当上面两个为true时,这个一定要配true,否则上面两个不起作用
  4. spring.rabbitmq.listener.simple.acknowledge-mode这个为manual表示手工确认,实际生产应该设为手工,才能保证你的业务是处理完成的,注意业务的幂等性,可重复调用,手工确认代码如下例子

4.3 java 代码

  • rabbit 配置类
/**
 * @Description rabbit provider
 * @Author yanghanwei
 * @Mail yanghanwei@geotmt.com
 * @Date 17:58 2019-11-25
 * @Version v1
 **/
@Configuration
public class DirectRabbitConfig {

    private static final Logger logger = LoggerFactory.getLogger(DirectRabbitConfig.class);

    @Value("${spring.rabbitmq.mail-queue}")
    private String queueName;

    /**
     * 邮件交换机名称
     */
    public static final String mail_Direct_Exchange = "mailDirectExchange";

    /**
     * 邮件路邮键
     */
    public static final String mail_Direct_Routing = "mailDirectRouting";

    @Autowired
    private CachingConnectionFactory connectionFactory;

    @Bean
    public Queue testDirectQueue() {
        //true 是否持久化
        return new Queue(queueName, true);
    }

    /**
     * Direct交换机 起名:mailDirectExchange
     *
     * @return
     */
    @Bean
    DirectExchange directExchange() {
        return new DirectExchange("mailDirectExchange", true, false);
    }

    /**
     * 绑定 将队列和交换机绑定, 并设置用于匹配键:mailDirectRouting
     *
     * @return
     */
    @Bean
    Binding bindingDirect() {
        return BindingBuilder.bind(testDirectQueue()).to(directExchange()).with("mailDirectRouting");
    }

    @Bean
    public RabbitTemplate rabbitTemplate() {
        /**
         * 若使用confirm-callback或return-callback,必须要配置publisherConfirms或publisherReturns为true
         * 每个rabbitTemplate只能有一个confirm-callback和return-callback,如果这里配置了,那么写生产者的时候不能再写confirm-callback和return-callback
         * 使用return-callback时必须设置mandatory为true,或者在配置中设置mandatory-expression的值为true
         */
        connectionFactory.setPublisherConfirms(true);
        connectionFactory.setPublisherReturns(true);
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMandatory(true);
        /**
         * 如果消息没有到exchange,则confirm回调,ack=false
         * 如果消息到达exchange,则confirm回调,ack=true
         * exchange到queue成功,则不回调return
         * exchange到queue失败,则回调return(需设置mandatory=true,否则不回回调,消息就丢了)
         */
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String msg) {
                if (ack) {
                    logger.info("Message send success : correlationData({}),cause({})", correlationData, msg);
                } else {
                    logger.info("Message send failure : correlationData({}),cause({})", correlationData, msg);
                }
            }
        });
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            @Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                logger.info("Message miss : exchange({}),route({}),replyCode({}),replyText({}),message:{}", exchange, routingKey, replyCode, replyText, message);
            }
        });
        return rabbitTemplate;
    }
}

队列属性

  • queue :队列的名字
  • durable :为true表示队列中数据持久化到磁盘,可以防止mq宕机重启数据丢失
  • exclusive :为true表示排他性,只允许一个当前连接访问该队列,当前已连接就不允许新的连接进入否则报错,当连接断开当前队列会销毁
  • autoDelete :为true表示自动删除,当没有Connection连接到队列的时候,会自动删除
  • arguments :这个参数用来添加一些额外参数的,如下图片
  • 比如添加x-message-ttl为5000,则表示消息超过5秒没被处理就会超时过期;
  • x-expires设置120000表示队列在2分钟内没被消费则被删除;
  • x-max-length,x-max-length-bytes表示传送数据的最大长度和字节数
  • x-dead-letter-exchangex-dead-letter-routing-key表示死信交换机和死信路由,放在需要过期或处理失败的队列属性中,这些数据会转发到死信队列存储起来,创建普通的交换机和队列绑定,把交换机名填到x-dead-letter-exchange的值,填写路由键要符合死信队列的路由键
  • x-max-priority,表示设置优先级,范围为0~255,只有当消息堆积的时候,这个优先级才有意义,数字越大优先级越高
  • x-queue-mode当为lazy,表示惰性队列,3.6.0之后才被引入的概念,相比默认的模式,惰性队列模式会将生产者产生的消息直接存到磁盘中,这当然会增加IO开销,但适合应对大量消息堆积的情况;因为当大量消息堆积时,内存也不够存放,会将消息转存到磁盘,这个过程也是比较耗时且过程中不能接收新的消息。如果需要将普通队列转换成惰性队列需要将原来的队列删除,重新创建个惰性队列绑定。

交换机属性

  1. exchange :交换机名称
  2. type :交换机类型
  3. durable :持久化,同队列
  4. autoDelete :是否自动删除,同队列
  5. internal :若为true,表示这个exchange不可以被client用来推送消息,仅用来进行exchange和exchange之间的绑定。
  6. arguments :额外参数,目前只有个alternate-exchange,表示当生产者发送消息到这个交换机,路由不到该交换机的队列,则会尝试这个参数指定的交换机进行路由,若路由键匹配,则路由到alternate-exchange指定的队列,相当于转发了,刚好和上一个参数internal配合,若不想本交换机起到路由队列的作用,可以设置internal为true,把消息都转发到alternate-exchange指定的交换机,由该交换机来路由指定队列,

  • rabbit 生产者
/**
 * @Description 生产者
 * @Author yanghanwei
 * @Mail yanghanwei@geotmt.com
 * @Date 11:51 2019-11-26
 * @Version v1
 **/
@Component
public class RabbitSender {

    private static final Logger logger = LoggerFactory.getLogger(RabbitSender.class);

    @Autowired
    private AmqpTemplate rabbitTemplate;

    public void send(MailQueueMessageVo mailQueueMessageVo) {
        logger.info("======= RabbitMQ ======= sender message: {}", mailQueueMessageVo);
        rabbitTemplate.convertAndSend(DirectRabbitConfig.mail_Direct_Exchange, DirectRabbitConfig.mail_Direct_Routing, JSONObject.toJSONString(mailQueueMessageVo));
    }
}

rabbit 消费者(监听)

/**
 * @Description mq 监听
 * @Author      yanghanwei
 * @Mail        yanghanwei@geotmt.com
 * @Date        09:42 2019-12-02
 * @Version     v1
 **/
@Configuration
public class AmqpListener {

    private static final Logger logger = LoggerFactory.getLogger(AmqpListener.class);

    @Value("${spring.rabbitmq.mail-queue}")
    private String mailQueue;

    @Autowired
    private SendMailUtils sendMailUtils;

    @Autowired
    private AckMailMsgService ackMailMsgService;

    @Bean
    public SimpleMessageListenerContainer messageContainer(ConnectionFactory connectionFactory) {

        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
        // 可监听多个队列
        container.setQueues(new Queue(mailQueue));
        // 设置当前消费者数量为1
        container.setConcurrentConsumers(1);
        // 设置最大的消费者数量为5
        container.setMaxConcurrentConsumers(5);
        // 是否重回队列:否
        container.setDefaultRequeueRejected(false);
        // 设置自动签收模式
        container.setAcknowledgeMode(AcknowledgeMode.AUTO);
        // 自定义消费者标签生成策略(队列名+uuid)
        container.setConsumerTagStrategy(new ConsumerTagStrategy() {
            @Override
            public String createConsumerTag(String queue) {
                return queue + "_" + UUID.randomUUID().toString();
            }
        });

        /*
         * 第一种监听方式:
         * 设置消息监听,ChannelAwareMessageListener是Spring AMQP提供的匿名的监听接口
         * 如果有一条消息发送到队列,则会被监听器监听到,进入onMessage方法
         */
        container.setMessageListener(new ChannelAwareMessageListener() {
            @Override
            public void onMessage(Message message, Channel channel) throws Exception {
                String msg = new String(message.getBody());
                try{
                    // send(sendMailUtils, msg);
                    logger.info("msg:{}",msg)
                    channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); //确认消息成功消费
                }catch (Exception e){
                    logger.error(" send mail failed" + e.getMessage(), e);
                    // 失败补偿,当下立即补偿一次,剩下的定时任务扫描,失败队列补偿,最多重试3次
                    // Long id = ackMailMsgService.insert(new AckMailMsgVo().setMailMsgStr(msg));
                    // 补偿消费
                    // compensationConsumption(sendMailUtils, ackMailMsgService, msg, id);
                    // 第三个参数:    true 重新放进队列, false 抛弃此条消息
                    channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,false);
                }
            }
        });

        // 第二种监听方式: 适配器模式,自定义一个delegate委托进行监听
        // container.setMessageListener(new MessageListenerAdapter(new MessageDelegate()));

        return container;
    }


    /**
     * @Description mq 监听
     * @Author      yanghanwei
     * @Mail        yanghanwei@geotmt.com
     * @Date        10:26 2019-12-02
     * @Version     v1
     **/
    public class MessageDelegate {
        public void handleMessage(String msg) {
            try {
                send(sendMailUtils, msg);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

}