rabbitmq几种交换机的理解以及实践

交换机

  • direct 直连
    把消息路由到那些binding key与routing key完全匹配的Queue

  • topic Routing Key必须与Binding Key相匹配的时候才将消息传送给Queue
    和direct 都需要绑定route-key
    topic的#和*,# 表示和多个关键字匹配,* 和一个关键字匹配

  • header 匹配不依赖于route-key和banding-key,会根据header中的值来 匹配规则,与queue绑定的时候不需要绑定route-key,但是可以设置x-match字段为any或者all,设置为any,则只要发送消息的header和绑定的header有一个相符合,则转发给队列,如果设置为all,header必须全部符合才转发给队列 ,因为按照header来匹配

  • fanout
    它会把所有发送到该Exchange的消息路由到所有与它绑定的Queue中。
    与queue绑定的时候不需要指定route-key(设置routeKey为""),发送给与fanout绑定的所有队列

Channel与Connection

Connection 是真实的TCP连接
Channel 是在TCP连接上虚拟的多个信道,,就像一条网线是一个Connect,,如果客户端程序是多线程的,,,有100人都要上网,为了降低成本,怎么办不可能拉100条网线吧。就给你每个人分了一个账号拨号上网,使用同一个网线。那怎么保证消息不会混乱呢? 每个账号就是唯一的标识,这个网线上的信息都会包含账号信息,,就是某条信息是哪个账号的。这样来保证消息不会混乱。

从ConnectionFactory可以生成connection,connection 可以生成Channel

channel可以声明 交换机,队列,绑定关系。发送消息。 很多工作都是由channel完成的。

两个很有用的类

  • RabbitAdmin // 封装了 对Rabbitmq的管理
    // Rabbitmq管理Bean
    @Bean
    public RabbitAdmin ampqManager(@Qualifier("connectionFactory") ConnectionFactory connectionFactory) throws IOException, TimeoutException {
        RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory);
         // 申明交换机
        rabbitAdmin.declareExchange(getExchange());
        // 声明队列
        rabbitAdmin.declareQueue(getQueue());
        // 交换机和队列的绑定
        rabbitAdmin.declareBinding(new Binding("queue1",Binding.DestinationType.QUEUE,"my_topicExchange","route-topic",null));
        return rabbitAdmin;
    }
  • RabbitTemplate 简化了数据收发
    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)  //
    public RabbitTemplate rabbitTemplate(@Qualifier("connectionFactory") ConnectionFactory connectionFactory) {
        return new RabbitTemplate(connectionFactory);
    }

数据收发使用RabbitTemplate中的现成的方法。

发送数据的底层是调用的basicPublish方法,
这个方法由三个重载

void basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) throws IOException;

void basicPublish(String exchange, String routingKey, boolean mandatory, BasicProperties props, byte[] body)
            throws IOException;

void basicPublish(String exchange, String routingKey, boolean mandatory, boolean immediate, BasicProperties props, byte[] body)
            throws IOException;

共有的参数是

  • exchange 交换机
  • routeKey 路由键
  • mandatory 设置为true 则如果过根据route-key找不到合适的队列,就调用rutern 将消息返还给发送者,如果设置为false,找不到队列直接丢弃。
重点总结:
1、几种交换机的效率以及特点
  • fanout模式 ,在设置交换机路由绑定的时候设置,会转发给每一个和当前fanout交换机绑定的队列,routekey设置为 “”。 效率最高
  • topic模式,,即队列只接收关心的topic。 设置交换机,队列绑定的时候 灵活的routeKey。 如 topic1.*可以匹配后面一个任意字段(topic.xxx),topic.#可以匹配任意多个字段(topic.xixi.heihei) 效率次之。 通常还是用的比较多的。消息分类处理,,发送不同类型的消息的时候指定不同的routekey 来使得其进入不同的队列。。设置监听器的时候,通过监听不同的队列,来对不同的消息进行消费。
  • direct交换机 直接连接,只有routekey完全匹配才会发送消息到对应的队列。适用于需要单独处理的消息。效率最低
2 消息监听的两种方式

1 注解监听。
优点:简单
缺点:不够灵活。效率没有监听器高。

    @RabbitListener(queues = "directQueue")
    public void receiveMessage(@Payload Message message) {
        System.out.println("收到消息" + message.getBody().toString());
    }

2 设置监听器。
理解: 监听器就像一个容器,就像班主任站在讲台上,下面每个学生都是一个队列,班主任监听,谁说话,就让体育老师抓出去去教育一顿。

  • 教室和班主任就相当于 监听容器
  • 学生 相当于放在容器中监听的队列
  • 学生说话 相当于 队列消息事件
  • 体育老师 相当于消费者
  • PreFechCount 相当于设置体育老师一次抓出去的学生数量。一个一个抓效率低。有三个学生说话的,一次抓三个出去一个一个处理,处理完了之后再回来继续抓。
    @Bean
    public SimpleMessageListenerContainer cmdLogMessageContainer() throws IOException, TimeoutException {
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
        container.setConnectionFactory(connectionFactory());
        //队列丢失,true : 队列在容器运行时被移除,则容器停止;false : 继续尝试启动消费者,每个消费者在每次恢复尝试时都会进行3次尝试(间隔5秒)
        container.setMissingQueuesFatal(false); 
        //将队列放入监听容器
        container.setQueues(direct1(),topic1(),topic2());
        //监听通道打开,默认打开
        container.setExposeListenerChannel(true);
        //最大消费者数
        container.setMaxConcurrentConsumers(3);
        // 每个消费者的发生一次ack处理数量
        container.setPrefetchCount(100);
        //每次接收消费者数
        container.setConcurrentConsumers(3);
        //设置确认模式手工确认
        container.setAcknowledgeMode(AcknowledgeMode.AUTO);
        //设置监听通道,自定义监听器
        container.setMessageListener(new DirectHandler());
        return container;
    }
3、两个类 RabbitTemplate 和RabbitAdmin

这两个类是spring封装好的。使用RabbitMq这两个类也就足够了。
RabbitAdmin 来配置RabbitMq的,交换机,队列,绑定关系
RabbitTemplate 为我们封装了发送消息的方法,使得发送消变的更简单。

4、一个生产者对应多个消费者
5、 总之 ranbbitmq四个步骤

1 创建连接工厂Bean 这是第一步

    @Bean
    public ConnectionFactory connectionFactory() throws IOException, TimeoutException {
        CachingConnectionFactory factory = new CachingConnectionFactory();
        factory.setHost(rabbitmqSetting.getHost());
        factory.setPort(rabbitmqSetting.getPort());
        factory.setUsername(rabbitmqSetting.getUsername());
        factory.setPassword(rabbitmqSetting.getPassword());
        factory.setVirtualHost(rabbitmqSetting.getVirtualHost());
        factory.setPublisherConfirms(true);  // 设置消息回调
        return factory;
    }

2 声明交换机,队列,绑定关系。。这个需要 拿到连接工厂的Bean

    // Rabbitmq管理Beangit
    @Bean
    public RabbitAdmin ampqManager(@Qualifier("connectionFactory") ConnectionFactory connectionFactory) throws IOException, TimeoutException {
        RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory);
        // 申明交换机
        rabbitAdmin.declareExchange(getDirectExchange());
        rabbitAdmin.declareExchange(getExchange());  // topic Exchange
        rabbitAdmin.declareExchange(getFanoutExchange());


        // 声明队列 direct
        rabbitAdmin.declareQueue(direct1());
        rabbitAdmin.declareQueue(direct2());

        // fanout
        rabbitAdmin.declareQueue(fanout1());
        rabbitAdmin.declareQueue(fanout2());

        // topic
        rabbitAdmin.declareQueue(topic1());
        rabbitAdmin.declareQueue(topic2());


        // 交换机和队列的绑定
        rabbitAdmin.declareBinding(new Binding("directQueue", Binding.DestinationType.QUEUE, "my_direct", "my_direct", null));
        rabbitAdmin.declareBinding(new Binding("directQueue2", Binding.DestinationType.QUEUE, "my_direct", "my_direct", null));


        // topic
        rabbitAdmin.declareBinding(new Binding("topicQueue2", Binding.DestinationType.QUEUE, "my_topic", "my_topic.*", null));
        rabbitAdmin.declareBinding(new Binding("topicQueue", Binding.DestinationType.QUEUE, "my_topic", "my_topic.q2", null));


        //fanout 不用指定routekey  不能填写null  要填写""  fanout 广播模式,发送给所有的队列
        rabbitAdmin.declareBinding(new Binding("fanoutQueue1",Binding.DestinationType.QUEUE,"my_fanout","",null));

//        rabbitAdmin.purgeQueue("queue1");
        return rabbitAdmin;
    }

3 添加RabbitmqTemplate
,需要注意如果使用回调的话,在添加RabbitTemplate这个Bean的时候

    /**
     * <p>如果需要在生产者需要消息发送后的回调,需要对rabbitTemplate设置ConfirmCallback对象
     * <p>由于不同的生产者需要对应不同的ConfirmCallback
     * <p>如果rabbitTemplate设置为单例bean
     * <p>则所有的rabbitTemplate实际的ConfirmCallback为最后一次申明的ConfirmCallback
     * <p>SCOPE_PROTOTYPE 每次注入或者通过上下文获取的时候,都会创建一个新的bean实例
     */

    //Template获取连接信息,然后才可以使用Template的模板方法
    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)  //
    public RabbitTemplate rabbitTemplate(@Qualifier("connectionFactory") ConnectionFactory connectionFactory) {
        return new RabbitTemplate(connectionFactory);
    }

4 设置监听器

   @Bean
    public SimpleMessageListenerContainer cmdLogMessageContainer() throws IOException, TimeoutException {
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
        container.setConnectionFactory(connectionFactory());
        //队列丢失,true : 队列在容器运行时被移除,则容器停止;false : 继续尝试启动消费者,每个消费者在每次恢复尝试时都会进行3次尝试(间隔5秒)
        container.setMissingQueuesFatal(false);
        //将队列放入监听容器
        container.setQueues(direct1(),topic1(),topic2());
        //监听通道打开,默认打开
        container.setExposeListenerChannel(true);
        //最大消费者数
        container.setMaxConcurrentConsumers(3);
        // 每个消费者的发生一次ack处理数量
        container.setPrefetchCount(100);
        //每次接收消费者数
        container.setConcurrentConsumers(3);
        //设置确认模式手工确认
        container.setAcknowledgeMode(AcknowledgeMode.AUTO);
        //设置监听通道,自定义监听器
        container.setMessageListener(new DirectHandler());
        return container;
    }

5 消费者,实现自定义监听器

@Component
public class DirectHandler implements ChannelAwareMessageListener {
Logger logger = LoggerFactory.getLogger(this.getClass());

@Async
    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        logger.info("监听器1收到数据"+new String(message.getBody()));
    }
}
消息的延时

两种方式 ,

  • 1 通过设置ttl和死信交换机
  • 2 使用延时交换机插件
1 队列ttl 单条消息ttl
  • 队列ttl的原理是 给一条普通队列设置ttl时间,以及绑定死信交换机,,在队列中的消息过期以后会路由到死信交换机,由死信交换机处理。。。死信交换机和普通交换机用法相同。
    /**
   * 定义死信队列相关信息
   */
  public final static String deadQueueName = "dead_queue";   
  public final static String deadRoutingKey = "dead_routing_key";  
  public final static String deadExchangeName = "dead_exchange";   
  /**
   * 死信队列 交换机标识符
   */
  public static final String DEAD_LETTER_QUEUE_KEY = "x-dead-letter-exchange";
  /**
   * 死信队列交换机绑定键标识符
   */
  public static final String DEAD_LETTER_ROUTING_KEY = "x-dead-letter-routing-key";

  /**
   * 队列的ttl时间
   */
   
  public static final String QUEUE_MSG_TTL = "x-message-ttl";

    @Bean
  Queue ttlQueue() {
      Map<String, Object> map = new HashMap<>();
      map.put(QUEUE_MSG_TTL, 20000);  //20s的存活时间   // 指定队列的存活时间20s
      map.put(DEAD_LETTER_QUEUE_KEY, deadExchangeName);  // 指定死信交换机
      map.put(DEAD_LETTER_ROUTING_KEY, deadRoutingKey);  // route_key 死信交换机将用这个route_key来路由消息,作用于死信交换机和死信队列之间。
      return new Queue("ttlQueue", true, false, false, map);
  }
  • 消息ttl是在发送消息的时候指定Properties的,此时延时队列可以指定队列的ttl,也可以不指定,如果队列和消息都有ttl, 则按照时间小的来。
    @GetMapping("/deadByMsg")
  public void deadByMessage(@RequestParam(value = "time")String time) {
      logger.info("我是线程" + Thread.currentThread().getName());
      MessageProperties properties = new MessageProperties();
      properties.setDeliveryMode(MessageDeliveryMode.PERSISTENT); // 持久
      properties.setExpiration(time);   // 设置消息的ttl
      Message msg = new Message("测试死信消息---发送消息的时候指定ttl------我是测试延时队列".getBytes(), properties);
      provide.send("ttlExchange", "ttl_key", msg);
  }
2 延时交换机插件

实现消息延时发送有两种方式

  • 一种是通过上面设置队列ttl或者单条消息的ttl,设置死信交换机和死信队列,让消息超时以后由死信交换机处理。以此达到延时目的
  • 给rabbitmq安装延时插件,安装教程https://blog.csdn.net/liyongbing1122/article/details/81225761。 安装完成以后,rabbitmq就由了一种新的交换机类型"x-delayed-message",这个交换机发送消息。可以用如下方法设置延时发送
    // 通过插件的方式,创建 延时交换机
    @Bean
    public Exchange delayExchange(){

        Map<String, Object> args = new HashMap<String, Object>();
        args.put("x-delayed-type", "direct");
        return new CustomExchange("delayExchange", "x-delayed-message", true, false, args);    }

// 普通队列
    @Bean
    Queue delayQueue() {
        return new Queue("delayQueue");
    }
    
// 发送消息的时候,消息头添加 x-delay字段 指定延时时间
    @GetMapping("/delayByMsg")
    public void delayTask(@RequestParam(value = "time") Long time) {
        logger.info("我是线程" + Thread.currentThread().getName());
        MessageProperties properties = new MessageProperties();
        properties.setDeliveryMode(MessageDeliveryMode.PERSISTENT); // 持久
        String msg = "测试延时发送--发送消息的时候指定延时-----";
        // 方式1  设置 在properties 的参数里面  (超过Int范围,编译报错)
        //properties.setDelay(time);
        //Message message = new Message(msg.getBytes(),properties);
        //provide.send("delayExchange", "delay_key", message);

        // 方式2 直接设置header    其实设置properties 就是header,本质上是一样的(超过Int范围,编译,运行都不会有异常,,但是延时不生效,队列会立即收到消息)
        rabbitTemplate.convertAndSend("delayExchange", "delay_key", msg, (message) -> {
            message.getMessageProperties().setHeader("x-delay", time); //延迟9秒
            return message;
        });
    }

CustomExchange 自定义交换机,

在使用到延迟交换机插件的时候,,,我们使用插件新添加了一个x-delayed-message类型的交换机。 但是在Rabbitadmin中并没有这个类型的交换机。然后找到了这个CustomExchange 它和其它交换机相同,实现了AbstructExchange。 唯一的区别是没有指定type类型。type类型可以自定义,这样我们就可以通过构造方法自定义交换机的类型。

两种延时方式的对比。
  • 设置队列ttl,然后转发死信交换机。。处理灵活,对一类消息统一管理,适合队列里面同一类消息,ttl相同,只需要设置队列ttl就可以了。
  • 设置消息的ttl,单独的某一类,个别消息的单独设置的情况。 超出ttl任然给到死信交换机
  • 延迟交换机插件, 使用这个方法不需要死信交换机和死信队列,消息是在延迟时间到了之后,才会给到队列。
两种方法共同点。

 延时时间的数据类型是Int 单位ms。。在设置队列的ttl的时候,超过Int范围会编译报错。
设置消息的ttl大于Int。有两种情况,设置了对列的ttl。会按照队列的ttl。没有设置队列的ttl。。???? 延时会不会超过int范围。。

延迟交换机的方式,两种方式 设置消息的properties的Delay参数,延时超过int编译报错,,直接设置header,编译,运行均没有异常,只是超过int延时不生效。

几个坑

  • jiva这边修改了交换机的配置,然后需要在rabbitmq上先把exchange 解绑,然后再运行否则原先的配置不会删除,新的配置也会添加,导致都会生效。
  • 在调试延时队列的时候,springboot启动报错。有可能的原因是,修改了rabbitmq的Config, 修改了队列的ttl。 然后再rabbitmq的管理后台没有删除队列,导致运行时候会添加相同的队列,但是ttl不同,造成冲突。。。解决办法。。如果修改了队列配置,交换机配置。。。在启动sprigboot之前要先删除掉原来配置,,,因为你启动的时候会再去声明交换机和队列。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值