什么是消息中间件?
消息指的是两个应用间传递的数据。数据的类型有很多种形式,可能只包含文本字符串,也可能包含嵌入对象。
“消息队列(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短时间积压数据是可以接受的,然后由消费者每次拉取2000条进行处理,防止在请求峰值时期大量的请求直接发送到MySQL导致系统崩溃。
同时也会面对一些问题:
- 系统可用性降低:系统引入的外部依赖越多,系统稳定性越差。一旦MQ宕机,就会对业务造成影响。
- 系统复杂度提高 :MQ的加入大大增加了系统的复杂度,以前系统间是同步的远程调用,现在是通过MQ进行异步调 用。
- 一致性问题:A系统处理完业务,通过MQ给B、C、D三个系统发消息数据,如果B系统、C系统处理成功,D系 统处理失败,则会造成数据处理的不一致。
Rabbit的特点?
RabbitMQ是一款使用Erlang语言开发的,实现AMQP(高级消息队列协议)的开源消息中间件。首先要知道一些RabbitMQ的特点,官网可查:
- 可靠性。支持持久化,传输确认,发布确认等保证了MQ的可靠性。
- 灵活的分发消息策略。这应该是RabbitMQ的一大特点。在消息进入MQ前由Exchange(交换机)进行路由消息。分发消息策略有:简单模式、工作队列模式、发布订阅模式、路由模式、通配符模式。
- 支持集群。多台RabbitMQ服务器可以组成一个集群,形成一个逻辑Broker。
- 多种协议。RabbitMQ支持多种消息队列协议,比如 STOMP、MQTT 等等。
- 支持多种语言客户端。RabbitMQ几乎支持所有常用编程语言,包括 Java、.NET、Ruby 等等。
- 可视化管理界面。RabbitMQ提供了一个易用的用户界面,使得用户可以监控和管理消息 Broker。
- 插件机制。RabbitMQ提供了许多插件,可以通过插件进行扩展,也可以编写自己的插件。
Rabbit中的组成部分?
RabbitMQ的组成,它是有这几部分:
- Broker:消息队列服务进程。此进程包括两个部分:Exchange和Queue。
- Exchange:消息队列交换机。按一定的规则将消息路由转发到某个队列。
- Queue:消息队列,存储消息的队列。
- Producer:消息生产者。生产方客户端将消息同交换机路由发送到队列中。
- Consumer:消息消费者。消费队列中存储的消息。
- 消息生产者连接到RabbitMQ Broker,创建connection,开启channel。
- 生产者声明交换机类型、名称、是否持久化等。
- 生产者发送消息,并指定消息是否持久化等属性和routing key。
- exchange收到消息之后,根据routing key路由到跟当前交换机绑定的相匹配的队列里面。
- 消费者监听接收到消息之后开始业务处理。
交换机的四种类型和用法?
有四种交换机:Direct Exchange、Fanout exchange、Topic exchange、Headers exchange。
Direct Exchange
直连交换机意思是此交换机需要绑定一个队列,要求该消息与一个特定的路由键完全匹配。简单点说就是一对一的,点对点的发送。
Fanout exchange
这种类型的交换机需要将队列绑定到交换机上。一个发送到交换机的消息都会被转发到与该交换机绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。简单点说就是发布订阅。
Topic exchange
直接翻译的话叫做主题交换机,如果从用法上面翻译可能叫通配符交换机会更加贴切。这种交换机是使用通配符去匹配,路由到对应的队列。通配符有两种:"*" 、 "#"。需要注意的是通配符前面必须要加上"."符号。
* 符号:有且只匹配一个词。比如 a.*可以匹配到"a.b"、"a.c",但是匹配不了"a.b.c"。
# 符号:匹配一个或多个词。比如"rabbit.#"既可以匹到"rabbit.a.b"、"rabbit.a",也可以匹配到"rabbit.a.b.c"。
Headers exchange
这种交换机用的相对没这么多。它跟上面三种有点区别,它的路由不是用routingKey进行路由匹配,而是在匹配请求头中所带的键值进行路由。如图所示:
创建队列需要设置绑定的头部信息,有两种模式:全部匹配和部分匹配。如上图所示,交换机会根据生产者发送过来的头部信息携带的键值去匹配队列绑定的键值,路由到对应的队列。
Rabbit如何保证消息的可靠性?
- 发送方
-
- 需要使用RabbitMQ发送端确认机制,确认消息成功发送到RabbitMQ并被处理
- 需要使用RabbitMQ消息返回机制,若没发现目标队列,中间件会通知发送方
- 消费方
-
- 需要使用RabbitMQ消费端确认机制,确认消息没有发生处理异常
- 需要使用RabbitMQ消费端限流机制,限制消息推送速度,保障接收端服务稳定
- RabbitMQ自身
-
- 大量堆积的消息会给RabbitMQ产生很大的压力,需要使用RabbitMQ消息过期时间,防止消息大量积压
- 过期后会直接被丢弃,无法对系统运行异常发出警报,需要使用RabbitMQ死信队列,收集过期消息,以供分析
Rabbit的高级特性?
1、发送端确认机制
- 单条同步确认 配置channel,开启确认模式: channel.confirmSelect() 每发送一条消息,调用channel.waitForConfirms()方法等待确认
- 多条同步确认 配置channel,开启确认模式:channel.confirmSelect() 发送多条消息后,调用channel.waitForConfirms()方法,等待确认
- 异步确认 配置channel,开启确认模式: channel.confirmSelect() 在channel上添加监听:addConfirmListener,发送消息后,会回调此方法,通知是否发送成功 异步确认有可能是单条,也有可能是多条,取决于MQ
2、消息返回机制
消息返回机制的原理
- 消息发送后,中间件会对消息进行路由
- 若没有发现目标队列,中间件会通知发送方
- Return Listener 会被调用
消息返回的开启方法
- 在RabbitMQ基础配置中有一个关键配置项:Mandatory
- Mandatory若为false,RabbitMQ将直接丢弃无法路由的消息
- Mandatory若为true,RabbitMQ才会处理无法路由的消息
3、消费端确认机制
消费端ACK类型
自动ACK:消费端收到消息后,会自动签收消息 手动ACK:消费端收到消息后,不会自动签收消息,需要我们在业务代码中显式签收消息
手动ACK类型
单条手动ACK: multiple=false 多条手动ACK: multiple=true (推荐使用单条ACK)
重回队列
若设置了重回队列,消息被NACK之后,会返回队列末尾,等待进一步被处理 一般不建议开启重回队列,因为第一次处理异常的消息,再次处理,基本上也是异常
实现步骤:
- 设置不自动签收
- 声明手动签收/拒收(拒收重回队列不推荐,会造成死循环)
- 设置单挑或者多条签收
4、消费端限流机制
消费端限流原因
业务高峰期,有个微服务崩溃了,崩溃期间队列挤压了大量消息,微服务上线后,收到大量并发消息。 将同样多的消息推给能力不同的副本,会导致部分副本异常。
RabbitMQ - QoS
- 针对以上问题,RabbitMQ 开发了QoS (服务质量保证)功能
- QoS功能保证了在一定数目的消息未被确认前,不消费新的消息
- QoS功能的前提是不使用自动确认
QoS原理
- QoS原理是当消费端有一定数量的消息未被ACK确认时,RabbitMQ不给消费端推送新的消息
- RabbitMQ使用QoS机制实现了消费端限流
消费端限流机制参数设置
- prefetchCount:针对一个消费端最多推送多少未确认消息
- global: true:针对整个消费端限流false:针对当前channel
- prefetchSize : 0 (单个消息大小限制, 一般为0)
- prefetchSize 与global两项,RabbitMQ暂时未实现
未开启qos限流前(消息全部推送,造成消费端消息挤压,无法一次性接收,并且全处于unacked状态,其他消费端也无法抢占资源)
开启qos限流后(消息全部推送,无法一次性接收,并且全处于ready状态,其他消费端可以抢占资源形成'负载均衡'的效果)
5、消息过期时间
RabbitMQ的过期时间(TTL)
- RabbitMQ的过期时间称为TTL (Time to Live),生存时间
- RabbitMQ的过期时间分为消息TTL和队列TTL
- 消息TTL设置了单条消息的过期时间
- 队列TTL设置了队列中所有消息的过期时间
如何找到适合自己的TTL?
- TTL的设置主要考虑技术架构与业务
- TTL应该明显长于服务的平均重启时间
- 建议TTL长于业务高峰期时间
6、死信队列
什么是死信队列
- 死信队列:队列被配置了DLX属性(Dead-Letter- Exchange)
- 当一个消息变成死信(dead message)后,能重新被发布到另一个Exchange,这个Exchange也是一个普通交换机
- 死信被死信交换机路由后,一般进入一个固定队列
怎样变成死信队列
- 消息被拒绝(reject/nack) 并且requeue=false
- 消息过期(TTL到期)
- 队列达到最大长度
死信队列设置方法
- 设置转发、接收死信的交换机和队列: ◆Exchange: dlx.exchange ◆Queue: dlx.queue ◆RoutingKey: #
- 在需要设置死信的队列加入参数: ◆x-dead-letter-exchange = dlx.exchange
七、面试常问的问题
如何保证消息不丢失:
- 生产者:使用confirm模式,确认消息被成功投递给消息队列,要求再高一些还有事务模式,但是性能差;
- 消息队列:将消息队列设置为持久化,将queue的持久化标识durable设置为true,则代表是一个持久的队列,发送消息的时候将deliveryMode=2;
- 消费者:使用手动ACK应答模式,也就是说没有得到应答,消息就会一直消费。
如何保证消息不积压:
- 流量控制:Rabbit提供了消费端的限流策略,channel.basicQos(prefetchCount),设置消费端合理的消费信息数;
- 合理设置队列信息:设置消息过期时间和队列长度;
如何保证消息幂等:
唯一标识;消息去重表;使用事务;版本号。
唯一标识:
- 在生产端给每个消息生产一个唯一id(根据雪花算法);
- 在消费端获取唯一的标识,在消费端获取到这个id,然后去redis的set集合判断是否重复,根据这个判断进行幂等控制。如果消息没有被消费,走正常的逻辑处理,然后将id存在redis的set里面。如果说发现已经有了,自然就是说明,已经被消费过了,直接返回成功。如果出现异常,就报错,打印日志(根据rabbitmq方法中的nack操作,将消息重新放回队列)。
版本号:
- 给每个消息加一个版本号,每次处理消息
如何保证消息按顺序发送:
将所有需要按顺序处理的消息发送到同一个队列对应一个消费者;根据不同的业务拆分成多个Queue,每个Queue对应一个消费者就好,自然每个消费者里面的消息,都是按照消息处理。对于mysql的binlog,可以根据mysql的表不同来设计队列和消费者。