为什么要用 MQ?
一、基本概念
MQ的应用场景
- 应用解耦。以电商为例,用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统出现故障或者因为升级等原因暂时不可用都会造成下单操作异常,影响用户使用体验。使用 MQ 后,比如物流系统发生故障,需要几分钟才能修复,在这段时间,物流系统要处理的数据被缓存到 MQ 中,用户的下单操作正常完成,当物流系统恢复后,补充处理存在 MQ 中的订单消息即可,终端系统感知不到物流系统发生过几分钟故障;
- 流量削峰。将大量请求缓存起来,分散到很长一段时间处理,这样可以大大提高系统的稳定性和用户体验;
- 通过异步处理提高系统性能(减少响应所需时间)。将用户的请求数据存储到消息队列之后就立即返回结果。随后,系统再对消息进行消费。
缺点
- 系统可用性降低。系统引入的外部依赖越多,系统稳定性越差,一旦 MQ 宕机,就会对业务造成影响;如何保证 MQ 的高可用?
- 系统复杂性提高。如何保证消息没有被重复消费?怎么处理消息丢失情况?怎么保证消息传递的顺序性?
- 一致性问题。A 系统处理完业务,通过 MQ 给 B、C、D 三个系统发消息数据,如果 B、C 系统处理成功,D 系统处理失败,就会造成一致性问题。如何保证消息数据处理的一致性?
各类 MQ 产品的比较
- | RabbitMQ | RocketMQ | Kafka |
---|---|---|---|
单机吞吐量 | 万级 | 10万级 | 10万级 |
时效性 | us级 | ms级 | ms级以内 |
可用性 | 高 (主从架构) | 非常高 (分布式架构) | 非常高 (分布式架构) |
功能特性 | 基于 erlang 开发,所以并发能力很强,性能极其好,延时很低,管理界面较丰富 | MQ 功能比较完备,扩展性佳 | 只支持主要的 MQ 功能,像一些消息查询、消息回溯等功能没有提供,毕竟是为大数据准备的,在大数据领域应用广 |
RocketMQ 各角色
角色 | 说明 | |
---|---|---|
Producer | 消息的生产者,类似 “发件人” | 与 NameServer 集群中的其中一个节点 (随机选择) 建立长连接,定期从 NameServer 取 Topic 路由信息,并向提供 Topic 服务的 Master 建立长连接,定时向 Master 发送心跳,Producer 完全无状态,可集群部署。 |
Consumer | 消息的消费者,类似 “收件人” | 与 NameServer 集群中的其中一个节点 (随机选择) 建立长连接,定期从 NameServer 取 Topic 路由信息,并向提供 Topic 服务的 Master、Slave 建立长连接,定时向 Master、Slave 发送心跳,Consumer 既可以从 Master 订阅消息,也可以从 Slave 订阅消息,订阅规则由 Broker 配置决定。 |
Broker | 暂存和传输消息,核心角色,类似 “邮局” | Master 主要处理写操作,Slave 主要处理读操作,Master 和 Slave 的对应关系通过指定相同的 BrokerName,不同的 BrokerId 来定义,BrokerId 为 0 表示 Master,非 0 表示 Slave。每个 Broker 与 NameServer 集群中的所有节点建立长连接,定时注册 Topic 信息到所有 NameServer。 |
NameServer | 管理 Broker,核心角色,类似 “邮局的管理机构” | NameServer 是无状态节点,节点之间无任何信息同步,因为 Broker 启动后,会给每一个 NameServer 节点上报信息。 |
Topic | 区分消息的种类 | 一个生产者可以发送消息给一个或者多个 Topic;一个消费者可以订阅一个或者多个 Topic 消息。 |
集群工作流程
- 启动 NameServer,NameServer 起来后监听端口,等待 Broker、Producer、Consumer 连接,相当于一个路由控制中心;
- Broker 启动,跟所有的 NameServer 保持长连接,定时发送心跳包,心跳包中包含当前 Broker 信息(IP + 端口等)以及存储所有 Topic 信息,注册成功后,NameServer 集群中就有 Topic 跟 Broker 的映射关系;
- 收发消息前,先创建 Topic,创建 Topic 时需指定该 Topic 要存储在哪些 Broker 上,也可以在发送消息时自动创建 Topic;
- Producer 发送消息,启动时先跟 NameServer 集群中的其中一台建立长连接,并从 NameServer 中获取当前发送的 Topic 存在哪些 Broker 上,轮询从队列表中选择一个队列,然后与队列所在的 Broker 建立长连接从而向 Broker 发送消息;
- Consumer 跟 Producer 类似,跟其中一台 NameServer 建立长连接,获取当前订阅 Topic 存在哪些 Broker 上,然后直接跟 Broker 建立连接通道,开始消费消息。
二、RocketMQ的使用
使用 RocketMQ 需要添加 Maven 依赖:
<!-- rocketmq -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.5.2</version>
</dependency>
基本用法
1、生产者生产消息
发送同步消息:
这种可靠性同步地发送方式使用的比较广泛,比如重要的消息通知、短信通知。
// 实例化消息生产者
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
// 设置NameServer的地址
producer.setNamesrvAddr("localhost:9876;localhost:9877");
producer.start();
// 创建消息, 指定Topic、Tag和消息体
Message msg = new Message("TopicTest", "Tag1", "hello rocketmq".getBytes("UTF-8"));
// 发送消息到一个Broker
SendResult sendResult = producer.send(msg);
// 发送状态
SendStatus sendStatus = sendResult.getSendStatus();
// 消息ID
String msgId = sendResult.getMsgId();
// 消息接收队列ID
int queueId = sendResult.getMessageQueue().getQueueId();
// 如果不再发送消息, 关闭消息生产者
// producer.shutdown();
发送异步消息:
通常用在对响应时间敏感的业务场景,即生产者不能容忍长时间地等待 Broker 的响应。
只需要调用 DefaultMQProducer 的异步实现即可:
// 发送消息到一个Broker
producer.send(msg, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
}
@Override
public void onException(Throwable e) {
}
});
发送单向消息:
这种方式主要用在生产者不关心发送结果的场景,例如发送日志。
只需要调用 DefaultMQProducer 的 sendOneway() 方法即可:
// 发送单向消息, 没有任何返回结果
producer.sendOneway(msg);
2、消费者消费消息
负载均衡模式(默认模式):
// 实例化消息消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name");
// 设置NameServer的地址
consumer.setNamesrvAddr("localhost:9876;localhost:9877");
// 订阅Topic、Tag
consumer.subscribe("TopicTest", "Tag1"); // 多个Tag可以用||隔开, 例如"Tag1||Tag2", 消费所有Tag使用"*"
// 设置负载均衡模式, 默认MessageModel.CLUSTERING
// consumer.setMessageModel(MessageModel.CLUSTERING);
// 设置回调函数, 处理消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
for (MessageExt msg : msgs) {
// 消息
String message = new String(msg.getBody());
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
消费者广播模式:
只需要消费者实例调用 setMessageModel() 方法设置广播模式即可:
// 设置广播模式
consumer.setMessageModel(MessageModel.BROADCASTING);
消息类型
顺序消息
顺序消息是指可以按照消息的发送顺序来消费,RocketMQ 可以严格保证消息有序,分为分区有序或者全局有序。
顺序消息的原理?
在默认的情况下消息发送会采取 Round Robin 轮询方式把消息发送到不同的 queue (分区队列);而消费消息的时候从多个 queue 上拉取消息,这种情况发送和消费是不能保证顺序的。
但是如果控制发送的顺序消息只依次发送到同一个 queue 中,消费的时候只从这个 queue 上依次拉取,则就保证了顺序,当发送和消费参与 queue 只有一个,则是全局有序;如果多个 queue 参与,则是分区有序,即相对每个 queue,消息都是有序的。
比如以订单的顺序流程为例,创建、付款、推送、完成。订单号相同的消息会被先后发送到同一个队列中,消费时,同一个订单号获取到的肯定是同一个队列。
1、生产者生产消息
对订单 id 取模选择队列:
// 订单流程
String[] msgs = new String[] {"创建消息", "付款消息", "推送消息", "完成消息"};
for (int i = 0; i < msgs.length; i++) {
// 创建消息, 指定Topic、Tag、key和消息体
Message msg = new Message("OrderTopic", "Order", "i" + i, msgs[i].getBytes("UTF-8"));
// 发送消息到一个Broker, 参数二: 消息队列的选择器, 参数三: 选择的业务标识
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
long orderId = (long) arg;
long index = orderId % mqs.size(); // 取模
return mqs.get((int) index);
}
}, orderId); // 动态传入订单id
}
2、消费者消费消息
只是设置 Consumer 回调函数不同:
// 订阅Topic、Tag
consumer.subscribe("OrderTopic", "*");
// 设置回调函数, 处理消息
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
for (MessageExt msg : msgs) {
// 消息
String message = new String(msg.getBody());
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
延时消息
比如电商场景,提交了一个订单就可以发送一个延时消息,1h 后去检查这个订单的状态,如果还是未付款就取消订单释放库存。
只需要为 Message 对象设置延时等级即可:
// 创建消息, 指定Topic、Tag和消息体
Message msg = new Message("TopicTest", "Tag1", "hello rocketmq".getBytes("UTF-8"));
// 设置延时等级3, 这个消息将在10s后发送
msg.setDelayTimeLevel(3);
// 发送消息到一个Broker
producer.send(msg);
使用限制:
private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";
现在 RocketMQ 不支持任意时间的延时,需要设置几个固定的延时等级,从 1s 到 2h 分别对应着等级 1 到 8。
消费者消费消息
只是设置 Consumer 回调函数不同:
// 订阅Topic、Tag
consumer.subscribe("TopicTest", "*");
// 设置回调函数, 处理消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
for (MessageExt msg : msgs) {
// 消息
String message = new String(msg.getBody());
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
批量消息
批量发送消息能显著提高传递小消息的性能,限制是这些批量消息应该有相同的 Topic,相同的 waitStoreMsgOK,而且不能是延时消息,此外,这一批消息的总大小不应超过 4MB。
List<Message> messages = new ArrayList<>();
messages.add(new Message("TopicTest", "Tag1", "OrderID001", "hello 0".getBytes("UTF-8")));
messages.add(new Message("TopicTest", "Tag1", "OrderID002", "hello 1".getBytes("UTF-8")));
messages.add(new Message("TopicTest", "Tag1", "OrderID003", "hello 2".getBytes("UTF-8")));
// 发送消息到一个Broker
producer.send(messages);
如果消息的总长度可能大于 4MB 时,最好把消息进行分割:
public class ListSplitter implements Iterator<List<Message>> {
private final int SIZE_LIMIT = 1024 * 1024 * 4;
private final List<Message> messages;
private int currIndex;
public ListSplitter(List<Message> messages) {
this.messages = messages;
}
@Override
public boolean hasNext() {
return currIndex < messages.size();
}
@Override
public List<Message> next() {
int nextIndex = currIndex;
int totalSize = 0;
for (; nextIndex < messages.size(); nextIndex++) {
Message message = messages.get(nextIndex);
int tmpSize = message.getTopic().length() + message.getBody().length;
Map<String, String> properties = message.getProperties();
for (Map.Entry<String, String> entry : properties.entrySet()) {
tmpSize += entry.getKey().length() + entry.getValue().length();
}
tmpSize = tmpSize + 20; // 增加日志的开销20字节
if (tmpSize > SIZE_LIMIT) { // 单个消息超过了最大的限制
// 假如下一个子列表没有元素, 则添加这个子列表然后退出循环, 否则只是退出循环
if (nextIndex - currIndex == 0) {
nextIndex++;
}
break;
}
if (tmpSize + totalSize > SIZE_LIMIT) { break; }
else { totalSize += tmpSize; }
}
List<Message> subList = messages.subList(currIndex, nextIndex);
currIndex = nextIndex;
return subList;
}
}
// 把大的消息分裂成若干个小的消息
ListSplitter splitter = new ListSplitter(messages);
while (splitter.hasNext()) {
try {
List<Message> listItem = splitter.next();
producer.send(listItem);
} catch (Exception e) {
}
}
事务消息
Apache RocketMQ在4.3.0版中已经支持分布式事务消息。
采用了2PC的思想来实现了提交事务消息,同时增加一个补偿逻辑来处理二阶段超时或者失败的消息。
事务消息整体流程:
事务消息的补偿流程:
- 对没有Commit/Rollback的事务消息(pending状态的消息),从服务端发起一次“回查”
- Producer收到回查消息,检查回查消息对应的本地事务的状态
- 根据本地事务状态,重新Commit或者Rollback
三、常见问题
1、RocketMQ 消息怎么保证高可用?
Broker(消息服务器)几种部署方式对比:
方式 | 优点 | 缺点 |
---|---|---|
单 Master | 1.一旦Broker重启或者宕机时,会导致整个服务不可用 2.不建议线上环境使用 | |
多 Master | 配置简单 | 1.单台机器宕机期间,该机器上未被消费的消息在机器恢复之前不可订阅,消息实时性会受影响 |
多 Master 多 Slave(同步双写) | 数据与服务都无单点,Master宕机情况下,消息无延迟,服务可用性与数据可用性都非常高 | 性能比异步复制模式略低,大约低 10%左右,发送单个消息的 RT会略高。目前主宕机后,备机不能自动切换为主机 |
多 Master多 Slave(异步复制) | 1.消息实时性不会受影响 2.主流生产环境部署集群采用方案 | Master 宕机,磁盘损坏情况,会丢失少量消息 |
2、RocketMQ 消息种类以及怎么保证消息有序?
生产者生产消息时指定特定的 MessageQueue ,消费者消费消息时,消费特定的 MessageQueue,其实单机版的消息中心在一个 MessageQueue 就天然支持了顺序消息
3、RocketMQ如何保证消息不丢失?
-
从Producer的视角来看:如果消息未能正确的存储在MQ中,或者消费者未能正确的消费到这条消息,都是消息丢失。
- 默认情况下,可以通过同步的方式阻塞式的发送,check SendStatus,状态是OK,表示消息一定成功的投递到了Broker,状态超时或者失败,则会触发默认的2次重试。
-
从Broker的视角来看:如果消息已经存在Broker里面了,如何保证不会丢失呢(宕机、磁盘崩溃)
- Broker设置为同步刷盘策略
- Broker集群支持设置为同步复制方式,同步复制可以保证即使Master 磁盘崩溃,消息仍然不会丢失
-
从Consumer的视角来看:如果消息已经完成持久化了,但是Consumer取了,但是未消费成功且没有反馈,就是消息丢失
4、RocketMQ如何避免消息重复消费?
-
RocketMQ 让使用者在消费者端去解决该问题,即需要消费者端在消费消息时支持幂等性的去消费消息,具体实现可以查询关于消息幂等消费的解决方案
-
最简单的解决方案是每条消费记录有个消费状态字段,根据这个消费状态字段来是否消费或者使用一个集中式的表,来存储所有消息的消费状态,从而避免重复消费