最近一段项目实践中大量使用了基于RabbitMQ的消息中间件,也积累的一些经验和思考,特此成文,望大家不吝赐教。

本文包括RabbitMQ基本概念、进阶概念、实践与思考等三部分,着重强调相关概念基于RabbitMQ进行扩展开发的思路,并简要展示RabbitMQ客户端的编码,接下来通过一个思维导图来展示整体思路,红星表示重点部分。

1.基本概念

官方文档: http://www.rabbitmq.com/#getstarted

1.1.核心实体

进入详细介绍之前,先来看一张简化版的消息流转的模型图。

  • a.生产者Producer发送消息,消息分为消息标识和消息体(payLoad有效载荷)两部分,消息标识中包含Exchange交换器的名字和RoutingKey路由键信息。
  • b.由于交换器之前已经通过2个不同的BindingKey绑定键分别绑定了两个队列,因此交换器可以对比路由键和绑定键,之后将消息路由到匹配的队列中。
  • c.队列将消息推送到指定的一个或多个消费者中,多个消费者会选择最简单的RoundRobin轮训方式进行选择。

Exchange交换器

核心概念,可以简化理解为路由器,其不存储数据,其通常会和一个队列绑定,但也可以绑定到另一个交换器上。其包括4种类型的交换器类型,生产实践中主要使用可以精细管理的direct和topic两种。direct,路由规则为完全匹配;topic,支持完全匹配,也支持模糊匹配;fanout,会将消息转发到该交换器绑定的所有队列中;header,实际中无应用。

Tip:

不要direct和topic这两个迷惑,其实他们都可以支持关联多个Queue,区别仅仅在于是否支持RoutingKey的模糊匹配。

Queue队列

用于存储消息,和Kafka的消息模型完全不同,其会将消息存储在Topic中。因此在实现类似ConsumerGroup概念时差异很大,Kafka是可以回溯消息的,但Rabbit新绑定的队列的数据是空的,不能回溯。

Binding绑定

其通过绑定键将交换器和队列关联起来

RouteKey & BindingKey 路由键和绑定键

通常会将路由键和绑定键都称为路由键,其差异是路由键是包含在消息标识中的,而绑定键是用于在交换器和队列间建立绑定关系的,消息会通过它们的匹配情况进行路由。

1.2.通信

通信实体

包括Connection连接和Channel通道,连接通常对应一个基于TCP的Socket,建立Connection的关键参数包括用户名、密码、虚拟主机、主机地址和端口。一个连接可以建立多个Channel实例,推荐控制数量(比如10个),但Channel实例不能在线程间共享,应用程序需要为每一个线程开辟一个Channel。

AMQP协议

Java技术栈汇中,关于消息通信听到比较多的是JMS,而AMQP协议相对更加严格一些,其包括Module Layer,Session Layer, Transport Layer三个层次,业务开发主要接触到的是Module Layer,客户端可以通过Queue.Declare、Basic.Consume等命令进行操作。

1.3.虚拟主机与用户

vhost

虚拟主机,可以在逻辑上看做一台RabbitMQ服务器,其拥有自己的交换器、队列和绑定关系等。RabbitMQ对权限的管理就是基于vhost进行的,默认会创建一个全局的/虚拟主机,通常不推荐直接使用该vhost,而是需要自定义一个vhost便于管理。

User

对于某一个用户,通常包括3种类型的权限:read,允许读取队列数据;write,允许向队列发送数据;config,允许创建队列,如果客户端需要支持添加队列,需要添加该权限,否则会报无权限错误【踩过坑】。

2.进阶概念

2.1.交换器与队列增强

TTL过期时间

目前在两个不同的粒度设置消息的TTL,分别是队列粒度和消息粒度。由于RabbitMQ实际机制的原因,通常都选择的是队列粒度,对于队列粒度来说,队列头的消息一定是最先失效的,因此可以高效的判断和丢弃。而对于消息粒度,其需要在消息真正投递到消费者时进行判断,如果该消息之前的消息并没有失效,那么它将一直存活。

死信交换器DLX

全称为 Dead-Letter-Exchange,也是RabbitMQ扩展开发的核心概念,当一条消息在一个队列中变成死信之后,它能自动的被转发到一个交换器中,这个交换器就是DLX,很多地方称和这个交换器绑定的队列是死信队列, 我并不是完全认同。

消息变为死信的原因

消息被拒绝Nack,Reject,并且requeue参数为false(重点强调一下,生产实践中通常不能打开requeue,因为打开后队列中的消息就会出现乱序的情况,且性能很差);消息过期;队列达到最大长度。

DLX 也是一个正常的交换器,和一般的交换器没有区别,它能在任何的队列上被指定 ,实际上就是设置某个队列的属性。

Map<String, Object> args = new HashMap<String, Object>();
args.put("x-dead-letter-exchange", "dlx_exchange");
args.put("x-dead-letter-routing-key" , "dlx-routing-key");
channel.exchangeDeclare("dlx_exchange" , "direct"); //创建 DLX:
channel.queueDeclare("normal_queue", false, false, false, args); //为队列normal_queue添加 DLX

命名规范:队列类型,[生产者.消费者.队列名后缀];Topic类型,[生产者.exchange.队列名后缀]

延迟队列

延迟队列存储的对象是对应的延迟消息,所谓"延迟消息"是指当消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。使用延迟消息场景如在订单系统中,希望用户下单后30分钟内支付,否则取消订单。那么业务系统可以在下单后,发送延迟消息,到达指定时间后消费该消息来判断是否支持。该方式在数据量比较大的场景中比通过Job扫描数据表合适。

在AMQP协议中,或者RabbitMQ本身没有直接支持延迟队列的功能,但是可以通过前面 所介绍的DLX和TTL模拟出延迟队列的功能,这部分在实践与思考部分进行介绍。

持久化

交换器和队列元数据持久化和消息的持久化,消息的持久化可以直接使用MessageProperties.PERSISTENT_TEXT_PLAIN

2.2.生产者

生产者客户端的代码比较简洁,如下所示。

byte[] messageBodyBytes = "Hello , Xionger!".getBytes();
channel.basicPublish(exchangeName, routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN,
messageBodyBytes);

从高可用HA的角度不经要问,消息的生产者将消息发送出去之后,Broker是否收到消息。RabbitMQ针对这个问题提供了两种解决方案,分别是事务机制和发送方确认PublisherConfirm。发送者确认的实现继续细分为3种形式,包括单条同步、批量同步和异步方式。事务机制和单条同步确认方式的性能都比较差,通常只能达到2000QPS左右,因此通常推荐使用发送方确认的批量方式和异步方式,其QPS可以达到8000QPS以上。其中批量方式也存在一个隐患,即发送一批消息到服务端时,如果有一条消息失败,那么该批次所有消息都需要重试。因此目前生产实践中 ,使用的是异步方式,简化的代码实践如下所示。

SortedSet confirmSet = Sets.newTreeSet();
channel.confirmSelect();
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
if (multiple) {
confirmSet.headSet(deliveryTag - 1);
} else {
confirmSet.remove(deliveryTag);
}
}
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
//omit
//消息重新投递处理
}
});

tip: 这部分在服务端ack时有一个优化,只会回传当前最大的标识,可以有效减少比对次数。

2.3.消费者

消费模式:拉模式,推模式,RabbitMQ推荐推模式,保持消息消费的有序性。

boolean autoAck = false;
channel.basicQos(64);//prefetchCount
channel.basicConsume(queueName, autoAck, "myConsumerTag",
new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String routingKey = envelope.getRoutingKey();
String contentType = properties.getContentType();
long deliveryTag = envelope.getDeliveryTag();
channel.basicAck(deliveryTag, false);
}
});

tip:

对于消息生产者,过去还有一个消息投递不可达被返回的概念,涉及mandatory和immediate两个参数,但其在生产实践中并不常用。

3.实践与思考

3.1.环境搭建

安装:Mac环境 brew install rabbitmq,非常简便

管理界面

  • unacked: 消费端没有Ack的数量
  • Publish: 推送消息的QPS
  • Deliver(manual ack): 手动Ack
  • durable: 持久化
  • Policy: 队列的规则
  • Mirrors: 镜像Broker

3.2.Client组件开发

在介绍了RabbitMQ主要知识后,扩展的分享一个简易的基于RabbitMQ消息中间件的思路。由于RabbitMQ是基于Erlang开发,虽然很棒但毕竟比较小众,Java技术栈的工程师一般很难去修改RabbitMQ的源码,因此通常只是通过构建一个合理的客户端SDK来支持业务开发。

生产者

生产者目标比较简单,需要实现健壮性强的的发送者确认机制【异步】和支持队列分片,队列分片可以给队列加上后缀标识,然后轮训处理即可。

消费者

消费者部分希望支持消费失败的重试机制、死信队列及其报警机制,以支持3次重试消费为例,整体思路如下图所示。【借助之前介绍的TTL和DLX】

Tip:

上图和真实实现还有一个小小的差异,就是retry2其实和retry[0-1]是在一起的,其由客户端判断次数然后直接通过DL交换机路由到DL队列。

3.3.场景思考

RabbitMQ最大的特点是成熟度高,管理功能全面,近似开箱即用,二次开发实现一个简单靠谱的客户端就足以满足大部分的场景,尤其对于初创企业、中小企业来说是一个非常棒的选择。

  • 高可用HA:源生支持镜像服务器、同步模型等机制
  • 高吞吐:可以通过队列分片的方式支持大量的QPS,比如单个队列推荐QPS4000以下,健康的水位在2000左右,那么就需要通过二次开发客户端来实现队列分片。

参考资料

[1]朱忠华.RabbitMQ实战指南[M].电子工业出版社:北京,2017.11:.

下期预告:深入理解MySQL索引机制

善良比聪明更重要--张小龙

RabbitMQ快速入门的更多相关文章

  1. 中小研发团队架构实践之RabbitMQ快速入门及应用

    原文:中小研发团队架构实践之RabbitMQ快速入门及应用 使用过分布式中间件的人都知道,程序员使用起来并不复杂,常用的客户端API就那么几个,比我们日常编写程序时用到的API要少得多.但是分布式中间 ...

  2. RabbitMQ(一):RabbitMQ快速入门

    RabbitMQ是目前非常热门的一款消息中间件,不管是互联网大厂还是中小企业都在大量使用.作为一名合格的开发者,有必要对RabbitMQ有所了解,本文是RabbitMQ快速入门文章. RabbitMQ ...

  3. RabbitMQ快速入门python教程

    摘要:HelloWorld 简介 RabbitMQ:接受消息再传递消息,可以视为一个“邮局”.发送者和接受者通过队列来进行交互,队列的大小可以视为无限的,多个发送者可以发生给一个队列,多个接收者也可以 ...

  4. RabbitMQ基础入门篇

    下载安装 Erlang RabbitMQ 启动RabbitMQ管理平台插件 DOS下进入到安装目录\sbin,执行以下命令 rabbitmq-plugins enable rabbitmq_manag ...

  5. RabbitMQ学习总结 第二篇:快速入门HelloWorld

    目录 RabbitMQ学习总结 第一篇:理论篇 RabbitMQ学习总结 第二篇:快速入门HelloWorld RabbitMQ学习总结 第三篇:工作队列Work Queue RabbitMQ学习总结 ...

  6. 消息中间件——RabbitMQ(五)快速入门生产者与消费者,SpringBoot整合RabbitMQ!

    前言 本章我们来一次快速入门RabbitMQ--生产者与消费者.需要构建一个生产端与消费端的模型.什么意思呢?我们的生产者发送一条消息,投递到RabbitMQ集群也就是Broker. 我们的消费端进行 ...

  7. 快速入门分布式消息队列之 RabbitMQ(3)

    目录 目录 前文列表 前言 通道 Channel 一个基本的生产者消费者实现 消费者 生产者 运行结果 应用预取计数 应用 ACK 机制 最后 前文列表 快速入门分布式消息队列之 RabbitMQ(1 ...

  8. 快速入门分布式消息队列之 RabbitMQ(2)

    目录 目录 前文列表 RabbitMQ 的特性 Message Acknowledgment 消息应答 Prefetch Count 预取数 RPC 远程过程调用 vhost 虚拟主机 插件系统 最后 ...

  9. 快速入门分布式消息队列之 RabbitMQ(1)

    目录 目录 前言 简介 安装 RabbitMQ 基本对象概念 Message 消息 Producer 生产者 Consumer 消费者 Queue 队列 Exchange 交换机 Binding 绑定 ...

随机推荐

  1. Confluence 6 数据库连接方式

    你可以使用 JDBC URL 或者一个 JNDI 数据源来连接 Confluence 到你的数据库. 在默认的设置向导中,只提供了使用 JDBC 数据库连接选项,这个也是推荐的数据库连接选项. 如果你 ...

  2. Confluence 6 注册外部小工具

    你可以从外部站点中注册小工具(Gadget)(例如 Jira 应用),你注册成功的小工具将会在 宏浏览器中显示出来,使用你 Confluence 站点的用户可以使用 Gadget Macro 来调用它 ...

  3. 洛谷 P3627 [APIO2009]抢掠计划

    这题一看就是缩点,但是缩完点怎么办呢?首先我们把所有的包含酒吧的缩点找出来,打上标记,然后建立一张新图, 每个缩点上的点权就是他所包含的所有点的点权和.但是建图的时候要注意,每一对缩点之间可能有多条边 ...

  4. Java编程之前的复习和练习

    日期:2018.7.14 星期六 博客期:001 今天先是试着写一下博客,最近去青海旅游了,学习时间有点少,但空余时间还是有学习的,不管怎么样吧!先说一下我的这几天的成果——“Bignum”类,虽然很 ...

  5. ubuntu sublime text 3 安装

    #安装GPG wget -qO - https://download.sublimetext.com/sublimehq-pub.gpg | sudo apt-key add - #确保apt被设置为 ...

  6. mysql 修改配置文件my.cnf失败

    一.连接Mysql提示无法通过socket的解决方法连接到本地MySQL服务器 http://www.aiezu.com/db/mysql_cant_connect_through_socket.ht ...

  7. Linux 编程笔记(四)

    一.用户和用户组管理 添加新的用户账户使用useradd 格式useradd   选项  用户名 1.创建一个用户tian 其中 -d -m参数用来为登陆,登录名产生一个主目录 /usr/tian(其 ...

  8. Brup Suite 渗透测试笔记(七)

    继续接上次笔记: 1.Burp Intruder的payload类型的子模块(Character blocks)使用一种给出的输入字符,根据指定的设置产生指定大小的字符块,表现形式为生成指定长度的字符 ...

  9. jQuery示例

    <!DOCTYPE html><html lang="en" class="loading"><head> <meta ...

  10. 饮冰三年-人工智能-linux-04 vim编辑器

    vim的三种模式:命令行模式.编辑模式.扩展模式 1:命令行模式下常见的操作 删除 a):dd 删除光标所在当前行 b):ndd   删除光标所在当前行后的n行 复制 c):yy 复制光标所在当前行 ...