视频:【黑马程序员RabbitMQ全套教程,rabbitmq消息中间件到实战】 https://www.bilibili.com/video/BV15k4y1k7Ep/?share_source=copy_web&vd_source=94d49d29c066af15c2803ab959fb6297
消息指的是两个应用间传递的数据。数据的类型有很多种形式,可能只包含文本字符串,也可能包含嵌入对象。
“消息队列(Message Queue)”是在消息的传输过程中保存消息的容器,多用于分布式系统之间进行通信。在消息队列中,通常有生产者和消费者两个角色。生产者只负责发送数据到消息队列,消费者只负责从消息队列中取出数据处理
优点
消息队列主要有三个作用(优点):
应用解耦,提升容错性和可维护性。如下图所示:假设有系统B、C、D都需要系统A的数据,系统A调用三个方法发送数据到B、C、D。这时,系统D不需要了,那就需要在系统A把相关的代码删掉。假设这时有个新的系统E需要数据,这时系统A又要增加调用系统E的代码。为了降低这种强耦合,就可以使用MQ,系统A只需要把数据发送到MQ,其他系统如果需要数据,则从MQ中获取即可。
异步提速,提升用户体验和系统吞吐量(单位时间内处理请求的数目)。如下图所示:一个客户端请求发送进来,系统A会调用系统B、C、D三个系统,同步请求的话,响应时间就是系统A、B、C、D的总和,也就是800ms。如果使用MQ,系统A发送数据到MQ,然后就可以返回响应给客户端,不需要再等待系统B、C、D的响应,可以大大地提高性能。对于一些非必要的业务,比如发送短信,发送邮件等等,就可以采用MQ。
削峰填谷,提高系统稳定性。如下图所示:这其实是MQ一个很重要的应用。假设系统A在某一段时间请求数暴增,有5000个请求发送过来,系统A这时就会发送5000条SQL进入MySQL进行执行,MySQL对于如此庞大的请求当然处理不过来,MySQL就会崩溃,导致系统瘫痪。如果使用MQ,系统A不再是直接发送SQL到数据库,而是把数据发送到MQ,MQ短时间积压数据是可以接受的,然后由消费者每次拉取1000条进行处理,防止在请求峰值时期大量的请求直接发送到MySQL导致系统崩溃。
使用了 MQ 之后,限制消费消息的速度为1000,这样一来,高峰期产生的数据势必会被积压在 MQ 中,高峰就被“削”掉了,但是因为消息积压,在高峰期过后的一段时间内,消费消息的速度还是会维持在1000,直到消费完积压的消息,这就叫做“填谷”。
缺点
系统可用性降低
系统引入的外部依赖越多,系统稳定性越差。一旦 MQ 宕机,就会对业务造成影响。如何保证MQ的高可用?
系统复杂度提高
MQ 的加入大大增加了系统的复杂度,以前系统间是同步的远程调用,现在是通过 MQ 进行异步调用。如何
保证消息没有被重复消费?怎么处理消息丢失情况?那么保证消息传递的顺序性?
一致性问题
A 系统处理完业务,通过 MQ 给B、C、D三个系统发消息数据,如果 B 系统、C 系统处理成功,D 系统处理
失败。如何保证消息数据处理的一致性?
应用
既然 MQ 有优势也有劣势,那么使用 MQ 需要满足什么条件呢?
目前业界有很多的 MQ 产品,例如 RabbitMQ、RocketMQ、ActiveMQ、Kafka、ZeroMQ、MetaMq等,也有直接使用 Redis 充当消息队列的案例,而这些消息队列产品,各有侧重,在实际选型时,需要结合自身需求及 MQ 产品特征,综合考虑。
AMQP
,即 Advanced Message Queuing Protocol
(高级消息队列协议),是一个网络协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。2006年,AMQP 规范发布。类比HTTP
。
2007年,Rabbit 技术公司基于 AMQP 标准开发的 RabbitMQ 1.0 发布。RabbitMQ 采用 Erlang 语言开发。
Erlang 语言由 Ericson 设计,专门为开发高并发和分布式系统的一种语言,在电信领域使用广泛。
RabbitMQ 基础架构如下图:
相关概念
Broker
:接收和分发消息的应用,RabbitMQ Server 就是 Message BrokerVirtual host
:出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多个vhost,每个用户在自己的 vhost 创建 exchange/queue 等Connection
:publisher/consumer 和 broker 之间的 TCP 连接Channel
:如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP Connection 的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的 channel 进行通讯,AMQP method 包含了channel id 帮助客户端和 message broker 识别 channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的 Connection 极大减少了操作系统建立 TCP connection 的开销Exchange
:message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到queue 中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast)Queue
:消息最终被送到这里等待 consumer 取走Binding
:exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key。Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据工作模式
RabbitMQ 提供了 6 种工作模式:简单模式、work queues、Publish/Subscribe 发布与订阅模式、Routing路由模式、Topics 主题模式、RPC 远程调用模式(远程调用,不太算 MQ;暂不作介绍)。
官网对应模式介绍:https://www.rabbitmq.com/getstarted.html
通过docker
启动rabbitmq-3.9-management
容器:
docker pull rabbitmq:management
docker run -d \
-p 5672:5672 \
-p 15672:15672 \
-e RABBITMQ_DEFAULT_VHOST=/ziang-rabbitmq \
-e RABBITMQ_DEFAULT_USER=ziang \
-e RABBITMQ_DEFAULT_PASS=ziang \
--name ziang-rabbitmq
rabbitmq:management
docker exec -it ziang-rabbitmq rabbitmq-plugins enable rabbitmq_management
此时可使用Chrome浏览器无痕浏览(最好使用无痕,否则可能会出现页面元素Error问题),输入localhost:15672
进入RabbitMQ管理界面
public static void main(String[] args) throws IOException, TimeoutException {
// 1.创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
// 2.设置参数
factory.setHost("localhost"); // 主机IP,默认localhost
factory.setPort(5672); // 主机端口,默认5672
factory.setVirtualHost("/ziang-rabbitmq"); // 虚拟机,默认/
factory.setUsername("ziang"); // 用户名,默认guest
factory.setPassword("ziang"); // 密码,默认guest
// 3.创建连接Connection
Connection connection = factory.newConnection();
// 4.创建通道Channel
Channel channel = connection.createChannel();
// 5.创建队列Queue
/*
queueDeclare(String queue, boolean durable,
boolean exclusive, boolean autoDelete, Map arguments);
参数:
1.queue:队列名称(如果不存在该队列,则会创建)
2.durable:是否持久化。当MQ重启后消息是否仍存在(消息会持久化于erlang实现的数据库中)
3.exclusive:
* 是否独占(只能有一个消费者监听这个队列)
* 当Connection关闭时,是否删除队列
4.autoDelete:是否自动删除(当无Consumer时自动删除)
5.arguments:其他参数
*/
String queue = "hello_world";
channel.queueDeclare(queue, true, false, false, null);
// 6.发送消息
/*
basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body);
参数:
1.exchange:交换机名称。简单模式下交换机会使用默认的""
2.routingKey:路由名称
3.props:其他配置
4.body:发送的消息数据
*/
String msg = "Hello World!";
channel.basicPublish("", queue, null, msg.getBytes(StandardCharsets.UTF_8));
// 7.释放资源(可以选择性执行该步骤,观察rabbitmq_management信息变化)
channel.close();
connection.close();
}
public static void main(String[] args) throws IOException, TimeoutException {
// 1.创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
// 2.设置参数
factory.setHost("localhost"); // 主机IP,默认localhost
factory.setPort(5672); // 主机端口,默认5672
factory.setVirtualHost("/ziang-rabbitmq"); // 虚拟机,默认/
factory.setUsername("ziang"); // 用户名,默认guest
factory.setPassword("ziang"); // 密码,默认guest
// 3.创建连接Connection
Connection connection = factory.newConnection();
// 4.创建通道Channel
Channel channel = connection.createChannel();
// 5.创建队列Queue
/*
queueDeclare(String queue, boolean durable,
boolean exclusive, boolean autoDelete, Map arguments);
参数:
1.queue:队列名称(如果不存在该队列,则会创建)
2.durable:是否持久化。当MQ重启后消息是否仍存在(消息会持久化于erlang实现的数据库中)
3.exclusive:
* 是否独占(只能有一个消费者监听这个队列)
* 当Connection关闭时,是否删除队列
4.autoDelete:是否自动删除(当无Consumer时自动删除)
5.arguments:其他参数
*/
String queue = "hello_world";
channel.queueDeclare(queue, true, false, false, null);
// 6.接受消息
/*
basicConsume(String queue, boolean autoAck, Consumer callback);
参数:
1.queue:队列名称
2.autoAck:是否自动确认
3.callback:回调对象
*/
channel.basicConsume(queue, true, new DefaultConsumer(channel) {
/**
* 回调方法,当收到消息后会自动执行该方法
* @param consumerTag 标识信息(该消费者唯一标识)
* @param envelope 更多信息(交换机、路由key等信息)
* @param properties 配置信息
* @param body 消息数据
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("consumerTag: " + consumerTag);
System.out.println("exchange: " + envelope.getExchange());
System.out.println("routingKey: " + envelope.getRoutingKey());
System.out.println("properties: " + properties);
System.out.println("body: " + new String(body));
}
});
// 需要释放资源?NO,一旦关闭则无法监听连接与接受消息
}
从上面的HelloWord例子中,我们大概也能体验到一些,就是RabbitMQ的组成,它是有这几部分:
这些组成部分是如何协同工作的呢,大概的流程如下,请看下图:
RabbitMQ 提供了 6 种工作模式:简单模式、work queues、Publish/Subscribe 发布与订阅模式、Routing路由模式、Topics 主题模式、RPC 远程调用模式(远程调用,不太算 MQ;暂不作介绍)。
官网对应模式介绍:https://www.rabbitmq.com/getstarted.html
在上图的模型中,有以下概念:
Work Queues:与入门程序的简单模式相比,多了一个或一些消费端,多个消费端共同消费同一个队列中的消息。
应用场景:对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。
特点:
在订阅模型中,多了一个 Exchange 角色,而且过程略有变化:
特点:
队列与交换机的绑定,不能是任意绑定了,而是要指定一个 RoutingKey(路由key)
图解:
特点:
Routing 模式要求队列在绑定交换机时要指定 routing key,消息会转发到符合 routing key 的队列
Topic 类型与 Direct 相比,都是可以根据 RoutingKey 把消息路由到不同的队列。只不过 Topic 类型Exchange 可以让队列在绑定 Routing key 的时候使用通配符!
特点:
Topic 主题模式可以实现 Pub/Sub 发布与订阅模式和 Routing 路由模式的功能,只是 Topic 在配置routing key 的时候可以使用通配符,显得更加灵活。
工作模式总结:
RabbitMQ 消息确认机制分为两大类:发送方确认、接收方确认。
其中发送方确认又分为:生产者到交换机到确认、交换机到队列的确认。
在使用 RabbitMQ 的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景。RabbitMQ 为我们提供了两种方式用来控制消息的投递可靠性模式。
rabbitmq 整个消息投递的路径为:producer ➡️ rabbitmq broker ➡️ exchange ➡️ queue ➡️ consumer
我们将利用这两个 callback 控制消息的可靠性投递
使用:
设置ConnectionFactory的publisher-confirms=“true” 开启 确认模式。
设置ConnectionFactory的publisher-returns=“true” 开启 退回模式,设置rabbitTemplate.setMandatory(true)。
在RabbitMQ中也提供了事务机制,但是性能较差,此处不做讲解。使用channel下列方法,完成事务控制:
Ack指Acknowledge确认。 表示消费端收到消息后的确认方式。有三种确认方式:
其中自动确认是指,当消息一旦被Consumer接收到,则自动确认收到,并将相应 message 从RabbitMQ 的消息缓存中移除。但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失。如果设置了手动确认方式,则需要在业务处理成功后,调用channel.basicAck(),手动签收,如果出现异常,则调用channel.basicNack()方法,让其自动重新发送消息。
使用
basicReject与basicNack相比较,只是前者不允许批量拒绝
Consumer可以通过配置,一次性仅拉取一定数量的消息,直到手动签收才会拉取下一条。通过此种方式达到限流的目的。
使用
设置队列过期时间使用参数:x-message-ttl,单位:ms(毫秒),会对整个队列消息统一过期。
设置消息过期时间使用参数:expiration。单位:ms(毫秒),当该消息在队列头部时(消费时),会单独判断其是否过期,若过期则移除。
如果两者都进行了设置,以时间短的为准。
死信交换机(Dead Letter Exchange)英文缩写:DLX。当消息成为死信(Dead Message)后,可以被重新发送到另一个交换机,这个交换机就是DLX。与DLX所绑定的队列就是死信队列(在其他MQ产品中也存在死信队列,但不存在交换机的概念)
给队列设置参数:x-dead-letter-exchange
和 x-dead-letter-routing-key
问题2:消息什么时候会成为死信?
三种情况:
basicNack
/basicReject
。并且不把消息重新放回原目标队列requeue=false
;延迟队列:消息进入队列后不会立即被消费,只有到达指定时间后才会被消费
需求:
实现方式:
但是在RabbitMQ中并未提供延时队列的实现,但我们可以使用组合TTL与死信队列来实现。
即给TTL队列发送消息,消息过期后会发送给DLX队列,消费者监听DLX队列。以此达到延迟效果
RabbitMQ默认日志存放路径:/var/log/rabbitmq/[email protected](xxx指主机名)
日志包含了RabbitMQ的版本号、Erlang的版本号、RabbitMQ服务节点名称、cookie的hash值、
RabbitMQ配置文件地址、内存限制、磁盘限制、默认账户guest的创建以及权限配置等等。
在使用任何消息中间件的过程中,难免会出现某条消息异常丢失的情况。对于RabbitMQ而言,可能
是因为生产者或消费者与RabbitMQ断开了连接,而它们与RabbitMQ又采用了不同的确认机制;也
有可能是因为交换器与队列之间不同的转发策略;甚至是交换器并没有与任何队列进行绑定,生产者又不感知或者没有采取相应的措施;另外RabbitMQ本身的集群策略也可能导致消息的丢失。这个时候就需要有一个较好的机制跟踪记录消息的投递过程,以此协助开发和运维人员进行问题的定位。
在RabbitMQ中可以使用Firehose和rabbitmq_tracing插件功能来实现消息追踪。
firehose的机制是将生产者投递给rabbitmq的消息,rabbitmq投递给消费者的消息按照指定的格式
发送到默认的exchange上。这个默认的exchange的名称为amq.rabbitmq.trace,它是一个topic类
型的exchange。发送到这个exchange上的消息的routing key为 publish.exchangename 和
deliver.queuename。其中exchangename和queuename为实际exchange和queue的名称,分别
对应生产者投递到exchange的消息,和消费者从queue上获取的消息。
注意:打开 trace 会影响消息写入功能,适当打开后请关闭。
开启消息追踪,RabbitMQ性能会有所下降
需求:100%确保消息发送成功
幂等性:一次和多次请求某一个资源,对于资源本身应该具有同样的结果。也就是说,其任
意多次执行对资源本身所产生的影响均与一次执行的影响相同。在MQ中指,消费多条相同的消息,得到与消费该消息一次相同的结果。