前言
本文主要参考了Understanding AMQP, the protocol used by RabbitMQ 要学习rabbitmq,就要先学习amqp协议,amqp全称Advanced Message Queuing Protocol (AMQP),rabbitmq实现了amqp,当然rocketmq也实现了amqp。amqp中有几个重要的概念,producer(publisher)、exchange、queue、consumer
消息从发送到接收的流程
producer发送message到指定的exchange,exchange根据message header中的routing-key与队列的binding-key决定绑定到哪个队列;那么consumer怎么获取消息呢?queue push或者consumer pull,这取决于配置。
消息的结构
一个消息由header、properties、data3部分组成,header是amq规范规定的类似http规范中的header,比如后边会讲到的routing-key,properties是业务自定义的,data是二进制数据;
通常我们把message分配到queue的过程叫binding,那消息结构跟binding有什么关系呢?exchange拿着message header中的routing-key与queue中的routing-key(每个queue都有一个routing-key)做匹配。一般的consumer创建queue时,会同时创建exchange,并将queue关联到exchange,producer发送到指定exchange,从而实现消息流转;
exchange类型
exchange常见的有以下几种类型,
- direct
- binding key与routing key完全匹配,
*
、#
会当作普通文本处理;
- topic
- binding key的
*
、#
当作通配符处理,*
只能匹配一个word,#
匹配零个或多个点号分隔的word;
- fanout,n. 输出(端数);展(散)开;扇出; 分列账户;
- 收到的全部消息转发给所有队列;
比如现有一个message,它的routing key是NYSE.TECH.MSFT,exchange是topic类型,那么匹配queue的binding key结果
以上包含了绝大多数情况。实际上,一个queue可以有多个consumer,一个exchange可以有多个queue,一个queue可以有多个exchange。
应用场景
RPC(远程过程调用)
这个过程是这样的
- 客户端发送一个message到queue,当然了message的routing key要匹配service。
- exchange将message传给service,service做了一些操作并返回响应message给exchange,响应message指定了routing_key以匹配响应queue。
- 客户端从响应queue中获取到message
至于这个过程是blocking还是non-blockding,就取决于client library,无论是哪种都可以做到。AmqpTemplate#sendAndReceive就是对应的这种模式
发布/订阅
一个消息会被多个消费者消费,在activemq中叫topic。
任务分发
一个message只会被多个consumer中的一个消费,在activemq中叫p2p(点对点)。
最后来一张自己总结的图
jms vs amqp
参考消息队列(为什么要用消息队列,常见消息队列对比,JMS和AMQP谁更好用?),简单来说JMS是java的规范,跟语言是相关的,而amqp是协议,与语言无关,jms提供了点对点、发布/订阅两种模式,amq也提供了这两种模式,并在此基础上,对路由提供了多种选择。
springboot集成rabbitmq
springboot可以很方便的集成rabbitmq,引入spring-boot-starter-amqp
,然后在配置文件中添加配置
spring:
rabbitmq:
host: "localhost"
port: 5672
username: "admin"
password: "secret"
发送消息
@Component
public class MyBean {
private final AmqpAdmin amqpAdmin;
private final AmqpTemplate amqpTemplate;
public MyBean(AmqpAdmin amqpAdmin, AmqpTemplate amqpTemplate) {
this.amqpAdmin = amqpAdmin;
this.amqpTemplate = amqpTemplate;
}
// ...
}
默认的template禁用了重试,可以启用重试,注意这是发送(AmqpTemplate )重试;
rabbitmq:
template:
retry:
enabled: true
initial-interval: "2s"
接收消息
@Component
public class MyBean {
@RabbitListener(queues = "someQueue")
public void processMessage(String content) {
// ...
}
}
注意,默认的listener重试被禁用,如果listener抛出异常,那么会无限重试;有2种方法解决,第一种
设置defaultRequeueRejected 属性为false,这样的话,就不会重试或者抛出一个AmqpRejectAndDontRequeueException 来通知消息不用再重新入queue。第2种方法是打开重试并设置最大重试次数。比如设置最大重试次数,这是监听器(Listener)的重试
rabbitmq:
listener:
simple:
acknowledge-mode: auto # 超过重试次数后,自动确认,消息会从队列中删除
retry:
enabled: true
max-attempts: 3
max-interval: 6000
initial-interval: 2000
default-requeue-rejected: true # 如果分发失败,是否仍然进入队列,设置为true
发送可以重试,Listener也可以重试,注意区分;
消息确认、消息丢失
publisher丢了消息
因为网络的不确定性,可能在publisher发送到broker时,丢了消息,通常的解决方法是publisher发送前写入数据库,如果真的mq server丢失了,再手动补偿;还可能broker在处理message时出错了,导致了消息丢失;
mq server丢了消息
如果mq server重启了,那么队列及消息都丢失了,可以配置队列及消息是持久化的,这样重启也不会丢失队列和消息。但这不是绝对的不会丢失,因为mq server收到message会暂时放入cache在合适的时机再写入磁盘,如果真的需要绝对可靠可以考虑publisher confirm;
consumer丢了消息
默认的,broker发送message给了consumer后,就会立刻删掉message。如果处理这个message要花费一段时间,还未处理完时,consumer终止了,那么这个message就丢失了,还有其他已经收到还未来得及处理的message;怎么解决这种问题呢?consumer 确认机制。处理完message后,发送ack,broker收到ack再删除message,如果一直没收到ack(默认是30分钟),则会放入messages_unacknowledged 队列。