RabbitMQ

RabbitMQ是什么?

RabbitMQ 是一个消息中间件:它接受并转发消息。你可以把它当做一个快递站点,当你要发送一个包裹时,你把你的包裹放到快递站,快递员最终会把你的快递送到收件人那里,按照这种逻辑 RabbitMQ 是一个快递站,一个快递员帮你传递快件。RabbitMQ 与快递站的主要区别在于,它不处理快件而是接收,存储和转发消息数据。

四大核心概念

  • 生产者 :产生数据发送消息的程序是生产者
  • 交换机 :交换机是 RabbitMQ 非常重要的一个部件,一方面它接收来自生产者的消息,另一方面它将消息推送到队列中。交换机必须确切知道如何处理它接收到的消息,是将这些消息推送到特定队列还是推送到多个队列,亦或者是把消息丢弃,这个得有交换机类型决定。
  • 队列: RabbitMQ 内部使用的一种数据结构,尽管消息流经 RabbitMQ 和应用程序,但它们只能存 储在队列中。队列仅受主机的内存和磁盘限制的约束,本质上是一个大的消息缓冲区。许多生产者可以将消息发送到一个队列,许多消费者可以尝试从一个队列接收数据。这就是我们使用队列的方式。
  • 消费者 :消费与接收具有相似的含义。消费者大多时候是一个等待接收消息的程序。请注意生产者,消费者和消息中间件很多时候并不在同一机器上。同一个应用程序既可以是生产者又是可以是消费者。

工作原理

  • Broker:接收和分发消息的应用,RabbitMQ Server 就是 Message Broker。
  • Virtual host:出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多个 vhost,每个用户在自己的 vhost 创建 exchangequeue 等。
  • Connectionpublisherconsumer broker 之间的 TCP 连接。
  • Channel:如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCPConnection 的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个 thread 创建单独的 channel 进行通讯,AMQP method 包含了 channel id 帮助客户端和 message broker 识别 channel,所以 channel 之间是完全隔离的.Channel 作为轻量级的Connection 极大减少了操作系统建立 TCP connection 的开销。
  • Exchangemessage 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到 queue 中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast)。
  • Queue消息最终被送到这里等待 consumer 取走。
  • Bindingexchange queue 之间的虚拟连接,binding 中可以包含 routing keyBinding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据。

产者发送消息流程:

1、生产者和Broker建立TCP连接。

2、生产者和Broker建立通道。

3、生产者通过通道消息发送给Broker,由Exchange将消息进行转发。

4、Exchange将消息转发到指定的Queue(队列)

消费者接收消息流程:

1、消费者和Broker建立TCP连接

2、消费者和Broker建立通道

3、消费者监听指定的Queue(队列)

4、当有消息到达Queue时Broker默认将消息推送给消费者。

5、消费者接收到消息。

6、ack回复

消息模型

RabbitMq有3种常用的消息模型,其中订阅模式又分成3类

简单模型

 代码演示

        依赖

<!--指定 jdk 编译版本-->
<build>
 <plugins>
 <plugin>
 <groupId>org.apache.maven.plugins</groupId>
 <artifactId>maven-compiler-plugin</artifactId>
 <configuration>
 <source>8</source>
 <target>8</target>
 </configuration>
 </plugin>
 </plugins>
</build>
<dependencies>
 <!--rabbitmq 依赖客户端-->
 <dependency>
 <groupId>com.rabbitmq</groupId>
 <artifactId>amqp-client</artifactId>
 <version>5.8.0</version>
 </dependency>
 <!--操作文件流的一个依赖-->
 <dependency>
 <groupId>commons-io</groupId>
 <artifactId>commons-io</artifactId>
 <version>2.6</version>
 </dependency>
</dependencies>

生产者

public class Producer {
 private final static String QUEUE_NAME = "hello";
 public static void main(String[] args) throws Exception {
 //创建一个连接工厂
 ConnectionFactory factory = new ConnectionFactory();
 factory.setHost("192.168.1.92");
 factory.setUsername("admin");
 factory.setPassword("123");
 //channel 实现了自动 close 接口 自动关闭 不需要显示关闭
 try(
    Connection connection = factory.newConnection();Channel channel = 
    connection.createChannel()) {
     /**
     * 生成一个队列
     * 1.队列名称
     * 2.队列里面的消息是否持久化 默认消息存储在内存中
     * 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
     * 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
     * 5.其他参数
     */
     channel.queueDeclare(QUEUE_NAME,false,false,false,null);
     String message="hello world";
     /**
     * 发送一个消息
     * 1.发送到那个交换机
     * 2.路由的 key 是哪个
     * 3.其他的参数信息
     * 4.发送消息的消息体
     */
     channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
     System.out.println("消息发送完毕");
     }
  }
}

消费者

public class Consumer {
     private final static String QUEUE_NAME = "hello";
     public static void main(String[] args) throws Exception {
     ConnectionFactory factory = new ConnectionFactory();
     factory.setHost("182.92.234.71");
     factory.setUsername("admin");
     factory.setPassword("123");
     Connection connection = factory.newConnection();
     Channel channel = connection.createChannel();
     System.out.println("等待接收消息....");
     //推送的消息如何进行消费的接口回调
     DeliverCallback deliverCallback=(consumerTag,delivery)->{
     String message= new String(delivery.getBody());
     System.out.println(message);
     };
     //取消消费的一个回调接口 如在消费的时候队列被删除掉了
     CancelCallback cancelCallback=(consumerTag)->{
     System.out.println("消息消费被中断");
     };
     /**
     * 消费者消费消息
     * 1.消费哪个队列
     * 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
     * 3.消费者未成功消费的回调
     */
     channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
     }
}

工作队列

 简单模型中,一个队列上的消息只能有一个消费者,当消息比较多时,容易消息挤压。工作模式一个队列可以有多个消费者,一条消息只能被多个消费者中的其中一个消费者所消费。

代码演示

抽取工具类

public class RabbitMqUtils {
  //得到一个连接的 channel
  public static Channel getChannel() throws Exception{
  //创建一个连接工厂
  ConnectionFactory factory = new ConnectionFactory();
  factory.setHost("182.92.234.71");
  factory.setUsername("admin");
  factory.setPassword("123");
  Connection connection = factory.newConnection();
  Channel channel = connection.createChannel();
  return channel;
  }
}

启动两个消费者

public class Worker01 {
  private static final String QUEUE_NAME="hello";
  public static void main(String[] args) throws Exception {
      Channel channel = RabbitMqUtils.getChannel();
      DeliverCallback deliverCallback=(consumerTag,delivery)->{
      String receivedMessage = new String(delivery.getBody());
      System.out.println("接收到消息:"+receivedMessage);
     };

 CancelCallback cancelCallback=(consumerTag)->{
     System.out.println(consumerTag+"消费者取消消费接口回调逻辑");
  };
 System.out.println("C1 消费者启动等待消费......");
 channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
 }
}
public class Worker02 {
  private static final String QUEUE_NAME="hello";
  public static void main(String[] args) throws Exception {
      Channel channel = RabbitMqUtils.getChannel();
      DeliverCallback deliverCallback=(consumerTag,delivery)->{
      String receivedMessage = new String(delivery.getBody());
      System.out.println("接收到消息:"+receivedMessage);
     };

 CancelCallback cancelCallback=(consumerTag)->{
     System.out.println(consumerTag+"消费者取消消费接口回调逻辑");
  };
 System.out.println("C2 消费者启动等待消费......");
 channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
 }
}

生产者

public class Task01 {
 private static final String QUEUE_NAME="hello";
 public static void main(String[] args) throws Exception {
     try(Channel channel=RabbitMqUtils.getChannel();) {
     channel.queueDeclare(QUEUE_NAME,false,false,false,null);
     //从控制台当中接受信息
     Scanner scanner = new Scanner(System.in);
     while (scanner.hasNext()){
     String message = scanner.next();
     channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
     System.out.println("发送消息完成:"+message);
     }
   }
 }
}

订阅模式之fanout

 发布订阅模式中,所有绑定的队列都会接收到一份消息,消息可以被多个队列重复消费。在声明交换机要指定交换机的类型为fanout。

代码演示

生产者

public class EmitLog {
 private static final String EXCHANGE_NAME = "logs";
 public static void main(String[] argv) throws Exception {
     try (Channel channel = RabbitUtils.getChannel()) {
     /**
     * 声明一个 exchange
     * 1.exchange 的名称
     * 2.exchange 的类型
     */
     channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
     Scanner sc = new Scanner(System.in);
     System.out.println("请输入信息");
     while (sc.hasNext()) {
     String message = sc.nextLine();
     channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
     System.out.println("生产者发出消息" + message);
   }
  }
 }
}

消费者

public class ReceiveLogs01 {
 private static final String EXCHANGE_NAME = "logs";
 public static void main(String[] argv) throws Exception {
     Channel channel = RabbitUtils.getChannel();
     channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
     /**
     * 生成一个临时的队列 队列的名称是随机的
     * 当消费者断开和该队列的连接时 队列自动删除
     */
     String queueName = channel.queueDeclare().getQueue();
     //把该临时队列绑定我们的 exchange 其中 routingkey(也称之为 binding key)为空字符串
     channel.queueBind(queueName, EXCHANGE_NAME, "");
     System.out.println("等待接收消息,把接收到的消息打印在屏幕.....");
     DeliverCallback deliverCallback = (consumerTag, delivery) -> {
     String message = new String(delivery.getBody(), "UTF-8");
     System.out.println("控制台打印接收到的消息"+message);
    };
     channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
  }
}

思考:
1、publish/subscribe与work queues有什么区别。

区别:

1)work queues不用定义交换机,而publish/subscribe需要定义交换机。

2)publish/subscribe的生产方是面向交换机发送消息,work queues的生产方是面向队列发送消息(底层使用默认交换机)。

3)publish/subscribe需要设置队列和交换机的绑定,work queues不需要设置,实际上work queues会将队列绑定到默认的交换机 。

相同点:

所以两者实现的发布/订阅的效果是一样的,多个消费端监听同一个队列不会重复消费消息。

2、实际工作用 publish/subscribe还是work queues。

建议使用 publish/subscribe,发布订阅模式比工作队列模式更强大(也可以做到同一队列竞争),并且发布订阅模式可以指定自己专用的交换机。

订阅模式之Direct

Fanout 这种交换类型并不能给我们带来很大的灵活性 - 它只能进行无意识的广播,在这里我们将使用 direct 这种类型来进行替换,这种类型的工作方式是,消息只去到它绑定的routingKey队列中去。

在这种绑定情况下,生产者发布消息到 exchange 上,绑定键为 orange 的消息会被发布到队列Q1。绑定键为 blackgreen 和的消息会被发布到队列 Q2 ,其他消息类型的消息将被丢弃。

代码演示

生产者

public class EmitLogDirect {
 private static final String EXCHANGE_NAME = "direct_logs";
 public static void main(String[] argv) throws Exception {
     try (Channel channel = RabbitUtils.getChannel()) {
     channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
     //创建多个 bindingKey
     Map<String, String> bindingKeyMap = new HashMap<>();
     bindingKeyMap.put("info","普通 info 信息");
     bindingKeyMap.put("warning","警告 warning 信息");
     bindingKeyMap.put("error","错误 error 信息");
     //debug 没有消费这接收这个消息 所有就丢失了
     bindingKeyMap.put("debug","调试 debug 信息");
     for (Map.Entry<String, String> bindingKeyEntry: bindingKeyMap.entrySet()){
     String bindingKey = bindingKeyEntry.getKey();
     String message = bindingKeyEntry.getValue();
     channel.basicPublish(EXCHANGE_NAME,bindingKey, null, 
     message.getBytes("UTF-8"));
     System.out.println("生产者发出消息:" + message);
    }
  }
 }
}

消费者

public class ReceiveLogsDirect01 {
 private static final String EXCHANGE_NAME = "direct_logs";
 public static void main(String[] argv) throws Exception {
     Channel channel = RabbitUtils.getChannel();
     channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
     String queueName = "disk";
     channel.queueDeclare(queueName, false, false, false, null);
     channel.queueBind(queueName, EXCHANGE_NAME, "error");
     System.out.println("等待接收消息.....");
     DeliverCallback deliverCallback = (consumerTag, delivery) -> {
     String message = new String(delivery.getBody(), "UTF-8");
     message="接收绑定键:"+delivery.getEnvelope().getRoutingKey()+",消息:"+message;
     System.out.println("接收到的消息为"+ message);
     };
     channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
   });
  }
}

订阅模式之Topics

        direct 交换机存在局限性 - 比方说,我们想接收的日志类型有info.base 和 info.advantage ,某个队列只想 info.base 的消息,那这个时候 direct 就办不到了。这个时候就只能使用 topic 类型。
        发送到类型是 topic 交换机的消息的 routing_key 不能随意写,必须满足一定的要求,它 必须是一个单 词列表,以点号分隔开 。这些单词可以是任意单词,比如说: "stock.usd.nyse", "nyse.vmw", "quick.orange.rabbit".这种类型的。当然这个单词列表最多不能超过 255 个字节。 在这个规则列表中: *(星号 ) 可以代替一个单词 , #(井号 ) 可以替代零个或多个单词
代码演示
生产者
public class EmitLogTopic {
 private static final String EXCHANGE_NAME = "topic_logs";
 public static void main(String[] argv) throws Exception {
     try (Channel channel = RabbitUtils.getChannel()) {
     channel.exchangeDeclare(EXCHANGE_NAME, "topic");
     /**
     * Q1-->绑定的是
     * 中间带 orange 带 3 个单词的字符串(*.orange.*)
     * Q2-->绑定的是
     * 最后一个单词是 rabbit 的 3 个单词(*.*.rabbit)
     * 第一个单词是 lazy 的多个单词(lazy.#)
     *
     */
     Map<String, String> bindingKeyMap = new HashMap<>();
     bindingKeyMap.put("quick.orange.rabbit","被队列 Q1Q2 接收到");
     bindingKeyMap.put("lazy.orange.elephant","被队列 Q1Q2 接收到");
     bindingKeyMap.put("quick.orange.fox","被队列 Q1 接收到");
     bindingKeyMap.put("lazy.brown.fox","被队列 Q2 接收到");
     bindingKeyMap.put("lazy.pink.rabbit","虽然满足两个绑定但只被队列 Q2 接收一次");
     bindingKeyMap.put("quick.brown.fox","不匹配任何绑定不会被任何队列接收到会被丢弃");
     bindingKeyMap.put("quick.orange.male.rabbit","是四个单词不匹配任何绑定会被丢弃");
     bindingKeyMap.put("lazy.orange.male.rabbit","是四个单词但匹配 Q2");
     for (Map.Entry<String, String> bindingKeyEntry: bindingKeyMap.entrySet()){
     String bindingKey = bindingKeyEntry.getKey();
     String message = bindingKeyEntry.getValue();
     channel.basicPublish(EXCHANGE_NAME,bindingKey, null, 
     message.getBytes("UTF-8"));
     System.out.println("生产者发出消息" + message);
   }
  }
 }
}

消费者

public class ReceiveLogsTopic01 {
 private static final String EXCHANGE_NAME = "topic_logs";
 public static void main(String[] argv) throws Exception {
     Channel channel = RabbitUtils.getChannel();
     channel.exchangeDeclare(EXCHANGE_NAME, "topic");
     //声明 Q1 队列与绑定关系
     String queueName="Q1";
     channel.queueDeclare(queueName, false, false, false, null);
     channel.queueBind(queueName, EXCHANGE_NAME, "*.orange.*");
     System.out.println("等待接收消息.....");
     DeliverCallback deliverCallback = (consumerTag, delivery) -> {
     String message = new String(delivery.getBody(), "UTF-8");
     System.out.println(" 接收队列 :"+queueName+" 绑 定
    键:"+delivery.getEnvelope().getRoutingKey()+",消息:"+message);
   };
     channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
  });
 }
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值