RabbitMQ——基本组件、概率、简单实现

865 阅读11分钟

基本概念

发布者(publisher) :发送消息
消费者(consumer):消费消息
交互机 (exchange) :它指定消息用上面规则( Binding),路由到哪一个队列
队列 (Queue) : 存储着即将被应用消费掉的消息

交互机类型


1.默认交互机


解释
默认交换机(default exchange)实际上是一个由消息代理预先声明好的没有名字(名字为空字符串)的直连交换机(direct exchange)。它有一个特殊的属性使得它对于简单应用特别有用处:那就是每个新建队列(queue)都会自动绑定到默认交换机上,绑定的路由键(routing key)名称与队列名称相同。
举个栗子:当你声明了一个名为 "search-indexing-online" 的队列,AMQP 代理会自动将其绑定到默认交换机上,绑定(binding)的路由键名称也是为 "search-indexing-online"。因此,当携带着名为 "search-indexing-online" 的路由键的消息被发送到默认交换机的时候,此消息会被默认交换机路由至名为 "search-indexing-online" 的队列中。换句话说,默认交换机看起来貌似能够直接将消息投递给队列,尽管技术上并没有做相关的操作。
ps:The default exchange is implicitly bound to every queue, with a routing key equal to the queue name. It is not possible to explicitly bind to, or unbind from the default exchange. It also cannot be deleted.( 默认交换器隐式地绑定到每个队列,其路由键等于队列名称。无法显式绑定到默认交换器或从默认交换器解绑定。它也不能被删除。)


图解


实现


发布者(publisher)

package com.test.默认交互机;import com.rabbitmq.client.Channel;import com.rabbitmq.client.Connection;import com.test.utils.ConnectionUtil;/*** @作者: tjx* @描述:* @创建时间: 创建于17:37 2019/8/26**/public class Send {    private final static String QUEUE_NAME = "search-indexing-online";    public static void main(String[] argv) throws Exception {        // 获取到连接以及mq通道        Connection connection = ConnectionUtil.getConnection();        // 从连接中创建通道        Channel channel = connection.createChannel();        // 声明(创建)队列        channel.queueDeclare(QUEUE_NAME, false, false, false, null);        // 消息内容        String message = "Hello World!";        channel.basicPublish("", QUEUE_NAME, null, message.getBytes());        System.out.println(" [x] Sent '" + message + "'");        //关闭通道和连接        channel.close();        connection.close();    }}

消费者(consumer)

package com.test.默认交互机;import com.rabbitmq.client.Channel;import com.rabbitmq.client.Connection;import com.rabbitmq.client.QueueingConsumer;import com.test.utils.ConnectionUtil;/*** @作者: tjx* @描述:* @创建时间: 创建于17:42 2019/8/26**/public class Recv {    private final static String QUEUE_NAME = "search-indexing-online";    public static void main(String[] argv) throws Exception {        // 获取到连接以及mq通道        Connection connection = ConnectionUtil.getConnection();        // 从连接中创建通道        Channel channel = connection.createChannel();        // 声明队列        channel.queueDeclare(QUEUE_NAME, false, false, false, null);        // 定义队列的消费者        QueueingConsumer consumer = new QueueingConsumer(channel);        // 监听队列        channel.basicConsume(QUEUE_NAME, true, consumer);        // 获取消息        while (true) {            QueueingConsumer.Delivery delivery = consumer.nextDelivery();            String message = new String(delivery.getBody());            System.out.println(" [x] Received '" + message + "'");        }    }}/*docker run -d --hostname localhost --name pdffiller/rabbitmq -p 15672:15672 -p 5672:5672* */


2.直连交互机


解释


直连型交换机(direct exchange)是根据消息携带的路由键(routing key)将消息投递给对应队列的。直连交换机用来处理消息的单播路由(unicast routing)(尽管它也可以处理多播路由)。下边介绍它是如何工作的:
  • 将一个队列绑定到某个交换机上,同时赋予该绑定一个路由键(routing key)
  • 当一个携带着路由键为R的消息被发送给直连交换机时,交换机会把它路由给绑定值同样为R的队列。
直连交换机经常用来循环分发任务给多个工作者(workers)。当这样做的时候,我们需要明白一点,在 AMQP 0-9-1 中,消息的负载均衡是发生在消费者(consumer)之间的,而不是队列(queue)之间。


图解


实现


模拟场景创建订单后 通知 order_service(订单系统) 添加订单,通知 mailbox_service(邮箱系统)发送邮件

发布者(publisher)

package com.test.路由模式_直连交互机;import com.rabbitmq.client.Channel;import com.rabbitmq.client.Connection;import com.test.utils.ConnectionUtil;/*** @作者: tjx* @描述:* @创建时间: 创建于17:26 2019/8/28**/public class Send {    private final static String EXCHANGE_NAME = "test_exchange_direct";    public static void main(String[] argv) throws Exception {        // 获取到连接以及mq通道        Connection connection = ConnectionUtil.getConnection();        Channel channel = connection.createChannel();        // 声明exchange        channel.exchangeDeclare(EXCHANGE_NAME, "direct");        // 消息内容        String message = "添加订单";        channel.basicPublish(EXCHANGE_NAME, "order.add", null, message.getBytes());        System.out.println(" [x] Sent '" + message + "'");        channel.close();        connection.close();    }}


消费者-订单系统(consumer)

package com.test.路由模式_直连交互机;import com.rabbitmq.client.Channel;import com.rabbitmq.client.Connection;import com.rabbitmq.client.QueueingConsumer;import com.test.utils.ConnectionUtil;/*** @作者: tjx* @描述:* @创建时间: 创建于17:14 2019/8/28**/public class OrderService {    private final static String QUEUE_NAME = "order_queue";    private final static String EXCHANGE_NAME = "test_exchange_direct";    public static void main(String[] argv) throws Exception {        // 获取到连接以及mq通道        Connection connection = ConnectionUtil.getConnection();        Channel channel = connection.createChannel();        // 声明队列        channel.queueDeclare(QUEUE_NAME, false, false, false, null);        // 绑定队列到交换机        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "order.add");        // 同一时刻服务器只会发一条消息给消费者        channel.basicQos(1);        // 定义队列的消费者        QueueingConsumer consumer = new QueueingConsumer(channel);        // 监听队列,手动返回完成        channel.basicConsume(QUEUE_NAME, false, consumer);        // 获取消息        while (true) {            QueueingConsumer.Delivery delivery = consumer.nextDelivery();            String message = new String(delivery.getBody());            System.out.println("接受到参数[" + message + "] 处理添加订单逻辑");            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);        }    }}

消费者-邮箱系统(consumer)

package com.test.路由模式_直连交互机;import com.rabbitmq.client.Channel;import com.rabbitmq.client.Connection;import com.rabbitmq.client.QueueingConsumer;import com.test.utils.ConnectionUtil;/*** @作者: tjx* @描述:* @创建时间: 创建于17:14 2019/8/28**/public class MailboxService {    private final static String QUEUE_NAME = "mailbox_queue";    private final static String EXCHANGE_NAME = "test_exchange_direct";    public static void main(String[] argv) throws Exception {        // 获取到连接以及mq通道        Connection connection = ConnectionUtil.getConnection();        Channel channel = connection.createChannel();        // 声明队列        channel.queueDeclare(QUEUE_NAME, false, false, false, null);        // 绑定队列到交换机        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "mailbox.send");        // 同一时刻服务器只会发一条消息给消费者        channel.basicQos(1);        // 定义队列的消费者        QueueingConsumer consumer = new QueueingConsumer(channel);        // 监听队列,手动返回完成        channel.basicConsume(QUEUE_NAME, false, consumer);        // 获取消息        while (true) {            QueueingConsumer.Delivery delivery = consumer.nextDelivery();            String message = new String(delivery.getBody());            System.out.println("接受到参数[" + message + "] 处理发送邮箱逻辑");            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);        }    }}

3.扇行交互机


解释

扇型交换机(funout exchange)将消息路由给绑定到它身上的所有队列,而不理会绑定的路由键。如果N个队列绑定到某个扇型交换机上,当有消息发送给此扇型交换机时,交换机会将消息的拷贝分别发送给这所有的N个队列。扇型用来交换机处理消息的广播路由(broadcast routing)。

因为扇型交换机投递消息的拷贝到所有绑定到它的队列,所以他的应用案例都极其相似:
  • 大规模多用户在线(MMO)游戏可以使用它来处理排行榜更新等全局事件
  • 体育新闻网站可以用它来近乎实时地将比分更新分发给移动客户端
  • 分发系统使用它来广播各种状态和配置更新
  • 在群聊的时候,它被用来分发消息给参与群聊的用户。(AMQP没有内置presence的概念,因此XMPP可能会是个更好的选择)


图解

实现

发布者(publisher)

package com.test.订阅模式_扇型交换机;import com.rabbitmq.client.Channel;import com.rabbitmq.client.Connection;import com.test.utils.ConnectionUtil;/*** @作者: tjx* @描述:* @创建时间: 创建于17:12 2019/8/28**/public class Send {    private final static String EXCHANGE_NAME = "test_exchange_fanout";    public static void main(String[] argv) throws Exception {        // 获取到连接以及mq通道        Connection connection = ConnectionUtil.getConnection();        Channel channel = connection.createChannel();        // 声明exchange        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");        // 消息内容        String message = "Hello World!";        channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());        System.out.println(" [x] Sent '" + message + "'");        channel.close();        connection.close();    }}

消费者1(consumer)

package com.test.订阅模式_扇型交换机;import com.rabbitmq.client.Channel;import com.rabbitmq.client.Connection;import com.rabbitmq.client.QueueingConsumer;import com.test.utils.ConnectionUtil;/*** @作者: tjx* @描述:* @创建时间: 创建于17:14 2019/8/28**/public class Recv {    private final static String QUEUE_NAME = "test_queue_work1";    private final static String EXCHANGE_NAME = "test_exchange_fanout";    public static void main(String[] argv) throws Exception {        // 获取到连接以及mq通道        Connection connection = ConnectionUtil.getConnection();        Channel channel = connection.createChannel();        // 声明队列        channel.queueDeclare(QUEUE_NAME, false, false, false, null);        // 绑定队列到交换机        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");        // 同一时刻服务器只会发一条消息给消费者        channel.basicQos(1);        // 定义队列的消费者        QueueingConsumer consumer = new QueueingConsumer(channel);        // 监听队列,手动返回完成        channel.basicConsume(QUEUE_NAME, false, consumer);        // 获取消息        while (true) {            QueueingConsumer.Delivery delivery = consumer.nextDelivery();            String message = new String(delivery.getBody());            System.out.println(" [Recv] Received '" + message + "'");            Thread.sleep(10);            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);        }    }}

消费者2(consumer)

package com.test.订阅模式_扇型交换机;import com.rabbitmq.client.Channel;import com.rabbitmq.client.Connection;import com.rabbitmq.client.QueueingConsumer;import com.test.utils.ConnectionUtil;/*** @作者: tjx* @描述:* @创建时间: 创建于17:14 2019/8/28**/public class Recv2 {    private final static String QUEUE_NAME = "test_queue_work2";    private final static String EXCHANGE_NAME = "test_exchange_fanout";    public static void main(String[] argv) throws Exception {        // 获取到连接以及mq通道        Connection connection = ConnectionUtil.getConnection();        Channel channel = connection.createChannel();        // 声明队列        channel.queueDeclare(QUEUE_NAME, false, false, false, null);        // 绑定队列到交换机        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");        // 同一时刻服务器只会发一条消息给消费者        channel.basicQos(1);        // 定义队列的消费者        QueueingConsumer consumer = new QueueingConsumer(channel);        // 监听队列,手动返回完成        channel.basicConsume(QUEUE_NAME, false, consumer);        // 获取消息        while (true) {            QueueingConsumer.Delivery delivery = consumer.nextDelivery();            String message = new String(delivery.getBody());            System.out.println(" [Recv2] Received '" + message + "'");            Thread.sleep(10);            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);        }    }}


4.主题交互机


解释
主题交换机(topic exchanges)通过对消息的路由键和队列到交换机的绑定模式之间的匹配,将消息路由给一个或多个队列。主题交换机经常用来实现各种分发 / 订阅模式及其变种。主题交换机通常用来实现消息的多播路由(multicast routing)。
主题交换机拥有非常广泛的用户案例。无论何时,当一个问题涉及到那些想要有针对性的选择需要接收消息的 多消费者 / 多应用(multiple consumers/applications) 的时候,主题交换机都可以被列入考虑范围。
使用案例:
  • 分发有关于特定地理位置的数据,例如销售点
  • 由多个工作者(workers)完成的后台任务,每个工作者负责处理某些特定的任务
  • 股票价格更新(以及其他类型的金融数据更新)
  • 涉及到分类或者标签的新闻更新(例如,针对特定的运动项目或者队伍)
  • 云端的不同种类服务的协调
  • 分布式架构 / 基于系统的软件封装,其中每个构建者仅能处理一个特定的架构或者系统。

图解


实现

模拟日志系统 ,所有的日志消息都会发送到(log_queue)日志队列,然后所有的错误日志会发送到
错误日志队列(error_log_queue)
所有的业务错误都会被发送到
业务错误队列(business_error_log_queue)
最后由消费者消费,生成文件:
#{time}.log 不管是否错误都会在这个里面
#{time}.error.log 只要是错误的就会在这个里面
#{time}.business.error.log 只有业务层自定义错误才会这这里面

发布者(publisher)

package com.test.主题模式_主题交换机;import com.rabbitmq.client.Channel;import com.rabbitmq.client.Connection;import com.test.utils.ConnectionUtil;/*** @作者: tjx* @描述:* @创建时间: 创建于11:22 2019/8/30**/public class Send {    private final static String EXCHANGE_NAME = "test_exchange_topic";    public static void main(String[] argv) throws Exception {        // 获取到连接以及mq通道        Connection connection = ConnectionUtil.getConnection();        Channel channel = connection.createChannel();        // 声明exchange        channel.exchangeDeclare(EXCHANGE_NAME, "topic");        // 消息内容        String message = "Hello World!!";        channel.basicPublish(EXCHANGE_NAME, "log.business.error", null, message.getBytes());        System.out.println(" [x] Sent '" + message + "'");        channel.close();        connection.close();    }}


日志队列(log_queue)

package com.test.主题模式_主题交换机;import com.rabbitmq.client.Channel;import com.rabbitmq.client.Connection;import com.rabbitmq.client.QueueingConsumer;import com.test.utils.ConnectionUtil;import jdk.nashorn.internal.runtime.logging.Logger;/*** @作者: tjx* @描述:* @创建时间: 创建于11:23 2019/8/30**/public class LogAllService {    private final static String QUEUE_NAME = "log_queue";    private final static String EXCHANGE_NAME = "test_exchange_topic";    public static void main(String[] argv) throws Exception {        // 获取到连接以及mq通道        Connection connection = ConnectionUtil.getConnection();        Channel channel = connection.createChannel();        // 声明队列        channel.queueDeclare(QUEUE_NAME, false, false, false, null);        // 绑定队列到交换机        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "log.#");        // 同一时刻服务器只会发一条消息给消费者        channel.basicQos(1);        // 定义队列的消费者        QueueingConsumer consumer = new QueueingConsumer(channel);        // 监听队列,手动返回完成        channel.basicConsume(QUEUE_NAME, false, consumer);        // 获取消息        while (true) {            QueueingConsumer.Delivery delivery = consumer.nextDelivery();            String message = new String(delivery.getBody());            System.out.println("接受到参数[" + message + "] ,写入#{time}.log ");            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);        }    }}

错误日志队列(error_log_queue)

package com.test.主题模式_主题交换机;import com.rabbitmq.client.Channel;import com.rabbitmq.client.Connection;import com.rabbitmq.client.QueueingConsumer;import com.test.utils.ConnectionUtil;/*** @作者: tjx* @描述:* @创建时间: 创建于11:23 2019/8/30**/public class LogErrorService {    private final static String QUEUE_NAME = "log_queue_queue";    private final static String EXCHANGE_NAME = "test_exchange_topic";    public static void main(String[] argv) throws Exception {        // 获取到连接以及mq通道        Connection connection = ConnectionUtil.getConnection();        Channel channel = connection.createChannel();        // 声明队列        channel.queueDeclare(QUEUE_NAME, false, false, false, null);        // 绑定队列到交换机        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "log.#.error");        // 同一时刻服务器只会发一条消息给消费者        channel.basicQos(1);        // 定义队列的消费者        QueueingConsumer consumer = new QueueingConsumer(channel);        // 监听队列,手动返回完成        channel.basicConsume(QUEUE_NAME, false, consumer);        // 获取消息        while (true) {            QueueingConsumer.Delivery delivery = consumer.nextDelivery();            String message = new String(delivery.getBody());            System.out.println("接受到参数[" + message + "] ,写入#{time}.error.log ");            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);        }    }}

业务错误队列(business_error_log_queue)

package com.test.主题模式_主题交换机;import com.rabbitmq.client.Channel;import com.rabbitmq.client.Connection;import com.rabbitmq.client.QueueingConsumer;import com.test.utils.ConnectionUtil;/*** @作者: tjx* @描述:* @创建时间: 创建于11:23 2019/8/30**/public class LogBusinessErrorService {    private final static String QUEUE_NAME = "log_business_queue";    private final static String EXCHANGE_NAME = "test_exchange_topic";    public static void main(String[] argv) throws Exception {        // 获取到连接以及mq通道        Connection connection = ConnectionUtil.getConnection();        Channel channel = connection.createChannel();        // 声明队列        channel.queueDeclare(QUEUE_NAME, false, false, false, null);        // 绑定队列到交换机        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "log.business.error");        // 同一时刻服务器只会发一条消息给消费者        channel.basicQos(1);        // 定义队列的消费者        QueueingConsumer consumer = new QueueingConsumer(channel);        // 监听队列,手动返回完成        channel.basicConsume(QUEUE_NAME, false, consumer);        // 获取消息        while (true) {            QueueingConsumer.Delivery delivery = consumer.nextDelivery();            String message = new String(delivery.getBody());            System.out.println("接受到参数[" + message + "] ,写入#{time}.business.error.log ");            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);        }    }}


4.头交互机


解释
有时消息的路由操作会涉及到多个属性,此时使用消息头就比用路由键更容易表达,头交换机(headers exchange)就是为此而生的。头交换机使用多个消息属性来代替路由键建立路由规则。通过判断消息头的值能否与指定的绑定相匹配来确立路由规则。
我们可以绑定一个队列到头交换机上,并给他们之间的绑定使用多个用于匹配的头(header)。这个案例中,消息代理得从应用开发者那儿取到更多一段信息,换句话说,它需要考虑某条消息(message)是需要部分匹配还是全部匹配。上边说的 “更多一段消息” 就是 "x-match" 参数。当 "x-match" 设置为 “any” 时,消息头的任意一个值被匹配就可以满足条件,而当 "x-match" 设置为 “all” 的时候,就需要消息头的所有值都匹配成功。
头交换机可以视为直连交换机的另一种表现形式。头交换机能够像直连交换机一样工作,不同之处在于头交换机的路由规则是建立在头属性值之上,而不是路由键。路由键必须是一个字符串,而头属性值则没有这个约束,它们甚至可以是整数或者哈希值(字典)等。