消息的交换

目录

RabbitMQ-从基础到实战(1)— Hello RabbitMQ

RabbitMQ-从基础到实战(2)— 防止消息丢失

1.简介

在前面的例子中,每个消息都只对应一个消费者,即使有多个消费者在线,也只会有一个消费者接收并处理一条消息,这是消息中间件的一种常用方式。

另外一种方式,生产者生产一条消息,广播给一个或多个队列,所有订阅了这个队列的消费者,都可以消费这条消息,这就是消息订阅。

官方教程列举了这样一个场景,生产者发出一条记录日志的消息,消费者1接收到后写日志到硬盘,消费者2接收到后打印日志到屏幕。工作中还有很多这样的场景有待发掘,适当的使用消息订阅后可以成倍的增加效率。

2.RabbitMQ的交换中心(Exchange)

在前两章的例子中,我们涉及到了三个概念

  1. 生产者
  2. 队列
  3. 消费者

这不禁让我们以为,生产者生产消息后直接到发送到队列,消费者从队列中获取消息,再消费掉。

其实这是错误的,在RabbitMQ中,生产者不会直接把消息发送给队列,实际上,生产者甚至不知道一条消息会不会被发送到队列上。

正确的概念是,生产者会把消息发送给RabbitMQ的交换中心(Exchange),Exchange的一侧是生产者,另一侧则是一个或多个队列,由Exchange决定一条消息的生命周期--发送给某些队列,或者直接丢弃掉。

这个概念在官方文档中被称作RabbitMQ消息模型的核心思想(core idea)

如下图,其中X代表的是Exchange。

RabbitMQ中,有4种类型的Exchange

  • direct    通过消息的routing key比较queue的key,相等则发给该queue,常用于相同应用多实例之间的任务分发

    • 默认类型   本身是一个direct类型的exchange,routing key自动设置为queue name。注意,direct不等于默认类型,默认类型是在queue没有指定exchange时的默认处理方式,发消息时,exchange字段也要相应的填成空字符串“”
  • topic    话题,通过可配置的规则分发给绑定在该exchange上的队列,通过地理位置推送等场景适用
  • headers    当分发规则很复杂,用routing key不好表达时适用,忽略routing key,用header取代之,header可以为非字符串,例如Integer或者String
  • fanout    分发给所有绑定到该exchange上的队列,忽略routing key,适用于MMO游戏、广播、群聊等场景

更详细的介绍,请看官方文档

3.临时队列

可以对一个队列命名是十分重要的,在消费者消费消息时,要指明消费哪个队列的消息(下面的queue),这样就可以让多个消费者同时分享一个队列

String basicConsume(String queue, boolean autoAck, Consumer callback) throws IOException;

上述记录日志的场景中,有以下几个特点

  • 所有消费者都需要监听所有的日志消息,因此每个消费者都需要一个单独的队列,不需要和别人分享
  • 消费者只关心最新的消息,连接到RabbitMQ之前的消息不需要关心,因此,每次连接时需要创建一个队列,绑定到相应的exchange上,连接断开后,删除该队列

自己声明队列是比较麻烦的,因此,RabbitMQ提供了简便的获取临时队列的方法,该队列会在连接断开后销毁

  1. String queueName = channel.queueDeclare().getQueue();

这行代码会获取一个名字类似于“amq.gen-JzTY20BRgKO-HjmUJj0wLg”的临时队列

4.绑定

再次注意,在RabbitMQ中,消息是发送到Exchange的,不是直接发送的Queue。因此,需要把Queue和Exchange进行绑定,告诉RabbitMQ把指定的Exchange上的消息发送的这个队列上来

绑定队列使用此方法

Queue.BindOk queueBind(String queue, String exchange, String routingKey) throws IOException;

其中,queue是队列名,exchange是要绑定的交换中心,routingKey就是这个queue的routingKey

5.实践

下面来实现上述场景,生产者发送日志消息,消费者1记录日志,消费者2打印日志

下面的代码中,把连接工厂等方法放到了构造函数中,也就是说,每new一个对象,都会创建一个连接,在生产环境这样做是很浪费性能的,每次创建一个connection都会建立一次TCP连接,生产环境应使用连接池。而Channel又不一样,多个Channel是共用一个TCP连接的,因此可以放心的获取Channel(本结论出自官方文档对Channel的定义)

AMQP 0-9-1 connections are multiplexed with channels that can be thought of as "lightweight connections that share a single TCP connection".

For applications that use multiple threads/processes for processing, it is very common to open a new channel per thread/process and not share channels between them.

日志消息发送类 LogSender

  1. 1 import java.io.IOException;
  2. 2 import java.util.concurrent.TimeoutException;
  3. 3
  4. 4 import org.slf4j.Logger;
  5. 5 import org.slf4j.LoggerFactory;
  6. 6
  7. 7 import com.rabbitmq.client.Channel;
  8. 8 import com.rabbitmq.client.Connection;
  9. 9 import com.rabbitmq.client.ConnectionFactory;
  10. 10
  11. 11 public class LogSender {
  12. 12
  13. 13 private Logger logger = LoggerFactory.getLogger(LogSender.class);
  14. 14 private ConnectionFactory factory;
  15. 15 private Connection connection;
  16. 16 private Channel channel;
  17. 17
  18. 18 /**
  19. 19 * 在构造函数中获取连接
  20. 20 */
  21. 21 public LogSender(){
  22. 22 super();
  23. 23 try {
  24. 24 factory = new ConnectionFactory();
  25. 25 factory.setHost("127.0.0.1");
  26. 26 connection = factory.newConnection();
  27. 27 channel = connection.createChannel();
  28. 28 } catch (Exception e) {
  29. 29 logger.error(" [X] INIT ERROR!",e);
  30. 30 }
  31. 31 }
  32. 32 /**
  33. 33 * 提供个关闭方法,现在并没有什么卵用
  34. 34 * @return
  35. 35 */
  36. 36 public boolean closeAll(){
  37. 37 try {
  38. 38 this.channel.close();
  39. 39 this.connection.close();
  40. 40 } catch (IOException | TimeoutException e) {
  41. 41 logger.error(" [X] CLOSE ERROR!",e);
  42. 42 return false;
  43. 43 }
  44. 44 return true;
  45. 45 }
  46. 46
  47. 47 /**
  48. 48 * 我们更加关心的业务方法
  49. 49 * @param message
  50. 50 */
  51. 51 public void sendMessage(String message) {
  52. 52 try {
  53. 53 //声明一个exchange,命名为logs,类型为fanout
  54. 54 channel.exchangeDeclare("logs", "fanout");
  55. 55 //exchange是logs,表示发送到此Exchange上
  56. 56 //fanout类型的exchange,忽略routingKey,所以第二个参数为空
  57. 57 channel.basicPublish("logs", "", null, message.getBytes());
  58. 58 logger.debug(" [D] message sent:"+message);
  59. 59 } catch (IOException e) {
  60. 60 e.printStackTrace();
  61. 61 }
  62. 62 }
  63. 63 }

在LogSender中,和之前的例子不一样的地方是,我们没有直接声明一个Queue,取而代之的是声明了一个exchange

发布消息时,第一个参数填了我们声明的exchange名字,routingKey留空,因为fanout类型忽略它。

在前面的例子中,我们routingKey填的是队列名,因为默认的exchange(exchange位空字符串时使用默认交换中心)会把队列的routingKey设置为queueName(声明队列的时候设置的,不是发送消息的时候),又是direct类型,所以可以通过queueName当做routingKey找到队列。

消费类 LogConsumer

  1. 1 package com.liyang.ticktock.rabbitmq;
  2. 2
  3. 3 import java.io.IOException;
  4. 4 import java.util.concurrent.TimeoutException;
  5. 5
  6. 6 import org.slf4j.Logger;
  7. 7 import org.slf4j.LoggerFactory;
  8. 8
  9. 9 import com.rabbitmq.client.AMQP;
  10. 10 import com.rabbitmq.client.Channel;
  11. 11 import com.rabbitmq.client.Connection;
  12. 12 import com.rabbitmq.client.ConnectionFactory;
  13. 13 import com.rabbitmq.client.Consumer;
  14. 14 import com.rabbitmq.client.DefaultConsumer;
  15. 15 import com.rabbitmq.client.Envelope;
  16. 16
  17. 17 public class LogConsumer {
  18. 18
  19. 19 private Logger logger = LoggerFactory.getLogger(LogConsumer.class);
  20. 20 private ConnectionFactory factory;
  21. 21 private Connection connection;
  22. 22 private Channel channel;
  23. 23
  24. 24 /**
  25. 25 * 在构造函数中获取连接
  26. 26 */
  27. 27 public LogConsumer() {
  28. 28 super();
  29. 29 try {
  30. 30 factory = new ConnectionFactory();
  31. 31 factory.setHost("127.0.0.1");
  32. 32 connection = factory.newConnection();
  33. 33 channel = connection.createChannel();
  34. 34 // 声明exchange,防止生产者没启动,exchange不存在
  35. 35 channel.exchangeDeclare("logs","fanout");
  36. 36 } catch (Exception e) {
  37. 37 logger.error(" [X] INIT ERROR!", e);
  38. 38 }
  39. 39 }
  40. 40
  41. 41 /**
  42. 42 * 提供个关闭方法,现在并没有什么卵用
  43. 43 *
  44. 44 * @return
  45. 45 */
  46. 46 public boolean closeAll() {
  47. 47 try {
  48. 48 this.channel.close();
  49. 49 this.connection.close();
  50. 50 } catch (IOException | TimeoutException e) {
  51. 51 logger.error(" [X] CLOSE ERROR!", e);
  52. 52 return false;
  53. 53 }
  54. 54 return true;
  55. 55 }
  56. 56
  57. 57 /**
  58. 58 * 我们更加关心的业务方法
  59. 59 */
  60. 60 public void consume() {
  61. 61 try {
  62. 62 // 获取一个临时队列
  63. 63 String queueName = channel.queueDeclare().getQueue();
  64. 64 // 把刚刚获取的队列绑定到logs这个交换中心上,fanout类型忽略routingKey,所以第三个参数为空
  65. 65 channel.queueBind(queueName, "logs", "");
  66. 66 //定义一个Consumer,消费Log消息
  67. 67 Consumer consumer = new DefaultConsumer(channel) {
  68. 68 @Override
  69. 69 public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
  70. 70 byte[] body) throws IOException {
  71. 71 String message = new String(body, "UTF-8");
  72. 72 logger.debug(" [D] 我是来打印日志的:"+message);
  73. 73 }
  74. 74 };
  75. 75 //这里自动确认为true,接收到消息后该消息就销毁了
  76. 76 channel.basicConsume(queueName, true, consumer);
  77. 77 } catch (IOException e) {
  78. 78 e.printStackTrace();
  79. 79 }
  80. 80 }
  81. 81 }

复制一个项目,把72行改为如下代码,代表两个做不同工作的消费者

  1. 1 logger.debug(" [D] 我已经把消息写到硬盘了:"+message);

消费者App

  1. 1 public class App
  2. 2 {
  3. 3 public static void main( String[] args )
  4. 4 {
  5. 5 LogConsumer consumer = new LogConsumer();
  6. 6 consumer.consume();
  7. 7 }
  8. 8 }

生产者App

  1. 1 public class App {
  2. 2 public static void main( String[] args ) throws InterruptedException{
  3. 3 LogSender sender = new LogSender();
  4. 4 while(true){
  5. 5 sender.sendMessage(System.nanoTime()+"");
  6. 6 Thread.sleep(1000);
  7. 7 }
  8. 8 }
  9. 9 }

把消费者打包成两个可执行的jar包,方便观察控制台

用java -jar 命令执行,结果如下

6.结束语

本章介绍了RabbitMQ中消息的交换,再次强调,RabbitMQ中,消息是通过交换中心转发到队列的,不要被默认的exchange混淆,默认的exchange会自动把queue的名字设置为它的routingKey,所以消息发布时,才能通过queueName找到该队列,其实此时queueName扮演的角色就是routingKey。

RabbitMQ消息的交换的更多相关文章

  1. RabbitMQ-从基础到实战(3)— 消息的交换

    1.简介 在前面的例子中,每个消息都只对应一个消费者,即使有多个消费者在线,也只会有一个消费者接收并处理一条消息,这是消息中间件的一种常用方式.还有另外一种方式,生产者生产一条消息,广播给所有的消费者 ...

  2. RabbitMQ-从基础到实战(4)— 消息的交换(下)

    0.目录 RabbitMQ-从基础到实战(1)- Hello RabbitMQ RabbitMQ-从基础到实战(2)- 防止消息丢失 RabbitMQ-从基础到实战(3)- 消息的交换(上) 1.简介 ...

  3. RabbitMQ-从基础到实战(5)— 消息的交换(下)

    转载请注明出处 0.目录 RabbitMQ-从基础到实战(1)- Hello RabbitMQ RabbitMQ-从基础到实战(2)- 防止消息丢失 RabbitMQ-从基础到实战(3)- 消息的交换 ...

  4. RabbitMQ消息队列

    RabbitMQ消息队列 !!!  注意,保证服务器的内存足够,磁盘足够,以及删除/etc/hosts中没有用的dns解析 # 优点,能够保证消息数据持久化,不丢失,支持高并发 安装学习rabbitm ...

  5. RabbitMQ-从基础到实战(3)— 消息的交换(上)

    转载请注明出处 0.目录 RabbitMQ-从基础到实战(1)— Hello RabbitMQ RabbitMQ-从基础到实战(2)— 防止消息丢失 RabbitMQ-从基础到实战(4)— 消息的交换 ...

  6. C# .net 环境下使用rabbitmq消息队列

    消息队列的地位越来越重要,几乎是面试的必问问题了,不会使用几种消息队列都显得尴尬,正好本文使用C#来带你认识rabbitmq消息队列 首先,我们要安装rabbitmq,当然,如果有现成的,也可以使用, ...

  7. 基于ASP.NET Core 5.0使用RabbitMQ消息队列实现事件总线(EventBus)

    文章阅读请前先参考看一下 https://www.cnblogs.com/hudean/p/13858285.html 安装RabbitMQ消息队列软件与了解C#中如何使用RabbitMQ 和 htt ...

  8. RabbitMQ消息队列(一): Detailed Introduction 详细介绍

     http://blog.csdn.net/anzhsoft/article/details/19563091 RabbitMQ消息队列(一): Detailed Introduction 详细介绍 ...

  9. RabbitMQ消息队列1: Detailed Introduction 详细介绍

    1. 历史 RabbitMQ是一个由erlang开发的AMQP(Advanced Message Queue )的开源实现.AMQP 的出现其实也是应了广大人民群众的需求,虽然在同步消息通讯的世界里有 ...

随机推荐

  1. jQuery -- 光阴似箭(二):jQuery效果的使用

    jQuery -- 知识点回顾篇(二):jQuery效果的使用 1. jQuery 效果 -- 隐藏和显示:  使用 hide() 和 show() 方法来隐藏和显示 HTML 元素. <!DO ...

  2. 解决新版chrome无法手动拖动安装插件 提示“无法从该网站添加应用,扩展程序和用户脚本”

    开发模式安装 把下载后的.crx扩展名的离线Chrome插件的文件扩展名改成.zip或者.rar 解压压缩文件 在Chrome的地址栏中输入:chrome://extensions/ 打开Chrome ...

  3. Unix/Linux环境C编程新手教程(21) 各个系统HelloWorld跑起来效果怎样?

    版权声明:本文为博主尹成联系QQ77025077,微信18510341407原创文章,欢迎转载侵权不究. https://blog.csdn.net/yincheng01/article/detail ...

  4. centos7下安装docker(17.5docker监控的总结对比)

    到现在为止,我已经学习了docker自带的监控方案:ps/ls/top/stats,以及sysdig,weave scope,cadvisor,prometheus多种监控工具,现在做个总结和比较 部 ...

  5. 机器学习算法总结(四)——GBDT与XGBOOST

    Boosting方法实际上是采用加法模型与前向分布算法.在上一篇提到的Adaboost算法也可以用加法模型和前向分布算法来表示.以决策树为基学习器的提升方法称为提升树(Boosting Tree).对 ...

  6. WiFi-ESP8266入门http(3-4)网页一键配网(1若为普通wifi直连 2若为西电网页认证自动网页post请求连接)+网页按钮灯控+MQTT通信

    网页一键配网(1若为普通wifi直连  2若为西电网页认证自动网页post请求连接)+网页按钮灯控+MQTT通信 工程连接:https://github.com/Dongvdong/ESP8266_H ...

  7. mybatis 参数为list时,校验list是否为空, mybatis ${}与#{}的区别

    一.参数list时,先判断是否为空,否则会报错. 二.mybatis ${}与#{}的区别 简单来说#{} 解析的是占位符?可以防止SQL注入, 比如打印出来的语句 select * from tab ...

  8. pyspider环境部署2--pyspider安装

    接上篇文章,在python3.6.4安装完成的基础上,安装相关依赖模块及pyspider. 依赖安装 1.setuptools和pip setuptools和pip是python的包管理工具,pyth ...

  9. 初学Python——文件操作

    一.文件的打开和关闭 1.常用的打开关闭语句 f=open("yesterday","r",encoding="utf-8") #打开文件 ...

  10. 输出 1-100 内的奇数和偶数,并对其分别求和(while嵌套if-else循环)

    package com.summer.cn; /** * @author Summer * 输出 1-100 内的奇数和偶数,并对其分别求和 * while嵌套if-else循环 */ public ...