RocketMQ编程模式的实现
RocketMQ消息模型:在开发中围绕这个消息模型设置不同的生产者和消费者。
rocketMQ存消息的方式:messageQueue,通过ofset来标识自己存储了哪些消息。位点-----消息的偏移量,或者说消息的编号
客户端发送消息的流程
-
在RocketMQ中提供了一个DefaultMQProducer对象,客户端通过这个发就可以了
- 声明一个生产者
- 知名一个生产者所属的组(可以有多个生产者属于相同的组,同一个组下面的生产者业务逻辑相同)
-
指明具体的NameServer----NameServer会记录具体的Broker,只要直到NameServer在哪,就可以从NameServer上面获得所有的Broker地址列表,客户端就可以选择一个NameServer发送消息。
-
调用start()方法保存producer的状态,可以和Broker进行多次的交互,发一次消息其实也是和Broker会有多次的请求交互的
-
发送消息的方式:
- sendoneway()性能最高但是可靠性低
- sendResult----同步发送
- 异步发送-----会异步接收消息发送返回的结果,使用最多
-
//有个sendCallBack()接口来实现发送成功或者失败后的回调处理,成功怎么办,失败了怎么办 producer.send(msg,new SendCallBack(){ @Override public void Onsuccess(SendResult sendResult){ countDownLatch.countDown(); System.out.println("%-10d ok %s %n",index,sendResult.getMsgId()); } }) ..... ....
消费者端:broker推模式和拉取模式,一般用的是推模式,因为不确定是否能拉到消息。
广播消息
⼴播模式和集群模式是RocketMQ的消费者端处理消息最基本的两种模式。集群模式下,⼀个消息,只会被⼀个消费者组中的多个消费者实例 共同 处理⼀次。⼴播模式下,⼀个消息,则会推送给所有消费者实例处理,不再关⼼消费者组。
-
示例代码:
消费者端:
consumer.setMessageModel(MessageModel.BROADCASTING);
启动多个消费者,⼴播模式下,这些消费者都会消费⼀次消息。
默认模式(也就是集群模式)下,Broker端会给每个ConsumerGroup维护⼀个统⼀的Offset,这个Offset可以保证
⼀个消息,在同⼀个ConsumerGroup内只会被消费⼀次。⽽⼴播模式的实现⽅式,是将Offset转移到消费者端⾃
⾏保管,这样Broker端只管向所有消费者推送消息,⽽不⽤负责维护消费进度。
- 注意点:
1、Broker端不维护消费进度,意味着,如果消费者处理消息失败了,将⽆法进⾏消息重试。
2、消费者端维护Offset的作⽤是可以在服务重启时,按照上⼀次消费的进度,处理后⾯没有消费过的消息。丢了也不影响服务稳定性。
RocketMQ会通过定时任务不断尝试本地Offsets⽂件的写⼊,但是,如果本地Offsets⽂件写⼊失败,RocketMQ不会进⾏任何的补救。
顺序消费
- 应用场景
每⼀个订单有从下单、锁库存、⽀付、下物流等⼏个业务步骤。每个业务步骤都由⼀个消息⽣产者通知给下游服务。如何保证对每个订单的业务处理顺序不乱很关键。
- 示例代码
for (int i = 0; i < 10; i++) {
int orderId = i;
for(int j = 0 ; j <= 5 ; j ++){
Message msg =
new Message("OrderTopicTest", "order_"+orderId, "KEY" +
orderId,
("order_"+orderId+" step " +
j).getBytes(RemotingHelper.DEFAULT_CHARSET));
SendResult sendResult = producer.send(msg, new
MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg,
Object arg) {
Integer id = (Integer) arg;
int index = id % mqs.size();
return mqs.get(index);
}
}, orderId);
System.out.printf("%s%n", sendResult);
}
}
通过MessageSelector--(消息选择器),将orderId相同的消息,都转发到同⼀个MessageQueue中
订单号相同的消息放到消息队列中
消费者核心代码:
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeOrderlyContext context) {
context.setAutoCommit(true);
for(MessageExt msg:msgs){
System.out.println("收到消息内容 "+new String(msg.getBody()));
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
注⼊⼀个MessageListenerOrderly实现。
- 实现思路:
基础思路:只有放到⼀起的⼀批消息,才有可能保持消息的顺序。
1、⽣产者只有将⼀批有顺序要求的消息,放到同⼀个MesasgeQueue上,Broker才有可能保持这⼀批消息的顺
序。
2、消费者只有⼀次锁定⼀个MessageQueue,拿到MessageQueue上所有的消息,
注意点:
1、理解局部有序与全局有序。⼤部分业务场景下,我们需要的其实是局部有序。如果要保持全局有序,那就只
保留⼀个MessageQueue。性能显然⾮常低。
2、⽣产者端尽可能将有序消息打散到不同的MessageQueue上,避免过于集中导致数据热点竞争 2、消费者端只能⽤同步的⽅式处理消息,不要使⽤异步处理。更不能⾃⾏使⽤批量处理。
3、消费者端只进⾏有限次数的重试。如果⼀条消息处理失败,RocketMQ会将后续消息阻塞住,让消费者进⾏重试。但是,**如果消费者⼀直处理失败,超过最⼤重试次数,**那么RocketMQ就会跳过这⼀条消息,处理后⾯的消息,这会造成消息乱序。
4、消费者端如果确实处理逻辑中出现问题,不建议抛出异常,可以返回
ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT作为替代。
延迟消息
- 应⽤场景:
延迟消息发送是指消息发送到Apache RocketMQ后,并不期望⽴⻢投递这条消息,⽽是延迟⼀定时间后才投递到Consumer进⾏消费。
相比于RabbitMq和Kafka,RocketMq这一特性是比较独特的,RabbitMQ使用死信队列来变相的实现延迟消息,Kafka则不太好实现延时消息。
- 示例代码:
生产端核心代码
msg.setDelayTimeLevel(3)
给msg消息设置一个延迟级别就行了,在RocketMq中定制了18个默认的延迟级别,分别对应18个不同的预设好的延时时间
- 实现思路
RocketMq实现延时消息的难点主要在于性能方面,需要不断的进行定时轮询,全部扫描所有的消息是不可能的。因此可以考虑根据设定的18个延时级别,预设⼀个系统Topic,名字叫做SCHEDULE_TOPIC_XXXX。在这个Topic下,预设18个延迟队列。然后每次只针对这18个队列⾥的消息进⾏延迟操作,这样就不⽤⼀直扫描所有的消息了。
批量消息
⽣产者要发送的消息⽐较多时,可以将多条消息合并成⼀个批量消息,⼀次性发送出去。这样可以减少⽹络IO,提升消息发送的吞吐量。
示例代码:
List<Message> messages = new ArrayList<>();
messages.add(new Message(topic, "Tag", "OrderID001", "Hello world
0".getBytes()));
messages.add(new Message(topic, "Tag", "OrderID002", "Hello world
1".getBytes()));
messages.add(new Message(topic, "Tag", "OrderID003", "Hello world
2".getBytes()));
producer.send(messages);
- 注意点
批量消息的使用还是比较简单的,但是RokectMq做了限制,同一批消息的主题必须相同(避免消息混乱)且不支持延迟消息.还有批量消息的⼤⼩不要超过1M,如果太⼤就需要⾃⾏分割。
过滤消息
- 应⽤场景:
同⼀个Topic下有多种不同的消息,消费者只希望关注某⼀类消息。
例如,某系统中给仓储系统分配⼀个Topic,在Topic下,会传递过来⼊库、出库等不同的消息,仓储系统的不同业务消费者就需要过滤出⾃⼰感兴趣的消息,进⾏不同的业务操作。
还可以进行sql过滤。
- 实例代码实现简单的过滤
String[] tags = new String[] {"TagA", "TagB", "TagC"};
for (int i = 0; i < 15; i++) {
Message msg = new Message("TagFilterTest",
tags[i % tags.length],
"Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
}
消费者端就可以通过这个Tag属性订阅⾃⼰感兴趣的内容。核⼼代码:
consumer.subscribe("TagFilterTest", "TagA");
这样消费者就只会处理TagA的消息
- 示例代码2:SQL过滤
通过Tag属性,只能进⾏简单的消息匹配。如果要进⾏更复杂的消息过滤,⽐如数字⽐较,模糊匹配等,就需要
使⽤SQL过滤⽅式。SQL过滤⽅式可以通过Tag属性以及⽤户⾃定义的属性⼀起,以标准SQL的⽅式进⾏消息过滤。
⽣产者端在发送消息时,出了Tag属性外,还可以增加⾃定义属性。核⼼代码:
String[] tags = new String[] {"TagA", "TagB", "TagC"};
for (int i = 0; i < 15; i++) {
Message msg = new Message("SqlFilterTest",
tags[i % tags.length],
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
);
msg.putUserProperty("a", String.valueOf(i));
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
}
消费者端在进行过滤的时候会指定一个标准的sql语句,定制复杂的sql语句-----跟语句有什么关联?
consumer.subscribe("SqlFilterTest",
MessageSelector.bySql("(TAGS is not null and TAGS in ('TagA', 'TagB'))" +
"and (a is not null and a between 0 and 3)"));