RabbitMQ 是一个由 Erlang 语言开发的 AMQP 的开源实现。
AMQP :Advanced Message Queue,高级消息队列协议。它是应用层协议的一个开放标准,为面向消息的中间件设计,基于此协议的客户端与消息中间件可传递消息,并不受产品、开发语言等条件的限制。
1.RabbitMQ 最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。具体特点包括:
2.可靠性(Reliability) RabbitMQ 使用一些机制来保证可靠性,如持久化、传输确认、发布确认。
3.灵活的路由(Flexible Routing) 在消息进入队列之前,通过 Exchange 来路由消息的。对于典型的路由功能,RabbitMQ 已经提供了一些内置的 Exchange 来实现。针对更复杂的路由功能,可以将多个 Exchange 绑定在一起,也通过插件机制实现自己的 Exchange 。
4.消息集群(Clustering) 多个 RabbitMQ 服务器可以组成一个集群,形成一个逻辑 Broker 。
5.高可用(Highly Available Queues) 队列可以在集群中的机器上进行镜像,使得在部分节点出问题的情况下队列仍然可用。
6.多种协议(Multi-protocol) RabbitMQ 支持多种消息队列协议,比如 STOMP、MQTT 等等。
7.多语言客户端(Many Clients) RabbitMQ 几乎支持所有常用语言,比如 Java、.NET、Ruby 等等。
8.管理界面(Management UI) RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息 Broker 的许多方面。
9.跟踪机制(Tracing) 如果消息异常,RabbitMQ 提供了消息跟踪机制,使用者可以找出发生了什么。
10.插件机制(Plugin System) RabbitMQ 提供了许多插件,来从多方面进行扩展,也可以编写自己的插件。
RabbitMQ的几种模式
基本消息模型
在上图的模型中,有以下概念:
P:生产者,也就是要发送消息的程序
C:消费者:消息的接受者,会一直等待消息到来。
queue:消息队列,图中红色部分。可以缓存消息;生产者向其中投递消息,消费者从其中取出消息。
工作消息模型
work queues与入门程序相比,多了一个消费端,两个消费端共同消费同一个队列中的消息,但是一个消息只能被一个消费者获取。
这个消息模型在Web应用程序中特别有用,可以处理短的HTTP请求窗口中无法处理复杂的任务。
接下来我们来模拟这个流程:
P:生产者:任务的发布者
C1:消费者1:领取任务并且完成任务,假设完成速度较慢(模拟耗时)
C2:消费者2:领取任务并且完成任务,假设完成速度较快
C1 与 C2 会平均的消费消息
Publish/subscribe(交换机类型:Fanout,也称为广播 )
和前面两种模式不同:
1) 声明Exchange,不再声明Queue
2) 发送消息到Exchange,不再发送到Queue
1、publish/subscribe与work queues有什么区别。
区别:
1)work queues不用定义交换机,而publish/subscribe需要定义交换机。
2)publish/subscribe的生产方是面向交换机发送消息,work queues的生产方是面向队列发送消息(底层使用默认交换机)。
3)publish/subscribe需要设置队列和交换机的绑定,work queues不需要设置,实际上work queues会将队列绑定到默认的交换机 。
相同点:
所以两者实现的发布/订阅的效果是一样的,多个消费端监听同一个队列不会重复消费消息。
2、实际工作用建议还是建议使用 publish/subscribe,发布订阅模式比工作队列模式更强大(也可以做到同一队列竞争),并且发布订阅模式可以指定自己专用的交换机。
Routing 路由模型(交换机类型:direct)
P:生产者,向Exchange发送消息,发送消息时,会指定一个routing key。
X:Exchange(交换机),接收生产者的消息,然后把消息递交给 与routing key完全匹配的队列
C1:消费者,其所在队列指定了需要routing key 为 error 的消息
C2:消费者,其所在队列指定了需要routing key 为 info、error、warning 的消息
Topics 通配符模式(交换机类型:topics)
每个消费者监听自己的队列,并且设置带统配符的routingkey,生产者将消息发给broker,由交换机根据routingkey来转发消息到指定的队列。
Routingkey一般都是有一个或者多个单词组成,多个单词之间以“.”分割,例如:inform.sms
通配符规则:
#:匹配一个或多个词
*:匹配不多不少恰好1个词
举例:
audit.#:能够匹配audit.irs.corporate 或者 audit.irs
audit.*:只能匹配audit.irs
RabbitMQ管理
启动
#1.服务启动相关
#启动
systemctl start rabbitmq-server
#重启
systemctl restart rabbitmq-server
#停止
systemctl stop rabbitmq-server
#查看状态
systemctl status rabbitmq-server
#2.管理命令行 用来不使用web管理界面情况下的命令操作RabbitMQ
rabbitmqctl help 可以查看更多命令
#3.插件管理命令行
rabbitmqplugins enable|list|disable
RabbitMQ传递对象
RabbitMQ是消息队列,发送和接收的都是字符串/字节数组类型的消息
消息队列可以发送字符串、字节数组、序列化对象
传递对象只需要序列化对象 或转化json字符串 即可
Boot 交换机与队列创建
创建一个配置类,用@Configuration标注
声明一个队列
/**
* dureable:是否持久化,默认是false, 持久化队列: 会被存储在磁盘上,当消息代理重启时仍然存在
* exclusive: 默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列会被关闭
* autoDelete:是否自动删除,当没有生产者或者消费者使用队列,该队列会自动删除
*/
public Queue newQueue(){
return new Queue("队列名",dureable,exclusive,autoDelete)
}
声明一个订阅交换机
@Bean
public FanouExchange newFanouExchange(){
return new FanouExchange("交换机名")
}
声明路由模式交换机
@Bean
public DirectExchange newDirectExchange(){
return new DirectExchange("交换机名")
}
绑定队列 需要队列和交换机 (如果需要绑定多个队列,只需要在重复操作即可)
@Bean
public Binding bindingDirectExchange(Queue 上方定义好的队列方法名,DirectExchange 上方定义好的交换机方法名){
return BindingBuilder.bind(Queue).to(DirectExchange).with("路由模式的key");
}
消息的可靠投递
RabbitMQ事务(不使用)
当在消息发送过程中添加了事务,处理效率降低几十倍甚至上百倍
channel.txSelect() 开启事务
channel.txCommit() 提交事务
channel.txRollback() 事务回滚
消息确认和return机制
消息确认机制:确认消息提供者是否成功发送消息到交换机
return机制:确认消息是否成功的从交换机分发到队列
在spring boot中配置
1,yml添加开启机制
publisher-confirm-type: simple ##开启消息确认机制
publisher-returns: true #使用return监听
2,创建监听类
@Component
public class MsgConfirmAndReturn implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback {
Logger logger = LoggerFactory.getLogger(MsgConfirmAndReturn.class);
@Resource
public RabbitTemplate rabbitTemplate;
//设置当前类给rabbitTemplate
@PostConstruct
public void init() {
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnsCallback(this);
}
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
//消息确认
if (b) {
logger.info("-----消息成功发送到交换机-----");
} else {
logger.warn("-----消息发送到交换机失败-----");
}
}
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
//return监听(当交换机分发消息到队列时执行)
logger.warn("-----交换机分发消息失败-----");
}
}
消息消费手动确认
1,yml开启手动确认机制
rabbitmq:
listener:
##开启手动确认机制
acknowledge-mode: manual
开启后如果消息手动确认后如果消息未被确认,该消息将一直停留在队列
//消息进行手动确认
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
如何保证消息可靠性
1、消息丢失 消息发送出去,由于网络问题没有抵达服务器
-
做好容错方法(try-catch),发送消息可能会网络失败,失败后要有重试机制,可记录到数据库,采用定期扫描 重发的方式
-
做好日志记录,每个消息状态是否都被服务器收到都应该记录
-
做好定期重发,如果消息没有发送成功,定期去数据库扫描未成功的消息进行重发
消息抵达Broker,Broker要将消息写入磁盘(持久化)才算成功。此时Broker尚未持久化完成,宕机。
- publisher也必须加入确认回调机制,确认成功的消息,修改数据库消息状态。
自动ACK的状态下。消费者收到消息,但没来得及消息然后宕机
- 一定开启手动ACK,消费成功才移除,失败或者没来得及处理就noAck并重新入队
2、消息重复 消息消费成功,事务已经提交,ack时,机器宕机。导致没有ack成功,Broker的消息重新由unack变为ready,并发送给其他消费者 消息消费失败,由于重试机制,自动又将消息发送出去
成功消费,ack时宕机,消息由unack变为ready,Broker又重新发送
-
消费者的业务消费接口应该设计为幂等性的。比如扣库存有工作单的状态标志
-
使用防重表(redis/mysql),发送消息每一个都有业务的唯一标识,处理过就不用处理
-
rabbitMQ的每一个消息都有redelivered字段,可以获取是否是被重新投递过来的,而不是第一次投递过来的
3、消息积压
消费者宕机积压
消费者消费能力不足积压
发送者发送流量太大
- 上线更多的消费者,进行正常消费
- 上线专门的队列消费服务,将消息先批量取出来,记录数据库,离线慢慢处理
rabbitmq basicReject / basicNack / basicRecover区别
channel.basicReject(deliveryTag, true);
basicReject方法拒绝deliveryTag对应的消息,第二个参数是否requeue,true则重新入队列,否则丢弃或者进入死信队列。该方法reject后,该消费者还是会消费到该条被reject的消息。
channel.basicNack(deliveryTag, false, true);
basic.nack方法为不确认deliveryTag对应的消息,第二个参数是否应用于多消息,第三个参数是否requeue,与basic.reject区别就是同时支持多个消息,可以nack该消费者先前接收未ack的所有消息。nack后的消息也会被自己消费到。
channel.basicRecover(true);
basic.recover是否恢复消息到队列,参数是是否requeue,true则重新入队列,并且尽可能的将之前recover的消息投递给其他消费者消费,而不是自己再次消费。false则消息会重新被投递给自己。
消息消费幂等问题
RabbitMQ消息自动重试机制 1.当我们消费者处理执行我们业务代码的时候,如果抛出异常的情况下 在这时候mq_会自动触发重试机制,默认的情况下rabbitmq是无限次数的重试。需要人为指定重试次数限制问题
##配置重试机制
rabbitmq:
listener:
simple:
retry:
##开启消费者(程序出现问题)进行重试
enabled: true
##重试次数 (重试完后将丢掉该未被成功消费的消息)
max-attempts: 5
##重试间隔时间
initial-interval: 3000
2.在什么情况下消费者需要实现重试策略?
A. 消费者获取消息后,调用第三方接口,但是调用第三方接口失败呢?是否需要重试? 该情况下需要实现重试策略,网络延迟只是暂时调用不通,重试多次有可能会调用通。
B. 消费者获取消息后,因为代码问题抛出数据异常,是否需要重试?
该情况下是不需要实现重试策略,就算重试多次,最终还是失败的。可以将日志存放起来,后期通过定时任务或者人工补偿形式。如果是重试多次还是失败消息,需要重新发布消费者版本实现消费可以使用死信队列
Mq在重试的过程中,有可能会引发消费者重复消费的问题。
Mq消费者需要解决幂等性问题 幂等性保证数据唯一
死信队列
当一条消息在队列中出现以下三种情况的时候,该消息就会变成一条死信。
- 消息被拒绝(basic.reject / basic.nack),并且requeue = false
- 消息TTL过期
- 队列达到最大长度
当消息在一个队列中变成一个死信之后,如果配置了死信队列,它将被重新publish到死信交换机,死信交换机将死信投递到一个队列上,这个队列就是死信队列。
创建队列的一些属性
/**
* 创建死信队列 DLX
* @return
*/
@Bean
public Queue orderDelayQueue(){
HashMap<String, Object> map = new HashMap<String, Object>();
map.put("x-message-ttl",10000);
map.put("x-dead-letter-exchange",ORDER_DELAY_EXCHANGE);
map.put("x-dead-letter-routing-key","k2");
return new Queue(ORDER_DELAY_QUEUE,true,false,false,map);
}
队列的一些属性参数
(1)x-message-ttl:消息的过期时间,单位:毫秒;
(2)x-expires:队列过期时间,队列在多长时间未被访问将被删除,单位:毫秒;
(3)x-max-length:队列最大长度,超过该最大值,则将从队列头部开始删除消息;
(4)x-max-length-bytes:队列消息内容占用最大空间,受限于内存大小,超过该阈值则从队列头部开始删除消息;
(5)x-overflow:设置队列溢出行为。这决定了当达到队列的最大长度时消息会发生什么。有效值是drop-head、reject-publish或reject-publish-dlx。仲裁队列类型仅支持drop-head;
(6)x-dead-letter-exchange:死信交换器名称,过期或被删除(因队列长度超长或因空间超出阈值)的消息可指定发送到该交换器中;
(7)x-dead-letter-routing-key:死信消息路由键,在消息发送到死信交换器时会使用该路由键,如果不设置,则使用消息的原来的路由键值
(8)x-single-active-consumer:表示队列是否是单一活动消费者,true时,注册的消费组内只有一个消费者消费消息,其他被忽略,false时消息循环分发给所有消费者(默认false)
(9)x-max-priority:队列要支持的最大优先级数;如果未设置,队列将不支持消息优先级;
(10)x-queue-mode(Lazy mode):将队列设置为延迟模式,在磁盘上保留尽可能多的消息,以减少RAM的使用;如果未设置,队列将保留内存缓存以尽可能快地传递消息;
(11)x-queue-master-locator:在集群模式下设置镜像队列的主节点信息。
延迟队列
AMQP协议和RabbitMQ队列本身是不支持延迟队列功能的,但是可以通过TTL(Time To Live) 特性模拟延迟队列的功能
TTL就是消息的存活时间。RabbitMQ可以分别对队列和消息设置存活时间
在创建队列的时候可以设置队列的存活时间,当消息进入到队列并且在存活时间内没有消费者消费,则此消息就会从当前队列被移除;
创建消息队列没有设置TTL,但是消息设置了TTL,那么当消息的存活时间结束,也会被移除;
实现延迟队列
1,创建路由交换机
2,创建两个队列,一个普通队列 设置k1,一个死信队列 设置k2
-
x-dead-letter-exchange:超时后转发的交换机 -
x-dead-letter-routing-key :超时后转发的交换机的key -
x-message-ttl:超时时间
3,进行绑定
4,发送消息到 死信队列 k2,而接收消息 k1
springboot整合rabbitmq
启动rabbitmq
#启动
systemctl start rabbitmq-server
#重启
systemctl restart rabbitmq-server
#停止
systemctl stop rabbitmq-server
#查看状态
systemctl status rabbitmq-server
启动成功后 ip地址+15672访问
创建队列必须要有消费者,如果没有消费者的话那队列就不会发送消息
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
yml
spring:
rabbitmq:
host: 192.168.141.124 #主机IP
port: 5672 #端口
username: root #用户名
password: root #密码
消息发送
##注入模板
@Autowired
private RabbitMQTemplate RabbitMQTemplate;
##直接发送消息到队列
amqpTemplate.convertAndSend("队列名",消息)
##发送消息到交换机(订阅交换机)
amqpTemplate.convertAndSend("交换机名" ,"" ,消息)
##发送消息到交换机(路由交换机)
amqpTemplate.convertAndSend("交换机名" ,路由key ,消息)
//接收消息
@Component
public class HelloCustomer {
@RabbitListener(queuesToDeclare = @Queue(value = "队列名"))
public void a(String msg){
System.out.println("msg = " + msg);
}
}