消息队列使用场景
在分布式系统下,消息队列的作用主要体现在:异步、削峰、解耦
- 跨系统的异步通信,所有需要异步交互的地方都可以使用消息队列。比如下订单需要库存、优惠券、积分、支付、短信等系统进行异步通信(当然你可以说,你的下订单功能全部在一个系统里走完,根本用不着消息队列,我们这里说的是分布式微服务系统)。
- 多个应用之间的解耦,由于消息是平台和语言无关的,而且语义上也不再是函数调用,因此更适合作为多个应用之间的松耦合的接口。基于消息队列的耦合,不需要发送方和接收方同时在线。
- 应用内的同步变异步,比如订单处理,就可以由前端应用将订单信息放到队列,后端应用从队列里依次获得消息处理,高峰时的大量订单可以积压在队列里慢慢处理掉。由于同步通常意味着阻塞,而大量线程的阻塞会降低计算机的性能。
- 消息驱动的架构,系统分解为消息队列,消息制造者和消息消费者,一个处理流程可以根据需要拆成多个阶段,阶段之间用队列连接起来,前一个阶段处理的结果放入队列,后一个阶段从队列中获取消息继续处理。
- 应用需要更灵活的耦合方式,如发布订阅。
主流消息队列对比
目前在市面上比较主流的消息队列中间件主要有:ActiveMQ、RabbitMQ、RocketMQ、Kafka 等这几种。
不过ActiveMQ和RabbitMQ这两着因为吞吐量还有GitHub的社区活跃度的原因,在各大互联网公司都已经基本上绝迹了,业务体量一般的公司会是有在用的,但是越来越多的公司更青睐RocketMQ这样的消息中间件了。
这里用网上找的对比图让大家看看具体区别:

区别也比较明显,拿吞吐量来说,后两者已经超越前两者一个数量级,在现在这样大数据的年代吞吐量是很重要的指标,由于ActiveMQ社区活跃度以及更新频繁度越来越低,在技术选型中我们基本可以优先排除它了。
RabbitMQ时效性是她的一大特点,对于小中型应用是很好的选择,不过其开发语言是erlang,我相信绝大部分工程师对erlang不熟,而且肯定不会为了一个中间件去刻意学习一门语言,因此维护成本较高,出了问题排查难度大。
至于RocketMQ(阿里开源的,和Dubbo一样成为了Apache顶级项目),git活跃度高,源码是Java,基本上push了自己的bug确认了有问题,阿里大佬都会跟你解答并修复。而且RocketMQ功能也是最全的,所以我是比较推荐使用这个的。
压轴大哥Kafka,大数据领域,日志采集,实时计算等场景,都离不开他的身影,他基本算得上是世界范围级别的消息队列标杆了。
以上这些都只是我个人意见,真正的选项还是要去深入研究,记住,只有最适合的技术,不要为了用而用。
RabbitMQ
上面简单介绍了消息队列的基础知识,本文重点还是RabbitMQ
RabbitMQ是一个由erlang开发的AMQP(Advanved Message Queue)的开源实现;在RabbitMQ官网上主要有这样的模块信息, Work queues消息队列,Publish/Subscribe发布订阅服务,Routing, Topics, RPC等主要应用的模块功能。
下面我会从工作原理、消息可靠性、不重复消费以及如何顺序消费来讲述。
核心概念说明
- Broker:它提供一种传输服务,它的作用就是维护一条从生产者到消费者的路线,保证数据能按照指定的方式进行传输,简单来说就是消息队列服务器实体
- Exchange(交换器):用于接受、分配消息,它指定消息按什么规则,路由到哪个队列
- Queue(队列):用于存储生产者的消息,每个消息都会被投到一个或多个队列
- RoutingKey(路由键):Exchange根据这个关键字进行消息投递
- Binding(绑定):它的作用就是把Exchange和Queue按照路由规则绑定起来
- vhost(虚拟主机):一个Broker里可以有多个vhost,用作不同用户的权限分离
- Producer(消息生产者):负责创建和推送数据到消息服务器的程序
- Consumer(消息消费者):就是接收消息的程序
- Channel(消息通道/信道):在客户端的每个连接里,可建立多个channel,每个channel代表一个会话任务
RabbitMQ原理:

流程思路
producer向consumer发送消息:
- producer客户端获取Connection,接着获取channel;
- producer客户端声明一个exchange,并设置相关属性;
- producer客户端声明一个queue,并设置相关属性
- producer客户端使用routing key,在exchange和queue之间建立好绑定关系(即queue binding到一个exchange);
- producer客户端通过指定一个Exchange和一个RoutingKey来将消息发送到对应的Queue上;
- consumer客户端在接收时也是获取connection,打开一个channel,然后指定一个Queue直接到它关心的Queue上取消息,它对Exchange,RoutingKey及如何binding都不关心,到对应的Queue上去取消息就OK了。
通信过程
假设P1和C1注册了相同的Broker,Exchange和Queue。P1发送的消息最终会被C1消费。基本的通信流程大概如下所示:
- P1生产消息,发送给服务器端的Exchange;
- Exchange收到消息,根据ROUTINKEY,将消息转发给匹配的Queue1;
- Queue1收到消息,将消息发送给订阅者C1;
- C1收到消息,发送ACK给队列确认收到消息;
- Queue1收到ACK,删除队列中缓存的此条消息。
注意要点
Consumer收到消息时需要显式的向RabbitMQ broker发送basic.ack消息或者consumer订阅消息时设置auto_ack参数为true。在通信过程中,队列对ACK的处理有以下几种情况:
- 如果consumer接收了消息,发送ack,rabbitmq会删除队列中这个消息,发送另一条消息给consumer。
- 如果consumer接受了消息,但在发送ack之前断开连接,rabbitmq会认为这条消息没有被投递,在consumer在次连接的时候,这条消息会被重复投递。
- 如果consumer接受了消息,但是程序中有bug,忘记了ack,rabbitmq不会重复发送消息。
Connection 与 Cchannel
- connection是指物理的连接,一个client与一个server之间建立的tcp连接;
- 一个连接上可以建立多个channel,可以理解为逻辑上的连接。一般应用的情况下,有一个channel就够用了,不需要创建更多的channel。
Exchange 与 RoutingKey
Exchange类似于数据通信网络中的交换机,提供消息路由策略。
RabbitMQ中,producer不是通过信道直接将消息发送给queue,而是先发送给Exchange。一个Exchange可以和多个Queue进行绑定,producer在传递消息的时候,会传递一个routingKey,Exchange会根据这个routingKey按照特定的路由算法,将消息路由给指定的queue。和Queue一样,Exchange也可设置为持久化(默认值,设置durable为true表示持久化,false表示临时),临时或者自动删除(设置autoDelete,默认为false)。
Exchange有4种类型:direct(默认),topic,fanout,headers
- Direct 直接交换器,工作方式类似于单播,Exchange会将消息发送完全匹配routingKey的Queue;
- topic 主题交换器,工作方式类似于组播,Exchange会将消息转发和routingKey匹配模式相同的所有队列,比如,routingKey为user.order的Message会转发给绑定匹配模式为 user.order, *.order, user.*, * . * 和#.user.order.#的队列( * 表是匹配一个任意词组,#表示匹配0个或多个词组);
- fanout 广播交换器,不管消息的routingKey设置为什么,Exchange都会将消息转发给所有绑定的Queue;
- headers类型的交换器不依赖于路由键的匹配规则来路由消息,而是根据发送消息内容中的headers属性进行匹配。headers类型的交换器性能差,不实用,基本上不会使用。

Queue
消息队列,提供了FIFO的处理机制,具有缓存消息的能力。rabbitmq中,队列消息可以设置为持久化(默认),临时或者自动删除
- 设置为持久化的队列,queue中的消息会在server本地硬盘存储一份,防止系统crash,数据丢失
- 设置为临时队列,queue中的数据在系统重启之后就会丢失
- 设置为自动删除的队列,当不存在用户连接到server,队列中的数据会被自动删除
Binding
所谓绑定就是将一个特定的Exchange和一个特定的 Queue 绑定起来,Exchange和Queue的绑定可以是多对多的关系。
代码实现
流程概念都说的差不多了,接下来我们看看代码具体实现
public class RabbitMQSender {
private final static String EXCHANGE_NAME = "durable-exchange";
private final static String QUEUE_NAME = "durable-queue";
public static void main(String[] args) throws IOException, TimeoutException {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
// 设置 RabbitMQ 的主机名、端口、用户名和密码
factory.setHost("locahost");
factory.setPort(5672);
factory.setUsername("guest");
factory.setPassword("guest");
// 创建一个连接
Connection connection = factory.newConnection();
// 创建一个通道
Channel channel = connection.createChannel();
// 创建一个direct类型的Exchange,参数分别代表“交换器名称、类型、是否持久化(不传,重载方法,默认为false)”
channel.exchangeDeclare(EXCHANGE_NAME, "direct", true);
// 创建一个Queue,第二个参数true表示持久化
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
// 创建一个binding,第三个参数时routingKey,fanout类型时,指定了也无效
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "user.order");
// 发送消息
String message = "durable exchange test";
// 设置deliveryMode(2) 表示设置消息的投递模式为2,即代表持久化
AMQP.BasicProperties props = new AMQP.BasicProperties().builder().deliveryMode(2).build();
channel.basicPublish(EXCHANGE_NAME, "user.order", props, message.getBytes());
// 关闭频道和连接
channel.close();
connection.close();
}
}
上面这个发送端我特意把Exchange、Queue和Message都设置为持久化,持久化即保存到磁盘上,持久化后重启RabbitMQ,这些内容都会存在。
持久化注意点
- 理论上可以将所有的消息都设置为持久化,但是这样会严重影响RabbitMQ的性能。因为写入磁盘的速度比写入内存的速度慢得不止一点点。对于可靠性不是那么高的消息可以不采用持久化处理以提高整体的吞吐量。在选择是否要将消息持久化时,需要在可靠性和吞吐量之间做一个权衡。
- 将交换器、队列、消息都设置了持久化之后仍然不能百分之百保证数据不丢失,因为当持久化的消息正确存入RabbitMQ之后,还需要一段时间(虽然很短,但是不可忽视)才能存入磁盘之中。如果在这段时间内RabbitMQ服务节点发生了宕机、重启等异常情况,消息还没来得及落盘,那么这些消息将会丢失。
- 单单只设置队列持久化,重启之后消息会丢失;单单只设置消息的持久化,重启之后队列消失,继而消息也丢失。单单设置消息持久化而不设置队列的持久化显得毫无意义。
RabbitMQ和SpringBoot整合
- 添加RabbitMQ依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
- application.yml配置RabbitMQ服务器(前提需要启动RabbitMQ服务程序,官网下载)
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
virtual-host: /
listener:
# 容器类型
type: simple
simple:
# 限流(海量数据,同时只能过来一条)
prefetch: 1
# 自auto:自动ack manual:手动ack 默认自动
acknowledge-mode: manual
retry:
# 开启重试机制
enabled: true
# 重试次数
max-attempts: 5
# 最大间隔时间(单位毫秒)
max-interval: 20000
# 重试间隔时间(单位毫秒)
initial-interval: 3000
#乘子 重试间隔*乘子得出下次重试间隔 3s 6s 12s 24s 此处24s>20s 走20s
multiplier: 2
# 重试次数超过上面的设置之后是否丢弃(false不丢弃时需要写相应代码将该消息加入死信队列)
default-requeue-rejected: false
- 配置RabbitTemplate、Exchange、Queue、Binding
@Slf4j
@Configuration
public class RabbitMQConfig {
@Bean
public RabbitTemplate rabbitTemplate(CachingConnectionFactory connectionFactory) {
connectionFactory.setPublisherConfirms(true); // 设置消息发布需要确认,默认为false
connectionFactory.setPublisherReturns(true); // 设置消息发布异步回调,默认为false
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if (ack) {
log.info("消息发送成功:correlationData({})", correlationData);
} else {
log.info("消息发送失败:correlationData({}), cause({})", correlationData, cause);
// TODO 重试机制,在实际应用中,不会像下面这样定义这么多Exchange和Queue(这里是为了展示效果)
// 一般一个服务都只有一个Exchange和一个Queue,所以在重试时,你可以明确发送到Exchange和queue
// correlationData里面包含有消息具体内容
}
});
// 当mandatory标志位设置为true时,如果exchange根据自身类型和消息routeKey无法找到一个符合条件的queue,那么会调用setReturnCallback方法将消息返回给生产者,通知发送失败
rabbitTemplate.setMandatory(true);
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) ->
log.info("消息从Exchangel路由到Queue失败:exchange({}),route({}),replyCode({}),replyText({}),message:{}", exchange, routingKey, replyCode, replyText, message));
// 这里发送失败,表示exchange和routingkey找不到一个符合条件的queue,可能是发送消息的时候哪个名字写错了,这个代码bug直接改,正常情况下是不会走到这里的
return rabbitTemplate;
}
/**
* 直接模式交换器
*/
@Bean
public DirectExchange directExchange() {
return new DirectExchange("directExchangeName");
}
/**
* 主题模式交换器
*/
@Bean
public TopicExchange topicExchange() {
return new TopicExchange("topicExchangeName");
}
/**
* 广播模式队列
*/
@Bean
public FanoutExchange fanoutExchange() {
return new FanoutExchange("fanoutExchangeName");
}
/**
* 延迟队列交换器, x-delayed-type 和 x-delayed-message 固定
*/
@Bean
public CustomExchange delayExchange() {
Map<String, Object> args = Maps.newHashMap();
args.put("x-delayed-type", "direct");
return new CustomExchange("delayExchangeName", "x-delayed-message", true, false, args);
}
/**
* 直接模式队列1
*/
@Bean
public Queue directQueueOne() {
return new Queue("directQueueName");
}
/**
* 队列2
*/
@Bean
public Queue queueTwo() {
return new Queue("twoQueueName");
}
/**
* 主题队列3
*/
@Bean
public Queue topicQueueThree() {
return new Queue("topicQueueName");
}
/**
* 延迟队列
*/
@Bean
public Queue delayQueue() {
return new Queue("delayQueueName", true); // 第二个参数durable默认就是true,可以不带
}
/**
* 直接模式绑定队列1
*/
@Bean
public Binding directBinding(Queue directQueueOne, DirectExchange directExchange) {
// with()是设置routingKey
return BindingBuilder.bind(directQueueOne).to(directExchange).with("queue.direct");
}
/**
* 广播模式绑定队列2
*/
@Bean
public Binding fanoutBinding(Queue queueTwo, FanoutExchange fanoutExchange) {
// 广播模式不需要指定routingKey
return BindingBuilder.bind(queueTwo).to(fanoutExchange);
}
/**
* 主题模式绑定广播模式
* @param fanoutExchange 分列模式交换器
* @param topicExchange 主题模式交换器
*/
@Bean
public Binding topicBinding1(FanoutExchange fanoutExchange, TopicExchange topicExchange) {
// with()是设置routingKey,匹配"queue"或"queue.[多个单词]"
return BindingBuilder.bind(fanoutExchange).to(topicExchange).with("queue.#");
}
/**
* 主题模式绑定队列3
*/
@Bean
public Binding topicBinding2(Queue queueThree, TopicExchange topicExchange) {
// with()是设置routingKey,匹配"[任意一个单词].queue"
return BindingBuilder.bind(queueThree).to(topicExchange).with("*.queue");
}
/**
* 延迟队列绑定自定义交换器
*
* @param delayQueue 队列
* @param delayExchange 延迟交换器
*/
@Bean
public Binding delayBinding(Queue delayQueue, CustomExchange delayExchange) {
return BindingBuilder.bind(delayQueue).to(delayExchange).with("queue.delay").noargs();
}
}
上面我分别配置了direct、fanout、topic和custom(x-delayed-message)四种类型的交换器、队列以及绑定。需要特别说明CustomExchange(x-delayed-message)类型RabbitMQ默认是不支持的,需要安装延迟插件,打开官网下载

rabbitmq_delayed_message_exchange-3.8.0.ez
将这个文件拷贝到RabbitMQ的plugins目录下
输入命令rabbitmq-plugins enable rabbitmq_delayed_message_exchange
开启插件,此时我们可以到RabbitMQ管理页面查看Exchange,可以看到如下图添加一个Exchange时,可以选择的交换器类型多了一个:

需要注意的一点是,如果使用命令rabbitmq-plugins disable rabbitmq_delayed_message_exchange
禁用了延迟插件,那么所有未发送的延迟消息都将丢失。
消息发送者和接收者就很简单了,下面代码只演示topic和延迟队列收发
消息发送
@AllArgsConstructor
public class MessageStruct implements Serializable {
private static final long serialVersionUID = 1L;
private String message;
}
@Component
public class SenderDemo {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 主题模式发送1
* 发送的消息默认是持久化的
*/
public void sendTopic1() {
rabbitTemplate.convertAndSend("topicExchangeName", "queue.aaa.bbb", new MessageStruct("topic message"));
}
/**
* 延迟队列发送
*/
public void sendDelay() {
rabbitTemplate.convertAndSend(delayExchangeName, "queue.delay", new MessageStruct("delay message, delay 5s, " + DateUtil.date()), message -> {
message.getMessageProperties().setHeader("x-delay", 5000);
return message;
});
}
}
上面发送消息默认是持久化的,在某些场景下,我们对消息的完整性要求并没有那么严格,反而更在意MQ的性能,丢失一些数据也可以接受的;这个时候我们可能需要定制一下发送的消息属性(比如将消息设置为非持久化的)
MessageProperties properties = new MessageProperties();
properties.setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT);
Message message = rabbitTemplate.getMessageConverter().toMessage(new MessageStruct("NonDurable message"), properties);
rabbitTemplate.convertAndSend("topicExchangeName", "queue.aaa.bbb", message);
消息消费
/**
* 延迟消息消费者
*/
@Slf4j
@Component
@RabbitListener(queues = "queue.delay") // 指定接收queue.delay的队列
public class DelayedReceiver {
@RabbitHandler
public void handlerManualAck(MessageStruct messageStruct, Message message, Channel channel) {
// 如果acknowledge-mode=manual 表示需要手动ACK,消息会被监听消费,但是消息在队列中依旧存在
// 如果未配置 acknowledge-mode 默认是auto会在消费完毕后自动ACK掉
// 还有一种是none,表示不需要确认,RabbitMQ发送给消费者后,就会删除消息
final long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
log.info("手动ACK,接收消息:{}", JSONUtil.toJsonStr(messageStruct));
// 通知 MQ 消息已被成功消费,RabbitMQ Server收到ack后才会把Message删除
channel.basicAck(deliveryTag, false);
} catch (IOException e) {
try {
// 处理失败,重新压入MQ
channel.basicRecover();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
消息的可靠性
保证消息的可靠性就是避免消息的丢失,分为三个方面:
- 生产者生产消息到 RabbitMQ Server 可靠性保证
- RabbitMQ Server中存储的消息可靠性保证
- RabbitMQ Server到消费者消息可靠性保证
1、生产者生产消息到RabbitMQ Server可靠性保证
这个过程,消息可能会丢,比如发生网络丢包、网络故障等造成消息丢失,一般情况下如果不采取措施,生产者无法感知消息是否已经正确无误的发送到exchange中,如果生产者能感知到的话,它可以进行进一步的处理动作,比如重新投递相关消息以确保消息的可靠性。
保留可靠性方式:发送方确认机制(publisher confirm)
首先生产者通过调用channel.confirmSelect方法将信道设置为confirm模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,RabbitMQ就会发送一个确认(Basic.Ack)给生产者(包含消息的唯一deliveryTag和multiple参数),这就使得生产者知晓消息已经正确到达了目的地了。
Confirm模式有三种方式实现:
- 串行confirm模式:producer每发送一条消息后,调用waitForConfirms()方法,等待broker端confirm,如果服务器端返回false或者在超时时间内未返回,客户端进行消息重传;
- 批量confirm模式:producer每发送一批消息后,调用waitForConfirms()方法,等待broker端confirm;
- 异步confirm模式:提供一个回调方法,broker confirm了一条或者多条消息后producer端会回调这个方法。(我在上面的和springboot整合的代码就是用了这种方式保证可靠性)
2、RabbitMQ Server中存储的消息可靠性保证
RabbitMQ自己弄丢了数据,确保消息的可靠性,你必须开启RabbitMQ的持久化,就是消息写入之后会持久化到磁盘,哪怕是RabbitMQ自己挂了,恢复之后会自动读取之前存储的数据,一般数据不会丢。除非极其罕见的是,RabbitMQ还没持久化,自己就挂了,可能导致少量数据会丢失的,但是这个概率较小。
而且持久化可以跟生产者那边的confirm机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者ack了,所以哪怕是在持久化到磁盘之前,rabbitmq挂了,数据丢了,生产者收不到ack,你也是可以自己重发的。
还有设置持久化时,得同时设置Exchange、Queue以及Message都持久化,上面代码中已经说明的很清楚了,而且对message非持久化方式也给出来代码。
3、RabbitMQ Server到消费者消息可靠性保证
消费者丢失消息,主要是因为你消费的时候,刚消费到,还没处理,结果进程挂了或抛异常了,那么就尴尬了,RabbitMQ认为你都消费了,这数据就丢了。
同样的,我们上面也解决了这个问题,就是用RabbitMQ提供的ack机制,简单来说,就是设置acknowledge-mode=manual手动来ack,每次你自己代码里确保处理完之后执行 channel.basicAck(deliveryTag, false);
来ack一把。这样的话,如果你还没处理完,不就没有ack?那RabbitMQ就认为你还没处理完,这个时候RabbitMQ就会采取其他措施,比如重试发送,或者把这个消费分配给别的consumer去处理。
这里细讲内容很多,涉及到死信队列,当一个消息在队列中变成死信(dead message)之后,它能被重新发送到另一个交换器中,这个交换器就是DLX(Dead-Letter-Exchange,称之为死信交换器),绑定DLX的队列就称之为死信队列。 DLX也是一个正常的交换器,和一般的交换器没有区别,实际上就是设置某个队列的这两个属性x-dead-letter-exchange ; x-dead-letter-routing-key
消息变成死信一般是由于以下几种情况
- 消息被拒绝(Basic.Reject/Basic.Nack)且不重新投递(requeue=false)
- 消息过期
- 队列达到最大长度
上面说的消费者手动ack,也可以手动nack,上面的配置我设置重试机制,当超过最大重试次数(上面配置的5次)后,消息就会进入死信队列,此时可以用定时任务去定时查看死信队列中的消息,做一些日志记录,人工干预等补偿措施。
如何保证消息不重复消费
保证消息不重复消费,处理这个问题的方式一般叫做接口幂等,幂等就是重复多次操作,结果和执行一次是一样的。
消息队列保证不重复消费的解决办法一般通用方法就是给每条消息添加一个唯一id,每次处理完这个消息,就记录这个id,下一次来消息时我先检查一下是否存在此id,如果存在则打印异常日志并放回,不存在则处理消费这个消息。
如何保证消息顺序消费
RabbitMQ保证顺序消费就比较简单了,设置一个Queue对应一下Consumer(消费者),把需要保证顺序的message都发送到这个Queue当中,消费者开启手动ack确认,设置prefetch=1每次只消费一条消息,处理完一条消息后进行手工ack,然后接收下一条message,只能由一个Consumer进行处理。
注意:如果是多个Consumer消费,是不能保证顺序消费的,如下图所示
