简介
阿里巴巴基于Java语言开发的分布式消息中间件。RocketMQ是Mateq3.0之后的开源版本。Metaq最早源于Kafka,早期借鉴了Kafka很多优秀的设计。
RocketMQ的使用场景
应用解耦:系统的耦合性越高,容错性就越低。以电商应用为例,用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障或者因为升级等原因暂时不可用,都会造成下单操作异常,影响用户使用体验。
流量削峰:应用系统如果遇到系统请求流量的瞬间猛增,有可能会将系统压垮。有了消息队列可以将大量请求缓存起来,分散到很长一段时间处理,这样可以大大提到系统的稳定性和用户体验。思考:RocketMQ是如何分撒请求的?
举例:业务系统正常时段的QPS如果是1000,流量最高峰是10000,为了应对流量高峰配置高性能的服务器显然不划算,这时可以使用消息队列对峰值流量削峰。
数据分发:通过消息队列可以让数据在多个系统之间进行流通。数据的产生方不需要关心谁来使用数据,只需要将数据发送到消息队列,数据使用方直接在消息队列中直接获取数据即可。
RocketMQ的角色介绍
Producer: 消息的发送者;举例:发信者
Consumer: 消息接收者;举例:收信者
Broker: 暂存和传输消息;举例:邮局
NameServer: 管理Broker 统计borker的各种元数据信息 ;举例:邮局的管理机构
Topic: 逻辑上概念:区分消息的种类,一个发送者可以发送消息给一个或者多个Topic; 一个消息的接收者可以订阅一个或者多个Topic消息
Message Queue:相当于是Topic的分区,用于并行发送和接收消息。 比如:一个主题分为三个分区,就可以将一个主题存放在三台服务器中。
NameServer是无状态的
- NameServer:是一个几乎无状态节点(启动即用,不启动就不用),节点之间无任何信息同步,可集群部署。
- Broker :部署相对复杂,Broker分为Master与Slave,BrokerName相同即为一组(主从结构),不同的BrokerId来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个。每个Broker与NameServer集群中的所有节点建立长连接,定时注册Topic信息到所有NameServer。 注意:当前RocketMQ版本在部署架构上支持一Master多Slave,但只有BrokerId=1的从服务器才会参与消息的读负载。
- Producer: 与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer获取Topic路由信息,并向提供Topic 服务的Master建立长连接,且定时向Master发送心跳。Producer完全无状态,可集群部署。
- Consumer:与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer获取Topic路由信息,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳。Consumer既可以从Master订阅消息,也可以从Slave订阅消息,
消费者在向Master拉取消息时,Master服务器会根据拉取偏移量与最大偏移量的距离(判断是否读老消息,产生读I/O),以及从服务器是否可读等因素建议下一次是从Master还是Slave拉取。 思考:读负载均衡是如何实现的?
执行流程:
1. 启动NameServer,NameServer起来后监听端口,等待Broker、Producer、Consumer连上来,相当于一个路由控制中心。
2. Broker启动,跟所有的NameServer保持长连接,定时发送心跳包。心跳包中包含当前Broker信息(IP+端口等)以及存储所有Topic信息。注册成功后,NameServer集群就知道了:哪些Broker存放了哪些Topic。
3. 收发消息前,先创建Topic,创建Topic时需要指定该Topic要存储在哪些Broker上,也可以在发送消息时自动创建Topic。
4. Producer发送消息,启动时先跟NameServer集群中的其中一台建立长连接,并从NameServer中获取当前发送的Topic存在哪些Broker上,轮询从队列列表中选择一个队列(message Queue 相当于topic的分区 ),然后与队列所在的Broker建立长连接从而向Broker发消息。
5. Consumer跟Producer类似,跟其中一台NameServer建立长连接,获取当前订阅Topic存在哪些Broker上,然后直接跟Broker建立连接通道,开始消费消息。
RocketMQ特性
- 订阅与发布:消息的发布是指某个生产者向某个topic发送消息;消息的订阅是指某个消费者关注了某个topic中带有某些tag的消息。
- 消息顺序: 消息有序指的是一类消息消费时,能按照发送的顺序来消费。
- 消息过滤:RocketMQ的消费者可以根据Tag(主要是根据Tag的hash值)/ SQL92标准 进行消息过滤,也支持自定义属性过滤。消息过滤目前是在Broker端实现(Broker端主要依靠众多的Filter实现)。各种消息过滤器机制,例如SQL和Tag
- 消息可靠性: 主要是通过 将消息持久化:刷盘,和集群化:复制 来实现的。
- 至少一次:指每个消息必须投递一次。Consumer先Pull消息到本地,消费完成后,才向服务器返回ack,如果没有消费一定不会ack消息,所以RocketMQ可以很好的支持此特性。
-
回溯消费:回溯消费是指Consumer已经消费成功的消息,由于业务上需求需要重新消费,要支持此功能, Broker在向Consumer投递成功消息后,消息仍然需要保留。并且重新消费一般是按照时间维度,例如:由于Consumer系统故障,恢复后需要重新消费1小时前的数据,那么Broker要提供一种机制,可以按 照时间维度来回退消费进度。RocketMQ支持按照时间回溯消费,时间维度精确到毫秒。
- 事务消息:是指应用本地事务和发送消息操作可以被定义到全局事务中,要么同时成功,要么同时失败。
- 定时消息:是指消息发送到broker后,不会立即被消费,等待特定时间投递给真正topic。定时消息会暂存在名为SCHEDULE_TOPIC_XXXX的topic中
- 消息重试:消费者消费消息失败后,要提供一种重试机制,令消息再消费一次。
-
消息重投: 生产者在发送消息时,当发生以下情况可以设置消息重投: 1.同步消息失败会重投 2.异步消息有重试 3.oneway没有任何保证 ,其实这是保证了消息至少一次特性
-
流量控制:生产者流控,因为broker处理能力达到瓶颈;消费者流控,因为消费能力达到瓶颈。
- 死信队列: 死信队列用于:处理无法被正常消费的消息。死信消息(Dead-Letter Message), 死信队列(Dead-Letter Queue),可通过控制台手动消费。
消费模式Push or Pull
RocketMQ消息订阅有两种模式,
- 一种是Push模式(MQPushConsumer),即MQServer主动向消费端推送;
- 一种是Pull模式 (MQPullConsumer), 即消费端在需要时,主动到MQ Server拉取。
但在具体实现时,Push和Pull模式本质都是采用消费端主动拉取的方式,即consumer轮询从broker拉取消息。
Push模式特点:
好处就是实时性高。不好处在于消费端的处理能力有限,当瞬间推送很多消息给消费端时,容易造成消费端的消息积压,严重时会压垮客户端。
Pull模式
好处就是主动权掌握在消费端自己手中,根据自己的处理能力量力而行。缺点就是控制Pull的频率较难。定时间隔太久担心影响时效性,间隔太短担心做太多“无用功”浪费资源。比较折中的办法就是长轮询
Push模式与Pull模式的区别:
Push方式里,consumer把长轮询的动作封装了,并注册MessageListener监听器,取到消息后,唤醒MessageListener的consumeMessage()来消费,对用户而言,感觉消息是被推送过来的。
Pull方式里,取消息的过程需要用户自己主动调用,首先通过打算消费的Topic拿到MessageQueue的集合,遍历MessageQueue集合,然后针对每个MessageQueue批量取消息,一次取完后,记录该队列下一次要取的开始offset,直到取完了,再换另一个MessageQueue。
RocketMQ中的角色及相关术语
RocketMQ主要由 Producer、Broker、Consumer 三部分组成,其中Producer 负责生产消息,Consumer 负责消费消息,Broker 负责存储消息。Broker 在实际部署过程中对应一台服务器,
每个Broker 可以存储多个Topic的消息,每个Topic的消息也可以分片存储于不同的 Broker。Message Queue 用于存储消息的物理地址,每个Topic中的消息地址存储于多个 Message Queue 中。
- Broker :消息中转角色,负责存储消息,转发消息,一般也称为 Server。在 JMS 规范中称为Provider。
- Producer : 消息生产者,负责产生消息,一般由业务系统负责产生消息。
- Consumer: 消息消费者,负责消费消息,一般是后台系统负责异步消费。
- PushConsumer
Consumer消费的一种类型,该模式下Broker收到数据后会主动推送给消费端。应用通常向
Consumer对象注册一个Listener接口,一旦收到消息,Consumer对象立刻回调Listener接口方法。该消费模式一般实时性较高。
Consumer消费的一种类型,应用通常主动调用Consumer的拉消息方法从Broker服务器拉消息、 主动权由应用控制。一旦获取了批量消息,应用就会启动消费过程。
同一类Producer的集合,这类Producer发送同一类消息且发送逻辑一致。如果发送的是事务消息且原始生产者在发送之后崩溃,则Broker服务器会联系同一生产者组的其他生产者实例以提交或回溯消费。
同一类Consumer的集合,这类Consumer通常消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面,实现负载均衡和容错的目标变得非常容易。要注意的是,消费者组的消费者实例必须订阅完全相同的Topic。
RocketMQ 支持两种消息模式:集群消费(Clustering)和广播消费(Broadcasting)
广播消费: 消息也会 被 Consumer Group 中的每个 Consumer 都消费一次, 在 JMS 规范中,相当于 JMS Topic( publish/subscribe )模型
集群消费: 一个 Consumer Group 中的 Consumer 实例平均分摊消费消息。例如某个 Topic 有 9 条消息,其 中一个 Consumer Group 有 3 个实例(可能是 3 个进程,或者3台机器),每个实例只消费其中的 3条消息。
- 顺序消息: 消费消息的顺序要同发送消息的顺序一致,在RocketMQ 中主要指的是局部顺序,即一类消息为满足顺序性,必须Producer单线程顺序发送,且发送到同一个队列,这样Consumer 就可以按照Producer发送的顺序去消费消息。
普通顺序消息
顺序消息的一种,正常情况下可以保证完全的顺序消息,但是一旦发生通信异常,Broker 重启,
由于队列总数发生发化,哈希取模后定位的队列会发化,产生短暂的消息顺序不一致。 如果业务能容忍在集群异常情况(如某个Broker 宕机或者重启)下,消息短暂的乱序,使用普通顺序方式比较合适。
严格顺序消息
顺序消息的一种,无论正常异常情况都能保证顺序,但是牺牲了分布式 Failover特性,即Broker集群中只要有一台机器不可用,则整个集群都不可用,服务可用性大大降低。
如果服务器部署为同步双写模式,此缺陷可通过备机自动切换为主避免,不过仍然会存在几分钟的服务不可用。(依赖同步双写,主备自动切换,自动切换功能目前还未实现)
- Message Queue: 在 RocketMQ 中,所有消息队列都是持久化的,长度无限的数据结构,所谓长度无限是指队列中的每个存储单元都是定长,访问其中的存储单元使用Offset来访问, offset 就是下标。
- 标签(Tag): 为消息设置的标志,用于同一主题下区分不同类型的消息。
RocketMQ API
DefaultMQProducer: 生产者的默认实现: 生产消息分同步发送和异步发送
public class MyProduct {
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
// 在实例化生产者的同时,指定生产组名称
DefaultMQProducer defaultMQProducer = new DefaultMQProducer("otter_metaq_group");
// 指定NameServer 的地址
defaultMQProducer.setNamesrvAddr("Rocketmq1.dev.dc.com:9876");
// 启动生产者
defaultMQProducer.start();
// 创建消息,第一个参数是主题名称,第二个参数是tag标签 第三个参数是消息内容
Message msg=new Message(
"otter_test_myProduct",
"test",
"this is test prodcut".getBytes(StandardCharsets.UTF_8)
);
// 发送消息 同步发送
final SendResult result = defaultMQProducer.send(msg);
System.out.println(result);
// 关闭生产者
defaultMQProducer.shutdown();
}
}
public class MyAsyncProducer {
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException {
// 在实例化生产者的同时,指定生产组名称
DefaultMQProducer defaultMQProducer = new DefaultMQProducer("otter_metaq_group");
// 指定NameServer 的地址
defaultMQProducer.setNamesrvAddr("Rocketmq1.dev.pdc.com:9876");
// 启动生产者
defaultMQProducer.start();
// 创建消息,第一个参数是主题名称,第二个参数是tag标签 第三个参数是消息内容
for (int i = 0; i < 100; i++) {
Message msg=new Message(
"otter_test_myProduct",
"test",
("this is test myAsyncProducer i=:"+i).getBytes(StandardCharsets.UTF_8)
);
// 异步发送消息
defaultMQProducer.send(msg, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println("发送成功:" + sendResult);
}
@Override
public void onException(Throwable throwable) {
System.out.println("发送失败:" + throwable.getMessage());
}
});
}
// 由于是异步发送消息,上面循环结束之后,消息可能还没收到broker的响应
// 如果不sleep一会儿,就报错
Thread.sleep(10_000);
// 关闭生产者
defaultMQProducer.shutdown();
}
}
// 源码解析
public class SendResult {
private SendStatus sendStatus; //发送状态结果: 是一个枚举类型
private String msgId;
private MessageQueue messageQueue;
private long queueOffset;
}
// 发送状态结果
public enum SendStatus {
SEND_OK,
FLUSH_DISK_TIMEOUT,
FLUSH_SLAVE_TIMEOUT,
SLAVE_NOT_AVAILABLE;
private SendStatus() {
}
} |
DefaultMQConsumer:消费者的默认实现: 消息的拉取和消息的推送
/**
* @version 1.0
* @description: 消息消费-主动拉取 :注意 主动拉取需要自己定义:拉取的频率
* @date 2021-7-9 10:12
*/
public class MyPullConsumer {
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException, UnsupportedEncodingException {
// 1.创建拉取模式的消费者
DefaultMQPullConsumer pullConsumer = new DefaultMQPullConsumer("comsumer_demo_01");
// 2.指定链接的nameServer
pullConsumer.setNamesrvAddr("Rocketmq1.dev.dc.com:9876");
// 3.启动消费者
pullConsumer.start();
// 4.获取指定主题所有的消息队列集合
Set<MessageQueue> msgQueue = pullConsumer.fetchSubscribeMessageQueues(
"otter_test_myProduct");
// 5.遍历该主题中的各个消息队列,进行消费
for (MessageQueue messageQueue : msgQueue) {
// 第一个参数是MessageQueue对象,代表了当前主题的一个消息队列
// 第二个参数是一个表达式,对接收的消息按照tag进行过滤
// 支持"tag1 || tag2 || tag3"或者 "*"类型的写法;null或者"*"表示不对消息进行tag过滤
// 第三个参数是消息的偏移量,从这里开始消费
// 第四个参数表示每次最多拉取多少条消息
PullResult pullResult = pullConsumer.pull(
messageQueue,
"*",
0,
10);
// 6.处理拉取结果
final List<MessageExt> msgFoundList = pullResult.getMsgFoundList();
if (msgFoundList == null) continue;
for (MessageExt messageExt : msgFoundList) {
System.out.println(messageExt);
System.out.println(new String(messageExt.getBody(), "utf-8"));
}
}
// 7.关闭消费者
pullConsumer.shutdown();
}
}
/**
* @version 1.0
* @description: 消费消息-推送模式
* @date 2021-7-9 11:01
*/
public class MyPushConsumer {
public static void main(String[] args) throws MQClientException {
// 实例化推送消息消费者的对象,同时指定消费组名称
DefaultMQPushConsumer pushComsumer = new DefaultMQPushConsumer("comsumer_demo_02");
// 指定nameserver的地址
pushComsumer.setNamesrvAddr("Rocketmq1.dev.dc.com:9876");
// 订阅主题
pushComsumer.subscribe("otter_test_myProduct","*");
// 添加监听器,一但有消息推送过来,就进行消费
pushComsumer.setMessageListener(new MessageListenerConcurrently() {
// 编写消息消费逻辑 consumeMessage
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgList, ConsumeConcurrentlyContext context) {
// ConsumeConcurrentlyContext:当前消费者上下文对像, 该对象可以设置很多消息消费特性
// 如获取当前消费的MQ队列,设置延时消息
final MessageQueue messageQueue = context.getMessageQueue();
// 遍历消息内容
for (MessageExt msg : msgList) {
try {
System.out.println(new String(msg.getBody(), "utf-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
// 返回消息消费状态
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 启动消费者
pushComsumer.start();
}
}
//------------------------------------源码解析--------------------------
/**
* 拉消息返回结果
* @since 2013-7-24
*/
public class PullResult {
private final PullStatus pullStatus; // 拉取结果状态,是一个枚举
private final long nextBeginOffset;
private final long minOffset;
private final long maxOffset;
private List<MessageExt> msgFoundList; // 存放了消息内容
}
// 拉取结果状态
public enum PullStatus {
FOUND,
NO_NEW_MSG,
NO_MATCHED_MSG,
OFFSET_ILLEGAL;
private PullStatus() { }
}
|
基于拉取模式的消费者编写—可用作模板
import com.alibaba.rocketmq.client.consumer.DefaultMQPushConsumer;
import com.alibaba.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import com.alibaba.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import com.alibaba.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import com.alibaba.rocketmq.client.exception.MQClientException;
import com.alibaba.rocketmq.common.message.MessageExt;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.util.List;
/**
* BaseConsumer,订阅消息
*/
public abstract class BaseConsumer implements MessageListenerConcurrently {
private static final Logger logger = LoggerFactory.getLogger(BaseConsumer.class);
protected DefaultMQPushConsumer consumer;
protected String nameServer;
protected int minConsumeThread = 5;
protected int maxConsumeThread = 5;
protected String group;
// 定时消息相关
// 线上环境:messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m
// 30m 40m 50m 1h 2h 6h
// 开发环境:messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m
// 30m 40m 50m 1h 2h 6h 12h 1d
private static final int[] DELAY_LEVELS = new int[]{3, 5, 9, 14, 15, 16, 17, 18, 19, 20, 21};
protected int maxRetryCount = 10;
/**
* 初始化consumer
*/
public void init() {
if (StringUtils.isBlank(System.getProperty("public.rocketmq.domain.name"))) {
System.setProperty("public.rocketmq.domain.name", nameServer);
}
consumer = new DefaultMQPushConsumer(getGroup());
consumer.setNamesrvAddr(nameServer);
consumer.setConsumeThreadMin(minConsumeThread);
consumer.setConsumeThreadMax(maxConsumeThread);
//可以不设置 设置后可以起多个 消费端
consumer.setInstanceName(getInstanceName());
try {
//设置订阅的topic 设置订阅过滤表达式
consumer.subscribe(getTopic(), getTags());
consumer.registerMessageListener(this);
consumer.start();
} catch (MQClientException e) {
logger.error("consumer start error!group={}", group, e);
}
logger.info("consumer start! group={}", getGroup());
}
/**
* 销毁consumer
*/
public void destroy() {
if (consumer != null) {
consumer.shutdown();
logger.info("consumer shutdown! group={}", group);
}
}
/**
* 基类实现消息监听接口,加上打印metaq监控日志的方法
*/
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
long startTime = System.currentTimeMillis();
if (msgs == null || msgs.size() < 1) {
logger.error("receive empty msg!");
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
final int reconsumeTimes = msgs.get(0).getReconsumeTimes();
if (reconsumeTimes >= maxRetryCount) {
logger.warn("reconsumeTimes >" + maxRetryCount + "msgs:" + msgs + "context:" + context);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
context.setDelayLevelWhenNextConsume(getDelayLevelWhenNextConsume(reconsumeTimes));
boolean ret = true;
for (MessageExt message : msgs) {
if (!doConsumeMessage(decodeMsg(message))) {
ret = false;
}
}
ConsumeConcurrentlyStatus status = ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
if (!ret) {
status = ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
// logger.info("ConsumeConcurrentlyStatus:{}|cost:{}", status, System.currentTimeMillis() - startTime);
return status;
}
/**
* 根据重试次数设置重新消费延迟时间
* 1s 10s 30s 2m 10m 30m 1h 2h 12h 1d
*
* @param reconsumeTimes 重试的次数
* @return level级别
*/
public int getDelayLevelWhenNextConsume(int reconsumeTimes) {
if (reconsumeTimes >= DELAY_LEVELS.length) {
return DELAY_LEVELS[DELAY_LEVELS.length - 1];
}
return DELAY_LEVELS[reconsumeTimes];
}
private Serializable decodeMsg(MessageExt msg) {
if (msg == null) {
return null;
}
//1.反序列化
try {
return HessianUtils.decode(msg.getBody());
} catch (Exception e) {
logger.warn("反序列化出错!" + e.getMessage(), e);
}
//反序列化异常的,直接转为String
try {
return new String(msg.getBody(), "UTF-8");
} catch (Exception e) {
logger.warn("decodeMsg msg.getBody() throw exception !", e);
}
return null;
}
public void setNameServer(String nameServer) {
this.nameServer = nameServer;
}
public void setMinConsumeThread(int minConsumeThread) {
this.minConsumeThread = minConsumeThread;
}
public void setMaxConsumeThread(int maxConsumeThread) {
this.maxConsumeThread = maxConsumeThread;
}
public void setGroup(String group) {
this.group = group;
}
public String getGroup() {
return group + "_" + getTopic() + "_" + getTags();
}
public abstract String getTopic();
public abstract String getTags();
public abstract boolean doConsumeMessage(Serializable message);
public String getInstanceName() {
return "OtterBaseConsumer" + "_" + getTopic() + "_" + getTags();
}
}
|