1.消费模型
rocketmq对于使用者来说,我们只需要关心如何推送各种不同类型的消息,以及如何消费各种类型的消息即可。
1.1 同一个消费者组里面消费者订阅关系必须保持一致
比如上图,在同一个消费者组里面,消费者1订阅的是topic1的消息,消费者2订阅的是topic2的消息,这个时候是消费不了的。同一个消费者组里面消费者订阅关系必须保持一致,必须订阅同一个topic并且tag也相同的消息。
1.2 消费者组模式
消费者组有两种模式,分别是集群模式和广播模式。
集群模式:消息会让消费者组中某个消费者消费。
广播模式:消息会让消费者组中所有消费者消费。
所以,集群模式可以通过增加消费者组中消费者数量,提高消费速度;但是广播模式却不能。
3.消费者的负载均衡
Apache RocketMQ 提供了多种集群模式下的分配策略,包括平均分配策略、机房优先分配策略、一致性hash分配策略等,可以通过如下代码进行设置相应负载均衡策略。
consumer.setAllocateMessageQueueStrategy(new AllocateMessageQueueAveragely());
在集群模式下,并且选择平均分配策略的话,尽量的把mqssagequeue平分给每个consumer,如果consumer数量超过了mqssagequeue的数量,剩下的一个consumer将不能消费数据。
4.消费者从mq中拉取数据的方式
push:mq主动推送,实时性较高,但是如果mq如果一次性推送太多消息给消费者的话,可能导致消费者消息堆积甚至崩溃。
pull:一般指的是长轮询的方式,消费者与mq建立长连接,当消费完消息过后,主动去找mq拉取消息,如果mq没有消息,会一直阻塞5秒,再次检查是否有消息到来。直到有消息后,返回结果。
2.推送消息
2.1 同步推送普通消息
同步的意思就是生产者将消息发送给mq后,会一致等待mq返回结果。 代码如下:
public class Producer {
/**
* The number of produced messages.
*/
public static final int MESSAGE_COUNT = 1000;
public static final String PRODUCER_GROUP = "please_rename_unique_group_name";
public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876";
public static final String TOPIC = "TopicTest";
public static final String TAG = "TagA";
public static void main(String[] args) throws MQClientException, InterruptedException {
//初始化生产者组
DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP);
//设置注册中心地址
producer.setNamesrvAddr(DEFAULT_NAMESRVADDR);
//启动生产者
producer.start();
for (int i = 0; i < MESSAGE_COUNT; i++) {
try {
//组装消息
Message msg = new Message(TOPIC /* Topic */,
TAG /* 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);
}
}
/*
* Shut down once the producer instance is not longer in use.
*/
producer.shutdown();
}
}
复制代码
2.2 异步推送消息
异步推送消息,会提供一个回调函数给mq,当mq返回结果时,会回调这个回调函数。 代码如下:
public class ProducerAsyn {
public static final int MESSAGE_COUNT = 100;
public static final String PRODUCER_GROUP = "please_rename_unique_group_name";
public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876";
public static final String TOPIC = "TopicTest";
public static final String TAG = "TagA";
public static void main(String[] args) throws MQClientException, InterruptedException {
DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP);
producer.setNamesrvAddr(DEFAULT_NAMESRVADDR);
producer.start();
CountDownLatch countDownLatch = new CountDownLatch(MESSAGE_COUNT);
for (int i = 0; i < MESSAGE_COUNT; i++) {
try {
//创建消息
Message msg = new Message(TOPIC /* Topic */,
TAG /* Tag */,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
);
//异步发送消息,可以设置超时时间
producer.send(msg, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
countDownLatch.countDown();
System.out.println(JSON.toJSONString(sendResult) + "异步消息发送成功");
}
@Override
public void onException(Throwable e) {
countDownLatch.countDown();
System.out.println(JSON.toJSONString(e) + "异步消息发送失败");
}
}, 3000);
} catch (Exception e) {
e.printStackTrace();
Thread.sleep(1000);
}
}
//等待所有消息发布成功
countDownLatch.await();
producer.shutdown();
}
}
复制代码
2.3 发送单向消息
单向消息常用于吞吐量要求较高的场景并且能够运行数据丢失的场景,这个时候生产者直接向mq发送消息,不需要关注返回结果。
代码如下:
public class ProducerOneWay {
public static final int MESSAGE_COUNT = 1000;
public static final String PRODUCER_GROUP = "please_rename_unique_group_name";
public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876";
public static final String TOPIC = "OneWayTopicTest";
public static final String TAG = "TagA";
public static void main(String[] args) throws MQClientException, InterruptedException {
DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP);
producer.setNamesrvAddr(DEFAULT_NAMESRVADDR);
producer.start();
for (int i = 0; i < MESSAGE_COUNT; i++) {
try {
Message msg = new Message(TOPIC /* Topic */,
TAG /* Tag */,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
);
producer.sendOneway(msg);
} catch (Exception e) {
e.printStackTrace();
Thread.sleep(1000);
}
}
producer.shutdown();
}
}
复制代码
2.4 延迟消息
延迟消息就是在发送给mq后,消费者并不能立刻消费消息,而是要等消息超过延迟时间过后,才会被消费者看到。
延迟消息常常用在定时任务中,比如订单超过15分钟,需要被关闭掉这一需求就可以用延时队列实现。订单创建的时候,可以发送一条延时时间为15分钟的消息给订单检查服务,订单检查服务在15分钟后消费这条消息过后,会反查订单服务的订单状态,判断是否需要做关单处理。
代码如下:
package org.apache.rocketmq.example.quickstart;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.common.RemotingHelper;
public class ProducerSchedule {
public static final int MESSAGE_COUNT = 1000;
public static final String PRODUCER_GROUP = "please_rename_unique_group_name";
public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876";
public static final String TOPIC = "OneWayTopicTest";
public static final String TAG = "TagA";
public static void main(String[] args) throws MQClientException, InterruptedException {
DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP);
producer.setNamesrvAddr(DEFAULT_NAMESRVADDR);
producer.start();
for (int i = 0; i < MESSAGE_COUNT; i++) {
try {
Message msg = new Message(TOPIC /* Topic */,
TAG /* Tag */,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
);
//设置10秒后才能被消费者消费
msg.setDelayTimeLevel(3);
SendResult sendResult = producer.send(msg);
System.out.println(sendResult);
} catch (Exception e) {
e.printStackTrace();
Thread.sleep(1000);
}
}
producer.shutdown();
}
}
复制代码
2.5 发送批量消息
批量消息其实就是把多条消息组装成一个集合,然后统一发送
要求:
1.所有的消息都在同一个topic中,因为他们最后要放入到同一个messagequeue中的。
2.所有的消息总量不能超过1m。
public class ProducerBatch {
public static final int MESSAGE_COUNT = 50;
public static final String PRODUCER_GROUP = "please_rename_unique_group_name";
public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876";
public static final String TOPIC = "BatchTopicTest";
public static final String TAG = "TagA";
public static void main(String[] args) throws MQClientException, InterruptedException, MQBrokerException, RemotingException {
DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP);
producer.setNamesrvAddr(DEFAULT_NAMESRVADDR);
producer.start();
List<Message> messageList = new ArrayList<>();
for (int i = 0; i < MESSAGE_COUNT; i++) {
//组装msg集合
try {
Message msg = new Message(TOPIC /* Topic */,
TAG /* Tag */,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
);
messageList.add(msg);
} catch (Exception e) {
e.printStackTrace();
Thread.sleep(1000);
}
}
//批量发送msg集合
SendResult sendResult = producer.send(messageList);
System.out.println(sendResult);
producer.shutdown();
}
}
复制代码
2.6 顺序消息
2.6.1 全局顺序消息
全局顺序消息就是保证用户发送的消息是绝对有序的,所以这里我们只能有一个messagequeue,并且所有的消息都是放到这个messagequeue中,通过fifo保证顺序性。实现方式就是对应的topic设置一个messagequeue,并且生产者单线程往这个messagequeue生产消息,同时消费者单线程从这个messagequeue消费消息。
2.6.2 分区顺序性消息
比如订单有下单->物料->签收,这三个动作要求是有序的,所以对于同一个订单来说,需要保证是顺序的,
所以这个时候我们需要保证生产者是同步发送消息到mq中,并且对于一个订单应该进入到同一个messagequeue,而在消费的时候,消费者也应该是同步进行消费的。
生产者:
public class ProducerOrder {
public static final int MESSAGE_COUNT = 100;
public static final String PRODUCER_GROUP = "please_rename_unique_group_name";
public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876";
public static final String TOPIC = "OrderTopicTest";
public static void main(String[] args) throws MQClientException, InterruptedException {
DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP);
producer.setNamesrvAddr(DEFAULT_NAMESRVADDR);
List<Order> orderList = Arrays.asList(new Order(1, 110, "下单"),
new Order(2, 110, "物流"),
new Order(3, 110, "签收"),
new Order(4, 120, "下单"),
new Order(5, 120, "物流"),
new Order(6, 120, "签收"));
producer.start();
for (int i = 0; i < orderList.size(); i++) {
Integer orderId = orderList.get(i).getOrderId();
try {
//创建消息
Message msg = new Message(TOPIC /* Topic */,
JSON.toJSONString(orderList.get(i)).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
);
//分区顺序性消息,将同一类的message放入到同一个messagequeue中
producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
//这里这个arg就是下面传入的orderId
Integer orderId = (Integer) arg;
int index = orderId % mqs.size();
return mqs.get(index);
}
}, orderId);
} catch (Exception e) {
e.printStackTrace();
Thread.sleep(1000);
}
}
producer.shutdown();
}
}
复制代码
消费者:
public class ConsumerOrder {
public static final String CONSUMER_GROUP = "please_rename_unique_group_name_4";
public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876";
public static final String TOPIC = "OrderTopicTest";
public static void main(String[] args) throws InterruptedException, MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP);
consumer.setNamesrvAddr(DEFAULT_NAMESRVADDR);
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
consumer.subscribe(TOPIC, "*");
//消费者需要顺序消费
consumer.registerMessageListener(new MessageListenerOrderly() {
AtomicLong consumeTimes = new AtomicLong(0);
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
context.setAutoCommit(true);
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
this.consumeTimes.incrementAndGet();
if ((this.consumeTimes.get() % 2) == 0) {
return ConsumeOrderlyStatus.SUCCESS;
} else if ((this.consumeTimes.get() % 5) == 0) {
context.setSuspendCurrentQueueTimeMillis(3000);
//表示等一会,rocketmq可能会消费失败,这个时候mq会将其加入到重试队列中,返回这个参数表示,让mq等一会儿,不要加入到重试队列,防止乱序
return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
consumer.start();
System.out.printf("Consumer Started.%n");
}
}
复制代码
2.7 事务消息
事务消息其实是通过2pc的方式保证数据一定能够从生产者到达rocketmq。
他的实现流程是:
1.生产者先向rocketmq发送一个half消息,这里其实就是生产者生产的消息,只是不能被消费者看到。
2.当half消息发送成功后,会向生产者返回一个响应。
3.生产者根据本地事务执行情况向rocketmq发送commit或者rollback请求。
4.rocketmq让消息对消费者可见。
5.如果长时间未收到commit或者rollback,便会启动定时任务回查生产者,超过16次,便直接删除half消息。
生产者:
public class ProducerTransaction {
public static final int MESSAGE_COUNT = 100;
public static final String PRODUCER_GROUP = "please_rename_unique_group_name";
public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876";
public static final String TOPIC = "TopicTest";
public static final String TAG = "TagA";
public static void main(String[] args) throws MQClientException, InterruptedException {
TransactionMQProducer producer = new TransactionMQProducer(PRODUCER_GROUP);
producer.setNamesrvAddr(DEFAULT_NAMESRVADDR);
producer.start();
//新建一个线程池用于处理mq的回调
ExecutorService executorService = new ThreadPoolExecutor(5,5,3000, TimeUnit.MILLISECONDS,new ArrayBlockingQueue<>(10));
producer.setExecutorService(executorService);
producer.setTransactionListener(new );
for (int i = 0; i < MESSAGE_COUNT; i++) {
try {
//创建消息
Message msg = new Message(TOPIC /* Topic */,
TAG /* Tag */,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
);
//异步发送消息,可以设置超时时间
producer.send(msg, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println(JSON.toJSONString(sendResult) + "异步消息发送成功");
}
@Override
public void onException(Throwable e) {
System.out.println(JSON.toJSONString(e) + "异步消息发送失败");
}
}, 3000);
} catch (Exception e) {
e.printStackTrace();
Thread.sleep(1000);
}
}
producer.shutdown();
}
}
class MyTransactionListenerImpl implements TransactionListener {
private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>();
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
//执行本地事务逻辑,根据本地事务状态确定二阶段是提交还是回滚
return LocalTransactionState.UNKNOW;
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
//查询本地单据状态,判断单据状态,针对的是第5步,mq回查
return LocalTransactionState.COMMIT_MESSAGE;
}
}
3.参考资料
[1] RocketMQ(二):原生API快速入门-优快云博客
[2] 基础概念 | RocketMQ
[3] 杨开元. RocketMQ实战与原理解析
[4] RocketMQ 核心原理解析 - SH的全栈笔记 - 掘金小册
[5] 儒猿课堂 从 0 开始带你成为RocketMQ高手https://apppukyptrl1086.pc.xiaoe-tech.com/p/t_pc/course_pc_detail/image_text/i_5d8b8b63e3215_gc02doDN?product_id=p_5d887e7ea3adc_KDm4nxCm&content_app_id=&type=6