2-rocketmq-消息发送和接收
quick start
添加依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.7.1</version>
</dependency>
生产者
public class Producer {
public static void main(String[] args) throws MQClientException, InterruptedException {
/**
* 生产者组,简单来说就是多个发送同一类消息的生产者称之为一个生产者组rocketmq支持事务消息,在发送事务消息时,如果事务消息异常(producer挂了),broker端会来回查事务的状态,这个时候会根据group名称来查找对应的producer来执行相应的回查逻辑。相当于实现了producer的高可用
*/
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
// namesrv地址 多个地址用 ; 隔开 从namesrv上拉取broker信息
producer.setNamesrvAddr("localhost:9876");
producer.start();
for (int i = 0; i < 1000; i++) {
try {
/**
* 创建消息实例,指定topic,tag,消息内容。tag
*/
Message msg = new Message("TopicTest" /* Topic */, "TagA" /* Tag */,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
);
// 发送消息并获取发送结果 同步发送
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
} catch (Exception e) {
e.printStackTrace();
Thread.sleep(1000);
}
}
producer.shutdown();
}
}
SendResult中,有一个sendStatus状态,表示消息的发送状态。一共有四种状态
- FLUSH_DISK_TIMEOUT : 表示没有在规定时间内完成刷盘(需要Broker 的刷盘策Ill创立设置成
SYNC_FLUSH 才会报这个错误) 。 - FLUSH_SLAVE_TIMEOUT :表示在主备方式下,并且Broker 被设置成SYNC_MASTER 方式,没有
在设定时间内完成主从同步。 - SLAVE_NOT_AVAILABLE : 这个状态产生的场景和FLUSH_SLAVE_TIMEOUT 类似, 表示在主备方
式下,并且Broker 被设置成SYNC_MASTER ,但是没有找到被配置成Slave 的Broker 。 - SEND OK :表示发送成功,发送成功的具体含义,比如消息是否已经被存储到磁盘?消息是否被
同步到了Slave 上?消息在Slave 上是否被写入磁盘?需要结合所配置的刷盘策略、主从策略来
定。这个状态还可以简单理解为,没有发生上面列出的三个问题状态就是SEND OK
消费者
public class Consumer {
public static void main(String[] args) throws InterruptedException, MQClientException {
//groupName 将多个consumer分组,提高并发处理能力。需要和MessageModel配合
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_4");
// 多个地址 ;分开 获取broker地址 并定时向broker发送心跳 可以从master/slave获取订阅
consumer.setNamesrvAddr("localhost:9876");
// 两种消息模式 BROADCASTING CLUSTERING
consumer.setMessageModel(MessageModel.BROADCASTING);
//设置consumer第一次启动从队列头部还是尾部开始消费
//如果非第一次启动,那么按上一次消费的位置继续消费(取决于本地的offeset数据)
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
// topic 可通过tag过滤消息 * 或 null 代表全部
consumer.subscribe("TopicTest", "*");
/**注册消息处理回调
* MessageListenerConcurrently 普通监听
* MessageListenerOrderly 顺序监听
*/
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
// todo 消息处理逻辑
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
// 返回消费状态
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 启动consumer
consumer.start();
System.out.printf("Consumer Started.%n");
}
}
消费状态
ConsumeConcurrentlyStatus {
// 消费成功
CONSUME_SUCCESS,
// 使用失败,稍后尝试使用
RECONSUME_LATER;
消息发送及消费的基本原理
集群部署,一个master可以有多个slave,一个slave只能有一个master.consumer可以从master获者slave中订阅消息
2m-2s示例:
rocketMQ 没有实现master选举(通过配置文件来指定主从)
当master挂了后 消费者依然能正常消费消息(slave提供读服务)
通过groupName实现分区,提高消费者的处理能力
消费者
两种消费者类型
- DefaultMQPushConsumer 由系统控制读取操作
DefaultMQPushConsumer
自动保存offset,自动做负载均衡
public class Consumer {
public static void main(String[] args) throws InterruptedException, MQClientException {
//groupName 将多个consumer分组,提高并发处理能力。需要和MessageModel配合
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_4");
// 多个地址 ;分开
consumer.setNamesrvAddr("localhost:9876");
// 两种消息模式 BROADCASTING CLUSTERING
consumer.setMessageModel(MessageModel.BROADCASTING);
//第一次启动从 offset头开始
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
// topic 可通过tag过滤消息 * 或 null 代表全部
consumer.subscribe("TopicTest", "*");
//注册消息处理回调
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
// todo 消息处理逻辑
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 启动consumer
consumer.start();
System.out.printf("Consumer Started.%n");
}
}
两种消息模式 BROADCASTING CLUSTERING:
1、在 Clustering 模式下,同一个 ConsumerGroup ( GroupName 相同 ) 里的每个 Consumer 只消费所订阅消息 的一部分 内 容, 同一个 ConsumerGroup里所有的 Consumer 消 费 的内 容合起来才是所订阅 Topic 内 容 的 整体 ,从而达到负载均衡的目的 (也就是集群消费)
2、在 Broadcasting 模式下,同一个 ConsumerGroup 里的每个 Consumer 都能消费到所订阅 Topic 的全部消息,也就是一个消息会被多次分发,被多个Consumer 消费 。(也就是广播模式)
通过长轮询的方式获取消息
Broker端HOLD住客户端过来的请求一小段时间,在这个时间内有新消息到达就利用现有的连接立刻返回消息给Consumer。主动权在Consumer
好处是客户端能充分利用资源,不至于处理不过来
流量控制
DefaultMQPullConsumer
需要自己维护offset,需要通过遍历MessageQueue获取消息
public class PullConsumer {
private static final Map<MessageQueue, Long> OFFSE_TABLE = new HashMap<MessageQueue, Long>();
public static void main(String[] args) throws MQClientException {
DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("please_rename_unique_group_name_5");
consumer.setNamesrvAddr("127.0.0.1:9876");
consumer.start();
// 获取分片
Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("broker-a");
for (MessageQueue mq : mqs) {
System.out.printf("Consume from the queue: %s%n", mq);
SINGLE_MQ:
while (true) {
try {
PullResult pullResult =
consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32);
System.out.printf("%s%n", pullResult);
putMessageQueueOffset(mq, pullResult.getNextBeginOffset());
switch (pullResult.getPullStatus()) {
// 获取到消息
case FOUND:
break;
case NO_MATCHED_MSG:
break;
// 没有新消息
case NO_NEW_MSG:
break SINGLE_MQ;
case OFFSET_ILLEGAL:
break;
default:
break;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
consumer.shutdown();
}
private static long getMessageQueueOffset(MessageQueue mq) {
Long offset = OFFSE_TABLE.get(mq);
if (offset != null)
return offset;
return 0;
}
private static void putMessageQueueOffset(MessageQueue mq, long offset) {
OFFSE_TABLE.put(mq, offset);
}
}
Consumer的启动、关闭
DefaultMQPushConsumer启动时不会检查nameServer地址的正确或者可用性
// 从指定topic中拉取所有消息队列
Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("order-topic");
可以通过上面的方法主动拉取消息队列来判断nameServer的可用性
关闭时调用shutdown()即可
DefaultMQPullConsumer关闭或者异常退出时需要将offset保存起来
才能保证下次启动时拉取消息的正确性
consumerGroup:位于同一个consumerGroup中的consumer实例和producerGroup中的各个produer实例承担的角色类似;同一个group中可以配置多个consumer,可以提高消费端的并发消费能力以及容灾
和kafka一样,多个consumer会对消息做负载均衡,意味着同一个topic下的不messageQueue会分发给同一个group中的不同consumer
消费端的负载均衡
和kafka一样,消费端也会针对Message Queue做负载均衡,使得每个消费者能够合理的消费多个分区的消息。
消费端会通过RebalanceService线程,10秒钟做一次基于topic下的所有队列负载
-
消费端遍历自己的所有topic,依次调rebalanceByTopic
-
根据topic获取此topic下的所有queue
-
选择一台broker获取基于group的所有消费端(有心跳向所有broker注册客户端信息)
-
选择队列分配策略实例AllocateMessageQueueStrategy执行分配算法
什么时候触发负载均衡
- 消费者启动之后
- 消费者数量发生变更
- 每10秒会触发检查一次rebalance
分配算法
RocketMQ提供了6中分区的分配算法
- (AllocateMessageQueueAveragely)平均分配算法(默认)
- (AllocateMessageQueueAveragelyByCircle)环状分配消息队列
- (AllocateMessageQueueByConfig)按照配置来分配队列: 根据用户指定的配置来进行负载
- (AllocateMessageQueueByMachineRoom)按照指定机房来配置队列
- (AllocateMachineRoomNearby)按照就近机房来配置队列:
- (AllocateMessageQueueConsistentHash)一致性hash,根据消费者的cid进行
生产者
DefaultMQProducer 默认生产者
public class Producer {
public static void main(String[] args) throws MQClientException, InterruptedException {
// producerGroupName
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
// namesrv地址 多个地址用 ; 隔开
producer.setNamesrvAddr("localhost:9876");
producer.start();
for (int i = 0; i < 1000; i++) {
try {
Message msg = new Message("TopicTest" /* Topic */, "TagA" /* Tag */,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
);
// 返回
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
} catch (Exception e) {
e.printStackTrace();
Thread.sleep(1000);
}
}
producer.shutdown();
}
}
消息返回状态:SendResult.sendStatus
1、FLUSH DISK TIMEOUT : 表示没有在规定时间内完成刷盘(需要Broker 的刷盘策略设置成 SYNC FLUSH 才会报这个错误) 。
2、 FLUSH SLAVE TIMEOUT :表示在主备方式下,并且 Broker 被设置成 SYNC MASTER 方式,没有在设定时间内完成主从同步 。
3、SLAVE NOT AVAILABLE : 这个状态产生的场景和 FLUSH SLAVETIMEOUT 类似, 表示在主备 方式下,并且 Broker 被设置成 SYNCMASTER ,但是没有找到被配置成 S lave 的 Broker 。
4、SEND OK :表示发送成功,发送成功的具体含义,比如消息是否已经被存储到融盘?消息是否被同步到了 S lave 上?消息在 S lave 上是否被写人磁盘?需要结合所配置的刷盘策略、主从策略来定 。 这个状态还可以简单理解为,没有发生上面列出的三个问题状态就是 SEND OK
延迟消息
通过Message.setDelayTimeLevel ( int level ) 方法设置延迟时间,只支持预设值(1s/5s/1Os/30s/Im/2m/3m/4m/5m/6m/7m/8m/9m/1 Om/20m/30m/1 h/2h )。 比如setDelayTimeLevel(3)表示延迟 10s 。
自定义消息发送规则
实现MessageQueueSelector接口
三种默认实现:
SelectMessageQueueByHash
SelectMessageQueueByMachineRoom
SelectMessageQueueByRandom
自定义消息发送可以将消息发送到指定的MessageQueue里
对事物的支持
new TransactionMQProducer("groupName");
设置生产者group,当一个producer挂掉了,消息会分发到其它producer保证消息一定会被回查确定
消息的可靠性原则
只有消费者返回CONSUME_SUCCESS消费成功的才会认为消费成功
返回ConsumeConcurrentlyStatus.RECONSUME_LATER消费失败会被重试
消息衰减重试
为了保证消息肯定至少被消费一次,RocketMQ会把这批消息重新发回到broker,在延迟的某个时间点
(默认是10秒,业务可设置)后,再次投递到这个ConsumerGroup。而如果一直这样重复消费都持续
失败到一定次数(默认16次),就会投递到DLQ死信队列。应用可以监控死信队列来做人工干预
可以修改broker-a.conf文件
messageDelayLevel = 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
重试消息的处理机制
一般情况下我们在实际生产中是不需要重试16次,这样既浪费时间又浪费性能,理论上当尝试重复次数
达到我们想要的结果时如果还是消费失败,那么我们需要将对应的消息进行记录,并且结束重复尝试
consumer.registerMessageListener((MessageListenerConcurrently) (list,
consumeOrderlyContext) -> {
for (MessageExt messageExt : list) {
if(messageExt.getReconsumeTimes()==3) {
//可以将对应的数据保存到数据库,以便人工干预
System.out.println(messageExt.getMsgId()+","+messageExt.getBody());
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
} r
eturn ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
死信队列
RocketMQ会为每个消费组都设置一个Topic命名为“%DLQ%+consumerGroup"的死信队列。