消息消费模式
RocketMQ 有两种消息消费模式,分别是集群模式和广播模式。
消费模式由消费者来决定,默认为集群模式,可通过设置改变消费模式:
// 设置为广播消费模式
consumer.setMessageModel(MessageModel.BROADCASTING);
// 设置为集群消费模式
consumer.setMessageModel(MessageModel.CLUSTERING);
同一消费者组内要保证消费模式相同,否则会出现消息丢失等问题。
集群模式
集群模式下,消费者集群化部署,RocketMQ认为同一条消息只需要被集群中同一消费组内的任意一个消费者消费即可。
特点:
- 每条消息只需要被每个消费者组处理一次, broker只会把消息发给同一消费者组中的一个消费者。
- 消息重投时,不能保证路由到同一台机器上。
- 消费状态由broker维护。
广播模式
当使用广播模式时,broker会将每条消息推送给集群内在线的所有消费者,保证消息至少被每台机器消费一次。
特点:
- 消费进度由consumer维护。
- 保证消息至少被每台机器消费一次。
- 消费失败的消息不会重投。
消息发送方式
同步发送
producer发送消息时同步等待,直到broker返回发送结果,可以保证消息投递一定到达。
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
DefaultMQProducer producer = new DefaultMQProducer("group001");
// 设置namesrvAddr
producer.setNamesrvAddr("localhost:9876");
// 启动生产者
producer.start();
// 发送消息
Message message = new Message("topic001", "sync message".getBytes());
SendResult sendResult = producer.send(message);
System.out.println("sendResult: " + sendResult);
// 关闭
producer.shutdown();
System.out.println("关闭生产者");
}
异步发送
producer发送消息时不等待,直接返回,消息投递的结果以异步的方式在注册的监听器中返回。
如果想要快速发送消息,且不想丢失消息,可以使用异步消息。
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException {
DefaultMQProducer producer = new DefaultMQProducer("group001");
// 指定namesrv
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
Message message = new Message("topic001", "async message".getBytes());
producer.send(message, new SendCallback() {
public void onSuccess(SendResult sendResult) {
System.out.println("ok");
System.out.println("sendResult: " + sendResult);
}
public void onException(Throwable throwable) {
System.out.println("error");
System.out.println(throwable.getMessage());
}
});
// 睡眠3秒
TimeUnit.SECONDS.sleep(3);
producer.shutdown();
System.out.println("已经停机");
}
单向发送
producer发送消息时不等待,直接返回,也无法接收消息投递的结果。
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException {
DefaultMQProducer producer = new DefaultMQProducer("group001");
// 指定namesrv
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
Message message = new Message("topic001", "oneway message".getBytes());
producer.sendOneway(message);
producer.shutdown();
System.out.println("已经停机");
}
批量发送消息
RocketMQ支持多条消息打包一起发送,减少网络开销提高效率。
- 批量发送消息要求所有消息必须同一topic,并且有相同的消息配置
- 批量发送不支持延迟消息
- 批量发送不支持重试消息
- 官方建议一个批量消息最好不要超过1MB大小
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
DefaultMQProducer producer = new DefaultMQProducer("group001");
// 指定namesrv
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
List<Message> msgs = new ArrayList<Message>();
for (int i = 0; i < 5; i++) {
Message message = new Message("topic001", ("collection message " + i).getBytes());
msgs.add(message);
}
SendResult sendResult = producer.send(msgs);
System.out.println("sendResult: " + sendResult);
producer.shutdown();
System.out.println("关闭生产者");
}
消息过滤
TAG过滤
RocketMQ支持使用tag过滤消息。在同一topic中,订阅想要的tag从而过滤不需要的消息。
-
在producer发送消息时,为message设置tag:
Message message = new Message("topic001", "TAG-A", "tag message".getBytes()); -
在consumer中订阅tag:
// 订阅topic,指定tag consumer.subscribe("topic001", "TAG-A||TAG-B"); // "*"表示全部消息
SQL过滤
除了tag过滤,RocketMQ还支持SQL92过滤方式,不够要使用这种方式过滤的话,需要先在配置文件 broker.conf 中开启一下配置。
enablePropertyFilter=true
然后在启动broker时加载指定的配置文件
../bin/mqbroker -n 192.168.1.6:9876 -c broker.conf
-
在producer中设置userProperties参数
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException { DefaultMQProducer producer = new DefaultMQProducer("group001"); // 设置namesrvAddr producer.setNamesrvAddr("localhost:9876"); // 启动生产者 producer.start(); // 发送消息 for (int i = 0; i < 10; i++) { Message message = new Message("topic001", "TAG-A", "KEY-01", ("sql message " + i).getBytes()); message.putUserProperty("age", String.valueOf(i)); SendResult sendResult = producer.send(message); System.out.println("sendResult: " + sendResult); } // 关闭 producer.shutdown(); System.out.println("关闭生产者"); } -
在consumer中使用sql过滤消息
public static void main(String[] args) throws MQClientException { DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_group_sql"); // 指定namesrv consumer.setNamesrvAddr("localhost:9876"); // 订阅topic // 创建MessageSelector,根据sql过滤消息 consumer.subscribe("topic001", MessageSelector.bySql("age >= 3 and age <= 7")); consumer.registerMessageListener(new MessageListenerConcurrently() { public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) { for (MessageExt messageExt : list) { System.out.println("message: " + new String(messageExt.getBody())); System.out.println("messageExt: " + messageExt); } return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); consumer.start(); System.out.println("consumer by sql started ..."); }
语法
RocketMQ只定义了一些基本的语法来支持这个功能。 你也可以很容易地扩展它.
- 数字比较, 像
>,>=,<,<=,BETWEEN,=; - 字符比较, 像
=,<>,IN; IS NULL或者IS NOT NULL;- 逻辑运算
AND,OR,NOT;
常量类型
- 数字, 像123, 3.1415;
- 字符串, 像‘abc’,必须使用单引号;
NULL, 特殊常数;- 布尔常量,
TRUE或FALSE;
延迟消息
RocketMQ支持使用messageDelayLevel设置延迟投递消息。
默认配置是:
messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
这个配置项配置了从1级开始,各级的延迟时间。
在broker.conf中可以修改其配置,但是并不建议修改,因为源码中也有许多地方用到延迟级别。
支持的时间单位分别有:
| 时间单位 | 说明 |
|---|---|
| s | 秒 |
| m | 分 |
| h | 时 |
| d | 天 |
使用
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
DefaultMQProducer producer = new DefaultMQProducer("group001");
// 设置namesrvAddr
producer.setNamesrvAddr("localhost:9876");
// 启动生产者
producer.start();
// 发送延时消息
Message message = new Message("topic001", "TAG-A", "KEY-01", "delay message".getBytes());
// 默认延时配置
// 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
message.setDelayTimeLevel(3); // 延时10秒
SendResult sendResult = producer.send(message);
System.out.println("sendResult: " + sendResult);
// 关闭
producer.shutdown();
System.out.println("关闭生产者");
}
顺序消费
在RocketMQ中,一个topic对应有多个MessageQueue,每个MessageQueue是一个队列,先天支持FIRO模型,所以只需要把消息投递到同一个MessageQueue中,再由一个线程去消费,就能保证顺序消费了。
-
具体实现-消息投递
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException { DefaultMQProducer producer = new DefaultMQProducer("group_orderly"); // 设置namesrvAddr producer.setNamesrvAddr("localhost:9876"); // 启动生产者 producer.start(); for (int i = 0; i < 20; i++) { // 发送消息 Message message = new Message("topic_orderly", "TAG-A", "KEY-01", ("orderly message " + i).getBytes()); SendResult sendResult = producer.send( // 消息 message, // 消息队列选择器 new MessageQueueSelector() { public MessageQueue select(List<MessageQueue> list, Message message, Object o) { // 所有消息都发送到第一个MessageQueue中 return list.get(0); } }, // 自定义参数 null); System.out.println("sendResult: " + sendResult); } // 关闭 producer.shutdown(); System.out.println("关闭生产者"); } -
具体实现-消息消费
public static void main(String[] args) throws MQClientException { DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_group_orderly"); // 指定namesrv consumer.setNamesrvAddr("localhost:9876"); // 订阅topic consumer.subscribe("topic_orderly", "*"); // 注册顺序消费监听器 consumer.registerMessageListener(new MessageListenerOrderly() { public ConsumeOrderlyStatus consumeMessage(List<MessageExt> list, ConsumeOrderlyContext consumeOrderlyContext) { for (MessageExt messageExt : list) { System.out.println("message: " + new String(messageExt.getBody()) + " thread: " + Thread.currentThread().getName()); } return ConsumeOrderlyStatus.SUCCESS; } }); consumer.start(); System.out.println("consumer by orderly started ..."); }
重试机制
producer 发送重试
默认配置:
public DefaultMQProducer(String namespace, String producerGroup, RPCHook rpcHook) {
this.log = ClientLogger.getLog();
this.createTopicKey = "TBW102";
this.defaultTopicQueueNums = 4;
// timeout for sending message
this.sendMsgTimeout = 3000;
this.compressMsgBodyOverHowmuch = 4096;
// 同步发送失败重试次数
this.retryTimesWhenSendFailed = 2;
// 异步发送失败重试次数
this.retryTimesWhenSendAsyncFailed = 2;
// 是否向其他broker发送请求
this.retryAnotherBrokerWhenNotStoreOK = false;
this.maxMessageSize = 4194304;
this.traceDispatcher = null;
this.namespace = namespace;
this.producerGroup = producerGroup;
this.defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook);
}
使用:
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
DefaultMQProducer producer = new DefaultMQProducer("group_retry");
// 设置namesrvAddr
producer.setNamesrvAddr("localhost:9876");
// 发送超时时间,默认3000毫秒
// this.sendMsgTimeout = 3000;
producer.setSendMsgTimeout(1000);
// 同步发送失败重试次数,默认2次
// this.retryTimesWhenSendFailed = 2;
producer.setRetryTimesWhenSendFailed(1);
// 异步发送失败重试次数,默认2次
// this.retryTimesWhenSendAsyncFailed = 2;
producer.setRetryTimesWhenSendAsyncFailed(1);
// 失败时是否向其他broker发送,默认false
// this.retryAnotherBrokerWhenNotStoreOK = false;
producer.setRetryAnotherBrokerWhenNotStoreOK(true);
// 启动生产者
producer.start();
// 发送消息
Message message = new Message("topic_retry", "TAG-RETRY", "retry message".getBytes());
SendResult sendResult = producer.send(message);
System.out.println("timestamp: " + new Date() + " sendResult: " + sendResult);
// 关闭
producer.shutdown();
System.out.println("关闭生产者");
}
consumer 消费重试
设置消费超时时间
// 消费超时时间,单位:分钟,默认15分钟
// this.consumeTimeout = 15L;
consumer.setConsumeTimeout(10);
若要重试消费的话,需要在监听器中返回RECONSUME_LATER。
consumer.registerMessageListener(new MessageListenerConcurrently() {
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt messageExt : list) {
System.out.println("timestamp: " + new Date() + " messageId: " + messageExt.getMsgId() + " body: " + new String(messageExt.getBody()));
}
// CONSUME_SUCCESS, 消费成功
// RECONSUME_LATER; 稍后再试
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
});
完整消费者案例:
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_retry");
// 指定namesrv
consumer.setNamesrvAddr("localhost:9876");
// 消费超时时间,单位:分钟,默认15分钟
// this.consumeTimeout = 15L;
consumer.setConsumeTimeout(10);
// 订阅topic
consumer.subscribe("topic_retry", "TAG-RETRY");
consumer.registerMessageListener(new MessageListenerConcurrently() {
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt messageExt : list) {
System.out.println("timestamp: " + new Date() + " messageId: " + messageExt.getMsgId() + " body: " + new String(messageExt.getBody()));
}
// CONSUME_SUCCESS, 消费成功
// RECONSUME_LATER; 稍后再试
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
});
consumer.start();
System.out.println("consumer by retry started ...");
}
broker 消息重投
只有在MessageModel.CLUSTERING集群模式下,broker才会进行消息重投,MessageModel.BROADCASTING广播模式下broker不会重投。
重投时间间隔使用上面说的messageDelayLevel。
事务消息
RocketMQ 4.3+提供分布事务功能,通过 RocketMQ 事务消息能达到分布式事务的最终一致。
事务消息逻辑:
Half Message:生产者先发送一个半消息(Half Message),broker收到半消息后,会将消息存入RMQ_SYS_TRANS_HALF_TOPIC消息消费队列中,半消息发送成功后,开始执行本地事务。
**检查事务状态:**Broker会开启一个定时任务,消费RMQ_SYS_TRANS_HALF_TOPIC队列中的消息,每次执行任务会向消息发送者确认事务执行状态(提交、回滚、未知),如果是未知,等待下一次回调。
超时:如果超过回查次数,默认回滚消息。
TransactionListener
-
executeLocalTransaction
半消息发送成功后,触发本方法执行本地事务。
-
checkLocalTransaction
检查本地事务状态
事务消息使用
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
TransactionMQProducer producer = new TransactionMQProducer("group_transaction");
// 设置namesrvAddr
producer.setNamesrvAddr("localhost:9876");
final CountDownLatch latch = new CountDownLatch(1);
// 设置事务监听器
producer.setTransactionListener(new TransactionListener() {
// 执行本地事务
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
System.out.println("executeLocalTransaction...");
System.out.println("msg: " + new String(message.getBody()));
System.out.println("transactionId: " + message.getTransactionId());
try {
TimeUnit.SECONDS.sleep(60);
} catch (InterruptedException e) {
e.printStackTrace();
}
latch.countDown();
// COMMIT_MESSAGE, 提交消息
// ROLLBACK_MESSAGE, 回滚消息
// UNKNOW; 不确定,等会再来
return LocalTransactionState.COMMIT_MESSAGE;
}
// 检查本地事务是否执行完毕
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
System.out.println("msg: " + new String(messageExt.getBody()));
System.out.println("transactionId: " + messageExt.getTransactionId());
if (latch.getCount() > 0) {
System.out.println("checkLocalTransaction: UNKNOW");
return LocalTransactionState.UNKNOW;
}
System.out.println("checkLocalTransaction: COMMIT_MESSAGE");
return LocalTransactionState.COMMIT_MESSAGE;
}
});
// 启动生产者
producer.start();
Message message = new Message("topic_transaction", "TAG-A", "KEY-01", "transaction message".getBytes());
SendResult sendResult = producer.sendMessageInTransaction(message, null);
System.out.println("sendResult: " + sendResult);
// 关闭
producer.shutdown();
System.out.println("关闭生产者");
}
总结
以上所有使用到的案例源代码链接: