每日一学:Publish/Subscribe发布与订阅模式

521 阅读4分钟

一、模式说明

image.png

发布订阅模式:

  1. 每个消费者监听自己的队列。
  2. 生产者将消息发给broker,由交换机将消息转发到绑定此交换机的每个队列,每个绑定交换机的队列都将接收到消息

二、代码

①生产者

package com.itheima.rabbitmq.ps; 

import com.itheima.rabbitmq.util.ConnectionUtil; 
import com.rabbitmq.client.BuiltinExchangeType; 
import com.rabbitmq.client.Channel; 
import com.rabbitmq.client.Connection; 

/**
 * 发布与订阅使用的交换机类型为:fanout 
 */ 
public class Producer { 
    //交换机名称 
    static final String FANOUT_EXCHAGE = "fanout_exchange"; 
    
    //队列名称 
    static final String FANOUT_QUEUE_1 = "fanout_queue_1"; 
    
    //队列名称 
    static final String FANOUT_QUEUE_2 = "fanout_queue_2";
    
    public static void main(String[] args) throws Exception { 
        //创建连接 
        Connection connection = ConnectionUtil.getConnection(); 
        
        // 创建频道 
        Channel channel = connection.createChannel(); 
        
        /**
         * 声明交换机 
         * 参数1:交换机名称 
         * 参数2:交换机类型,fanout、topic、direct、headers 
        */ 
        channel.exchangeDeclare(FANOUT_EXCHAGE, BuiltinExchangeType.FANOUT); 
        
        // 声明(创建)队列 
        /**
         * 参数1:队列名称 
         * 参数2:是否定义持久化队列 
         * 参数3:是否独占本次连接 
         * 参数4:是否在不使用的时候自动删除队列 
         * 参数5:队列其它参数 
        */ 
        channel.queueDeclare(FANOUT_QUEUE_1, true, false, false, null); 
        channel.queueDeclare(FANOUT_QUEUE_2, true, false, false, null); 
        
        //队列绑定交换机 
        channel.queueBind(FANOUT_QUEUE_1, FANOUT_EXCHAGE, ""); 
        channel.queueBind(FANOUT_QUEUE_2, FANOUT_EXCHAGE, ""); 
        for (int i = 1; i <= 10; i++) { 
            // 发送信息 
            String message = "你好;小兔子!发布订阅模式--" + i; 
            
            /**
             * 参数1:交换机名称,如果没有指定则使用默认Default Exchage 
             * 参数2:路由key,简单模式可以传递队列名称 
             * 参数3:消息其它属性 
             * 参数4:消息内容 
            */ 
            channel.basicPublish(FANOUT_EXCHAGE, "", null, message.getBytes()); 
            System.out.println("已发送消息:" + message); 
        }
        
        // 关闭资源 
        channel.close(); connection.close(); 
    } 
}

②消费者1

package com.itheima.rabbitmq.ps; 

import com.itheima.rabbitmq.util.ConnectionUtil;
import com.rabbitmq.client.*; 
import java.io.IOException;

public class Consumer1 { 
    public static void main(String[] args) throws Exception { 
        Connection connection = ConnectionUtil.getConnection(); 
        
        // 创建频道 
        Channel channel = connection.createChannel(); 
        
        //声明交换机 
        channel.exchangeDeclare(Producer.FANOUT_EXCHAGE, BuiltinExchangeType.FANOUT); 
        
        // 声明(创建)队列 
        /**
         * 参数1:队列名称 
         * 参数2:是否定义持久化队列 
         * 参数3:是否独占本次连接 
         * 参数4:是否在不使用的时候自动删除队列 
         * 参数5:队列其它参数 
        */ 
        channel.queueDeclare(Producer.FANOUT_QUEUE_1, true, false, false, null); 
        
        //队列绑定交换机 
        channel.queueBind(Producer.FANOUT_QUEUE_1, Producer.FANOUT_EXCHAGE, ""); 
        
        //创建消费者;并设置消息处理 
        DefaultConsumer consumer = new DefaultConsumer(channel){ 
            @Override 
            /**
             * consumerTag 消息者标签,在channel.basicConsume时候可以指定 
             * envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重传标志(收到消息失败后是否需要重新发送) 
             * properties 属性信息 
             * body 消息 
            */ 
            public void handleDelivery(String consumerTag, Envelope envelope, 
                    AMQP.BasicProperties properties, byte[] body) throws IOException { 
                    
                //路由key 
                System.out.println("路由key为:" + envelope.getRoutingKey()); 
                
                //交换机 
                System.out.println("交换机为:" + envelope.getExchange()); 
                
                //消息id 
                System.out.println("消息id为:" + envelope.getDeliveryTag()); 
                
                //收到的消息 
                System.out.println("消费者1-接收到的消息为:" + new String(body, "utf-8")); 
            } 
        };
        
        //监听消息
        /**
         * 参数1:队列名称 
         * 参数2:是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置为false则需要手动确认 
         * 参数3:消息接收到后回调
        */ 
        channel.basicConsume(Producer.FANOUT_QUEUE_1, true, consumer);
    } 
}

③消费者2

package com.itheima.rabbitmq.ps; 

import com.itheima.rabbitmq.util.ConnectionUtil; 
import com.rabbitmq.client.*; 
import java.io.IOException; 

public class Consumer2 { 
    public static void main(String[] args) throws Exception { 
        Connection connection = ConnectionUtil.getConnection(); 
        
        // 创建频道 
        Channel channel = connection.createChannel(); 
        
        //声明交换机 
        channel.exchangeDeclare(Producer.FANOUT_EXCHAGE, BuiltinExchangeType.FANOUT); 
        
        // 声明(创建)队列 
        /**
         * 参数1:队列名称 
         * 参数2:是否定义持久化队列 
         * 参数3:是否独占本次连接 
         * 参数4:是否在不使用的时候自动删除队列 
         * 参数5:队列其它参数 
        */ 
        channel.queueDeclare(Producer.FANOUT_QUEUE_2, true, false, false, null); 
        
        //队列绑定交换机 
        channel.queueBind(Producer.FANOUT_QUEUE_2, Producer.FANOUT_EXCHAGE, "");
         
        //创建消费者;并设置消息处理 
        DefaultConsumer consumer = new DefaultConsumer(channel){ 
        
            @Override 
            /**
             * consumerTag 消息者标签,在channel.basicConsume时候可以指定 
             * envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重传标志(收到消息失败后是否需要重新发送) 
             * properties 属性信息 
             * body 消息 
            */ 
            public void handleDelivery(String consumerTag, Envelope envelope, 
                    AMQP.BasicProperties properties, byte[] body) throws IOException { 
                
                //路由key 
                System.out.println("路由key为:" + envelope.getRoutingKey()); 
                
                //交换机 
                System.out.println("交换机为:" + envelope.getExchange());
                
                //消息id 
                System.out.println("消息id为:" + envelope.getDeliveryTag()); 
                
                //收到的消息 
                System.out.println("消费者2-接收到的消息为:" + new String(body, "utf-8")); 
            } 
        };
        //监听消息 
        /**
         * 参数1:队列名称 
         * 参数2:是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置为false则需要手动确认 
         * 参数3:消息接收到后回调 
        */ 
        channel.basicConsume(Producer.FANOUT_QUEUE_2, true, consumer); 
    } 
}

三、测试

启动所有消费者,然后使用生产者发送消息。在每个消费者对应的控制台可以查看到生产者发送的所有消息,到达广播的效果。

在执行完测试代码后,其实到RabbitMQ的管理后台找到 Exchanges 选项卡,点击 fanout_exchange 的交换机,可以查看到如下的绑定:

image.png

总结

交换机需要与队列进行绑定,绑定之后,一个消息可以被多个消费者都收到。

发布订阅模式与工作队列模式的区别

  1. 工作队列模式不用定义交换机,而发布/订阅模式需要定义交换机。
  2. 发布/订阅模式的生产方是面向交换机发送消息,工作队列模式的生产方是面向队列发送消息(底层使用默认交换机)。
  3. 发布/订阅模式需要设置队列和交换机的绑定,工作队列模式不需要设置,实际上工作队列模式会将队列绑定到默认的交换机 。