博文目录
<dependency>
<groupId>org.apache.rocketmqgroupId>
<artifactId>rocketmq-clientartifactId>
<version>4.7.1version>
dependency>
生产者和消费者都需要依赖NameServer才能运行
生产者发送的三种方式
消费者消费消息有两种模式,一种是消费者主动去Broker上拉取消息的拉模式,另一种是消费者等待Broker把消息推送过来的推模式
通常情况下,用推模式比较简单。实际上RocketMQ的推模式也是由拉模式封装出来的。
4.7.1版本中DefaultMQPullConsumerImpl这个消费者类已标记为过期,但是还是可以使用的。替换的类是DefaultLitePullConsumerImpl。
package com.mrathena.rocket.mq.simple;
import com.mrathena.toolkit.IdKit;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.common.RemotingHelper;
import org.junit.Test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@Slf4j
public class ProducerTest {
private static final String GROUP = "producer";
private static final String ADDRESS = "116.62.162.48:9876";
private static final String TOPIC = "topic";
private static final String TAG = "tag";
private static final String KEY = "key";
/**
* 同步发送
*/
@Test
public void syncSend() {
try {
DefaultMQProducer producer = new DefaultMQProducer(GROUP);
producer.setNamesrvAddr(ADDRESS);
producer.start();
Message message = new Message(TOPIC, TAG, KEY, IdKit.getSerialNo().getBytes(RemotingHelper.DEFAULT_CHARSET));
// 同步传递消息,消息会发给集群中的一个Broker节点。
SendResult result = producer.send(message);
log.info("{}", result);
log.info("{}", producer.send(new Message(TOPIC, TAG, KEY, IdKit.getSerialNo().getBytes())));
log.info("{}", producer.send(new Message(TOPIC, TAG, KEY, IdKit.getSerialNo().getBytes())));
log.info("{}", producer.send(new Message(TOPIC, TAG, KEY, IdKit.getSerialNo().getBytes())));
producer.shutdown();
} catch (Throwable cause) {
cause.printStackTrace();
}
}
/**
* 异步发送
*/
@Test
public void asyncSend() {
try {
DefaultMQProducer producer = new DefaultMQProducer(GROUP);
producer.setNamesrvAddr(ADDRESS);
producer.start();
producer.setRetryTimesWhenSendAsyncFailed(0);
Message message = new Message(TOPIC, TAG, KEY, IdKit.getSerialNo().getBytes(RemotingHelper.DEFAULT_CHARSET));
// 异步发送消息, 没有返回值, 但是有结果回调, 需要阻塞等待异步回调
CountDownLatch countDownLatch = new CountDownLatch(1);
producer.send(message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
log.info("{}", sendResult);
countDownLatch.countDown();
}
@Override
public void onException(Throwable e) {
log.error("", e);
countDownLatch.countDown();
}
});
countDownLatch.await(1, TimeUnit.MINUTES);
producer.shutdown();
} catch (Throwable cause) {
cause.printStackTrace();
}
}
/**
* 单向发送
*/
@Test
public void oneWaySend() {
try {
DefaultMQProducer producer = new DefaultMQProducer(GROUP);
producer.setNamesrvAddr(ADDRESS);
producer.start();
Message message = new Message(TOPIC, TAG, KEY, IdKit.getSerialNo().getBytes(RemotingHelper.DEFAULT_CHARSET));
// 单向发送, 没有返回值,也没有回调。就是只管把消息发出去就行了。
producer.sendOneway(message);
producer.shutdown();
} catch (Throwable cause) {
cause.printStackTrace();
}
}
}
package com.mrathena.rocket.mq.simple;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.consumer.*;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.common.protocol.heartbeat.MessageModel;
import org.junit.Test;
import java.util.*;
import java.util.concurrent.TimeUnit;
@Slf4j
public class ConsumerTest {
private static final String GROUP = "consumer";
private static final String ADDRESS = "116.62.162.48:9876";
private static final String TOPIC = "topic";
/**
* 推模式消费
*/
@Test
public void pushTest() {
try {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(GROUP);
consumer.setNamesrvAddr(ADDRESS);
// 订阅 topic 下的全部 tab
consumer.subscribe(TOPIC, "*");
// BROADCASTING:广播模式,把消息发给了所有订阅了对应主题的消费者,不管消费者是不是同一个消费者组, CLUSTERING:集群模式(默认值),每一条消息只会被同一个消费者组中的一个实例消费
consumer.setMessageModel(MessageModel.CLUSTERING);
// CONSUME_FROM_LAST_OFFSET:从最新的偏移值开始消费(默认值), CONSUME_FROM_FIRST_OFFSET:从队列最开始的偏移值开始消费, CONSUME_FROM_TIMESTAMP:从指定的时间戳处开始消费
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
// yyyyMMddHHmmss: 当选择从指定的时间戳处开始消费时, 需要指定该时间戳
// consumer.setConsumeTimestamp("20181109221800");
// 使用并发方式从多个MessageQueue中取数据的方式监听
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
System.out.println();
for (MessageExt msg : msgs) {
log.info("收到消息: {}", new String(msg.getBody()));
}
// 返回消费成功, 还可以是 RECONSUME_LATER:稍后重新消费
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
TimeUnit.DAYS.sleep(1);
} catch (Throwable cause) {
cause.printStackTrace();
}
}
/**
* pull 模式时, 记录某主题下各队列的消费偏移量
*/
private static final Map<MessageQueue, Long> PULL_MODE_OFFSET_MAP = new HashMap<>();
/**
* 拉模式消费
*/
@Test
public void pullTest() {
try {
// 已过期, 使用 org.apache.rocketmq.client.consumer.DefaultLitePullConsumer 替代
DefaultMQPullConsumer consumer = new DefaultMQPullConsumer(GROUP);
consumer.setNamesrvAddr(ADDRESS);
consumer.start();
// 拿到该主题下的所有消息队列
Set<MessageQueue> mqSet = consumer.fetchSubscribeMessageQueues(TOPIC);
log.info("拿到主题下的所有消息队列: {}", mqSet);
// 遍历拿到的所有消息队列, 一个个的处理
for (MessageQueue mq : mqSet) {
System.out.println();
log.info("开始处理消息队列: {}", mq);
// java保留标签的唯一目的是中断嵌套循环,所以标签的位置在循环语句上面,之间什么都不写
// 使用break+标签时,会跳过标签下面的循环体(尽管标签是在循环体上面)
// 使用continue+标签时,会重新进入标签下面的循环体
flag:
while (true) {
// 从本地缓存中获取该队列的消息偏移量, 可以使用redis等分布式中间件来存
long offset = getMessageQueueOffset(mq);
// 从该消息队列下获取最大2条消息
PullResult result = consumer.pullBlockIfNotFound(mq, null, offset, 3);
log.info("从该消息队列下获取最大3条消息: {}", result);
// 记录该消息队列本次消费到的偏移, 下次获取时消息时从这个点继续走起, 而不是从起点重来
setMessageQueueOffset(mq, result.getNextBeginOffset());
// 判断拉取状态, 采取不同的处理方式
switch (result.getPullStatus()) {
case FOUND:
// 从拉取结果中获取消息
List<MessageExt> messageList = result.getMsgFoundList();
for (MessageExt message : messageList) {
log.info("收到消息: {}", new String(message.getBody()));
}
break;
case NO_MATCHED_MSG:
// 没有匹配的消息
break;
case NO_NEW_MSG:
// 没有新消息, 则跳过flag这个标签下面的循环体
break flag;
case OFFSET_ILLEGAL:
// 偏移量非法
break;
default:
break;
}
}
}
consumer.shutdown();
TimeUnit.DAYS.sleep(1);
} catch (Throwable cause) {
cause.printStackTrace();
}
}
private static long getMessageQueueOffset(MessageQueue mq) {
Long offset = PULL_MODE_OFFSET_MAP.get(mq);
if (offset != null) {
return offset;
}
return 0;
}
private static void setMessageQueueOffset(MessageQueue mq, long offset) {
PULL_MODE_OFFSET_MAP.put(mq, offset);
}
/**
* 定时拉模式消费(间隔延迟)
*/
@Test
public void schedulePullTest() {
try {
MQPullConsumerScheduleService scheduleService = new MQPullConsumerScheduleService(GROUP);
// NameServer
scheduleService.getDefaultMQPullConsumer().setNamesrvAddr(ADDRESS);
scheduleService.getDefaultMQPullConsumer().setConsumerGroup(GROUP);
scheduleService.registerPullTaskCallback(TOPIC, new PullTaskCallback() {
/**
* 该方法没有做遍历, 所以可以认为是每秒消费3条
*/
@Override
public void doPullTask(MessageQueue mq, PullTaskContext context) {
System.out.println();
log.info("启动新的拉取任务");
MQPullConsumer consumer = context.getPullConsumer();
try {
// 通过队列获取该消费者消费的偏移量
long offset = consumer.fetchConsumeOffset(mq, false);
offset = offset < 0 ? 0 : offset;
// 主动获取3条消息
PullResult result = consumer.pull(mq, "*", offset, 3);
log.info("从该消息队列下获取最大3条消息: {}", result);
switch (result.getPullStatus()) {
case FOUND:
// 从拉取结果中获取消息
List<MessageExt> messageList = result.getMsgFoundList();
for (MessageExt message : messageList) {
log.info("收到消息: {}", new String(message.getBody()));
}
break;
case NO_MATCHED_MSG:
break;
case NO_NEW_MSG:
case OFFSET_ILLEGAL:
break;
default:
break;
}
// 更新该消费者在该队列消费的偏移量
consumer.updateConsumeOffset(mq, result.getNextBeginOffset());
// 设置到下次启动拉取任务的间隔时间
context.setPullNextDelayTimeMillis(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
});
scheduleService.start();
TimeUnit.DAYS.sleep(1);
} catch (Throwable cause) {
cause.printStackTrace();
}
}
public static volatile boolean running = true;
/**
* 新的拉取模式消费
* 订阅模式, 自动安排分区, 自动负载均衡
*/
@Test
public void litePullSubscribeTest() {
try {
DefaultLitePullConsumer consumer = new DefaultLitePullConsumer(GROUP);
consumer.setNamesrvAddr(ADDRESS);
consumer.subscribe(TOPIC, "*");
consumer.start();
try {
while (running) {
List<MessageExt> messageList = consumer.poll();
System.out.println();
log.info("尝试拉取消息: {}", messageList);
for (MessageExt message : messageList) {
log.info("消息内容: {}", new String(message.getBody()));
}
}
} finally {
consumer.shutdown();
}
} catch (Throwable cause) {
cause.printStackTrace();
}
}
/**
* 新的拉取模式消费
* 分配模式, 手动指定消费分区, 不再支持负载均衡
* 多次assign, 后面的会覆盖前面的效果
*/
@Test
public void litePullAssignTest() {
try {
DefaultLitePullConsumer consumer = new DefaultLitePullConsumer(GROUP);
consumer.setNamesrvAddr(ADDRESS);
// 设置不自动提交(默认自动)
consumer.setAutoCommit(false);
consumer.start();
// 获取该主题下的消息队列
Collection<MessageQueue> mqSet = consumer.fetchMessageQueues(TOPIC);
List<MessageQueue> list = new ArrayList<>(mqSet);
// 指定消费全队列中的一半
List<MessageQueue> assignList = new ArrayList<>();
for (int i = 0; i < list.size() / 2; i++) {
assignList.add(list.get(i));
}
// 指定消费这些消息队列
consumer.assign(assignList);
// 显式指定mq的offset,并不会提交到到broker,需要同consumer commit一次才能生效
consumer.seek(assignList.get(0), 5);
try {
while (running) {
List<MessageExt> messageList = consumer.poll();
System.out.println();
log.info("尝试拉取消息: {}", messageList);
for (MessageExt message : messageList) {
log.info("消息内容: {}", new String(message.getBody()));
}
consumer.commitSync();
}
} finally {
consumer.shutdown();
}
} catch (Throwable cause) {
cause.printStackTrace();
}
}
}
RocketMQ保证的是消息的局部有序,而不是全局有序。所以这就涉及到了RocketMQ消息有序的原理。要保证最终消费到的消息是有序的,需要从Producer、Broker、Consumer三个步骤都保证消息有序才行。
首先在发送者端:在默认情况下,消息发送者会采取Round Robin轮询方式把消息发送到不同的MessageQueue(分区队列),而消费者消费的时候也从多个MessageQueue上拉取消息,这种情况下消息是不能保证顺序的。而只有当一组有序的消息发送到同一个MessageQueue上时,才能利用MessageQueue先进先出的特性保证这一组消息有序。
而Broker中一个队列内的消息是可以保证有序的。
然后在消费者端:消费者会从多个消息队列上去拿消息。这时虽然每个消息队列上的消息是有序的,但是多个队列之间的消息仍然是乱序的。消费者端要保证消息有序,就需要按队列一个一个来取消息,即取完一个队列的消息后,再去取下一个队列的消息。而给consumer注入的MessageListenerOrderly对象,在RocketMQ内部就会通过锁队列的方式保证消息是一个一个队列来取的。MessageListenerConcurrently这个消息监听器则不会锁队列,每次都是从多个Message中取一批数据(默认不超过32条)。因此也无法保证消息有序。
package com.mrathena.rocket.mq.order;
import com.mrathena.toolkit.IdKit;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.MessageQueueSelector;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageQueue;
import org.junit.Test;
import java.util.List;
@Slf4j
public class ProducerTest {
private static final String GROUP = "producer";
private static final String ADDRESS = "116.62.162.48:9876";
private static final String TOPIC = "topic";
private static final String TAG = "tag";
private static final String KEY = "key";
@Test
public void specificMessageQueueSend() {
try {
DefaultMQProducer producer = new DefaultMQProducer(GROUP);
producer.setNamesrvAddr(ADDRESS);
producer.start();
// 假如有8个用户(大约每个队列会进两个用户的消息,用于模拟单用户消息有序,用户间消息可能无序的情况), 每个用户有5条消息
for (int i = 0; i < 8; i++) {
for (int j = 1; j <= 5; j++) {
Message message = new Message(TOPIC, TAG, KEY, (i + ":" + j).getBytes());
// 没条消息按照某种规律放到指定的消息队列中
log.info("{}", producer.send(message, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
// 这里的arg就是外面传进来的i, 相当于第i个用户, 把第i个用户放到第i个消息队列中
int index = ((Integer) arg) % mqs.size();
return mqs.get(index);
}
}, i));
}
}
Message message = new Message(TOPIC, TAG, KEY, IdKit.getSerialNo().getBytes());
SendResult result = producer.send(message);
log.info("{}", result);
log.info("{}", producer.send(new Message(TOPIC, TAG, KEY, IdKit.getSerialNo().getBytes())));
log.info("{}", producer.send(new Message(TOPIC, TAG, KEY, IdKit.getSerialNo().getBytes())));
log.info("{}", producer.send(new Message(TOPIC, TAG, KEY, IdKit.getSerialNo().getBytes())));
producer.shutdown();
} catch (Throwable cause) {
cause.printStackTrace();
}
}
}
package com.mrathena.rocket.mq.order;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.common.protocol.heartbeat.MessageModel;
import org.junit.Test;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Slf4j
public class ConsumerTest {
private static final String GROUP = "consumer";
private static final String ADDRESS = "116.62.162.48:9876";
private static final String TOPIC = "topic";
/**
* 局部有序测试, 两个同时开启
*/
@Test
public void orderTest() {
try {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(GROUP);
consumer.setNamesrvAddr(ADDRESS);
consumer.subscribe(TOPIC, "*");
// 使用顺序方式从多个MessageQueue中取数据的方式监听(取完一个队列再取下一个队列)
// 不能使用 MessageListenerConcurrently 这种并发方式, 这样拿出来不是顺序的, 但是拿的快
// MessageListenerOrderly 顺序方式, 可以看到 单用户消息有序,用户间消息可能无序, 因为各用户间没有关联,互不影响,无需搞全局有序
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
context.setAutoCommit(true);
for (MessageExt msg : msgs) {
log.info("收到消息: {}", new String(msg.getBody()));
}
// SUSPEND_CURRENT_QUEUE_A_MOMENT:挂起当前队列一会儿(我猜是先走下一个队列?), SUCCESS:消费成功
return ConsumeOrderlyStatus.SUCCESS;
}
});
consumer.start();
TimeUnit.DAYS.sleep(1);
} catch (Throwable cause) {
cause.printStackTrace();
}
}
/**
* 局部有序测试, 两个同时开启
*/
@Test
public void orderTestTwo() {
try {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(GROUP);
consumer.setNamesrvAddr(ADDRESS);
consumer.subscribe(TOPIC, "*");
// 使用顺序方式从多个MessageQueue中取数据的方式监听(取完一个队列再取下一个队列)
// 不能使用 MessageListenerConcurrently 这种并发方式, 这样拿出来不是顺序的, 但是拿的快
// MessageListenerOrderly 顺序方式, 可以看到 单用户消息有序,用户间消息可能无序, 因为各用户间没有关联,互不影响,无需搞全局有序
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
context.setAutoCommit(true);
for (MessageExt msg : msgs) {
log.info("收到消息: {}", new String(msg.getBody()));
}
// SUSPEND_CURRENT_QUEUE_A_MOMENT:挂起当前队列一会儿(我猜是先走下一个队列?), SUCCESS:消费成功
return ConsumeOrderlyStatus.SUCCESS;
}
});
consumer.start();
TimeUnit.DAYS.sleep(1);
} catch (Throwable cause) {
cause.printStackTrace();
}
}
}
广播消息并没有特定的消息消费者样例,这是因为这涉及到消费者的集群消费模式。在集群状态(MessageModel.CLUSTERING)下,每一条消息只会被同一个消费者组中的一个实例消费到(这跟kafka和rabbitMQ的集群模式是一样的)。而广播模式则是把消息发给了所有订阅了对应主题的消费者,而不管消费者是不是同一个消费者组。
package com.mrathena.rocket.mq.broadcast;
import com.mrathena.toolkit.IdKit;
import lombok.extern.slf4j.Slf4j;
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;
import org.junit.Test;
@Slf4j
public class ProducerTest {
private static final String GROUP = "producer";
private static final String ADDRESS = "116.62.162.48:9876";
private static final String TOPIC = "topic";
private static final String TAG = "tag";
private static final String KEY = "key";
/**
* 同步发送
*/
@Test
public void syncSend() {
try {
DefaultMQProducer producer = new DefaultMQProducer(GROUP);
producer.setNamesrvAddr(ADDRESS);
producer.start();
Message message = new Message(TOPIC, TAG, KEY, IdKit.getSerialNo().getBytes(RemotingHelper.DEFAULT_CHARSET));
SendResult result = producer.send(message);
log.info("{}", result);
log.info("{}", producer.send(new Message(TOPIC, TAG, KEY, IdKit.getSerialNo().getBytes())));
log.info("{}", producer.send(new Message(TOPIC, TAG, KEY, IdKit.getSerialNo().getBytes())));
log.info("{}", producer.send(new Message(TOPIC, TAG, KEY, IdKit.getSerialNo().getBytes())));
producer.shutdown();
} catch (Throwable cause) {
cause.printStackTrace();
}
}
}
package com.mrathena.rocket.mq.broadcast;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.common.protocol.heartbeat.MessageModel;
import org.junit.Test;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Slf4j
public class ConsumerTest {
private static final String GROUP = "consumer";
private static final String ADDRESS = "116.62.162.48:9876";
private static final String TOPIC = "topic";
/**
* 广播模式消费, 两个同时开启
*/
@Test
public void broadcastingTest() {
try {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(GROUP);
consumer.setNamesrvAddr(ADDRESS);
consumer.subscribe(TOPIC, "*");
// BROADCASTING:广播模式,把消息发给了所有订阅了对应主题的消费者,不管消费者是不是同一个消费者组, CLUSTERING:集群模式(默认值),每一条消息只会被同一个消费者组中的一个实例消费
consumer.setMessageModel(MessageModel.BROADCASTING);
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
System.out.println();
for (MessageExt msg : msgs) {
log.info("收到消息: {}", new String(msg.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
TimeUnit.DAYS.sleep(1);
} catch (Throwable cause) {
cause.printStackTrace();
}
}
/**
* 广播模式消费, 两个同时开启
*/
@Test
public void broadcastingTestTwo() {
try {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(GROUP);
consumer.setNamesrvAddr(ADDRESS);
consumer.subscribe(TOPIC, "*");
consumer.setMessageModel(MessageModel.BROADCASTING);
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
System.out.println();
for (MessageExt msg : msgs) {
log.info("收到消息: {}", new String(msg.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
TimeUnit.DAYS.sleep(1);
} catch (Throwable cause) {
cause.printStackTrace();
}
}
}
延迟消息实现的效果就是在调用producer.send方法后,消息并不会立即发送出去,而是会等一段时间再发送出去。这是RocketMQ特有的一个功能。
延迟时间的设置就是在Message消息对象上设置一个延迟级别message.setDelayTimeLevel(3);
开源版本的RocketMQ中,对延迟消息并不支持任意时间的延迟设定(商业版本中支持),而是只支持18个固定的延迟级别,1到18分别对应messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h。这从哪里看出来的?其实从rocketmq-console控制台就能看出来。而这18个延迟级别也支持自行定义,不过一般情况下最好不要自定义修改。
package com.mrathena.rocket.mq.delay;
import com.mrathena.toolkit.IdKit;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.junit.Test;
@Slf4j
public class ProducerTest {
private static final String GROUP = "producer";
private static final String ADDRESS = "116.62.162.48:9876";
private static final String TOPIC = "topic";
private static final String TAG = "tag";
private static final String KEY = "key";
/**
* 延迟发送
*/
@Test
public void delaySend() {
try {
DefaultMQProducer producer = new DefaultMQProducer(GROUP);
producer.setNamesrvAddr(ADDRESS);
producer.start();
Message message = new Message(TOPIC, TAG, KEY, IdKit.getSerialNo().getBytes());
// 延迟消息实现的效果就是在调用producer.send方法后,消息并不会立即发送出去,而是会等一段时间再发送出去。这是RocketMQ特有的一个功能。
// 开源版本的RocketMQ中,对延迟消息并不支持任意时间的延迟设定(商业版本中支持),而是只支持18个固定的延迟级别
// 1到18分别对应messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h。
message.setDelayTimeLevel(3);
SendResult result = producer.send(message);
log.info("{}", result);
producer.shutdown();
} catch (Throwable cause) {
cause.printStackTrace();
}
}
}
同简单模式中的消费者推模式
批量消息是指将多条消息合并成一个批量消息,一次发送出去。这样的好处是可以减少网络IO,提升吞吐量。
如果批量消息大于1MB就不要用一个批次发送,而要拆分成多个批次消息发送。也就是说,一个批次消息的大小不要超过1MB
实际使用时,这个1MB的限制可以稍微扩大点,实际最大的限制是4194304字节,大概4MB。但是使用批量消息时,这个消息长度确实是必须考虑的一个问题。而且批量消息的使用是有一定限制的,这些消息应该有相同的Topic,相同的waitStoreMsgOK。而且不能是延迟消息、事务消息等。
package com.mrathena.rocket.mq.batch;
import com.mrathena.toolkit.IdKit;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.junit.Test;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@Slf4j
public class ProducerTest {
private static final String GROUP = "producer";
private static final String ADDRESS = "116.62.162.48:9876";
private static final String TOPIC = "topic";
private static final String TAG = "tag";
private static final String KEY = "key";
/**
* 简单批量发送
*/
@Test
public void simpleBatchTest() {
try {
DefaultMQProducer producer = new DefaultMQProducer(GROUP);
producer.setNamesrvAddr(ADDRESS);
producer.start();
List<Message> messageList = new LinkedList<>();
messageList.add(new Message(TOPIC, TAG, KEY, IdKit.getSerialNo().getBytes()));
messageList.add(new Message(TOPIC, TAG, KEY, IdKit.getSerialNo().getBytes()));
messageList.add(new Message(TOPIC, TAG, KEY, IdKit.getSerialNo().getBytes()));
messageList.add(new Message(TOPIC, TAG, KEY, IdKit.getSerialNo().getBytes()));
SendResult result = producer.send(messageList);
log.info("{}", result);
producer.shutdown();
} catch (Throwable cause) {
cause.printStackTrace();
}
}
/**
* 大小分割批量发送
*/
@Test
public void splitBatchTest() {
try {
DefaultMQProducer producer = new DefaultMQProducer(GROUP);
producer.setNamesrvAddr(ADDRESS);
producer.start();
// 创建一个1024byte长度的消息, 1m是1024*1024byte, 4096次才能达到4194304byte(4m), 这里按1m一发的方式来
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < 1024; i++) {
stringBuilder.append("*");
}
String content = stringBuilder.toString();
// 就是手动分割list, 保证每个消息的大小总和不超过4m就行了
List<Message> messageList = new LinkedList<>();
for (int i = 0; i < 1024; i++) {
messageList.add(new Message(TOPIC, TAG, KEY, content.getBytes()));
}
SendResult result = producer.send(messageList);
log.info("{}", result);
producer.shutdown();
// 计算message的size, 按如下公式来
Message message = new Message(TOPIC, TAG, KEY, content.getBytes());
int size = message.getTopic().length() + message.getBody().length;
Map<String, String> properties = message.getProperties();
for (Map.Entry<String, String> entry : properties.entrySet()) {
size += entry.getKey().length() + entry.getValue().length();
}
size = size + 20;
log.info("一条消息的大小是: {}", size);
} catch (Throwable cause) {
cause.printStackTrace();
}
}
}
同简单模式中的消费者推模式
在大多数情况下,可以使用Message的Tag属性来简单快速的过滤信息。
主要是看消息消费者。consumer.subscribe(“TagFilterTest”, “TagA || TagC”); 这句只订阅TagA和TagC的消息。
TAG是RocketMQ中特有的一个消息属性。RocketMQ的最佳实践中就建议,使用RocketMQ时,一个应用可以就用一个Topic,而应用中的不同业务就用TAG来区分。
但是,这种方式有一个很大的限制,就是一个消息只能有一个TAG,这在一些比较复杂的场景就有点不足了。 这时候,可以使用SQL表达式来对消息进行过滤。
需要配置开启sql过滤, enablePropertyFilter=true
这个模式的关键是在消费者端使用MessageSelector.bySql(String sql)返回的一个MessageSelector。这里面的sql语句是按照SQL92标准来执行的。sql中可以使用的参数有默认的TAGS和一个在生产者中加入的a属性。
SQL92语法:
RocketMQ只定义了一些基本语法来支持这个特性。你也可以很容易地扩展它。
数值比较,比如:>,>=,<,<=,BETWEEN,=;
字符比较,比如:=,<>,IN;
IS NULL 或者 IS NOT NULL;
逻辑符号 AND,OR,NOT;
常量支持类型为:
数值,比如:123,3.1415;
字符,比如:‘abc’,必须用单引号包裹起来;
NULL,特殊的常量
布尔值,TRUE 或 FALSE
使用注意:只有推模式的消费者可以使用SQL过滤。拉模式是用不了的。
package com.mrathena.rocket.mq.filter;
import com.mrathena.toolkit.IdKit;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.junit.Test;
@Slf4j
public class ProducerTest {
private static final String GROUP = "producer";
private static final String ADDRESS = "116.62.162.48:9876";
private static final String TOPIC = "topic";
private static final String TAG = "tag";
private static final String KEY = "key";
/**
* tag过滤
*/
@Test
public void tagFilterTest() {
try {
DefaultMQProducer producer = new DefaultMQProducer(GROUP);
producer.setNamesrvAddr(ADDRESS);
producer.start();
String[] tags = new String[] {"a", "b", "c"};
for (int i = 0; i < 10; i++) {
String tag = tags[i % tags.length];
SendResult result = producer.send(new Message(TOPIC, tag, KEY, (IdKit.getSerialNo() + ":" + tag).getBytes()));
log.info("{}", result);
}
producer.shutdown();
} catch (Throwable cause) {
cause.printStackTrace();
}
}
/**
* sql过滤
*/
@Test
public void sqlFilterTest() {
try {
DefaultMQProducer producer = new DefaultMQProducer(GROUP);
producer.setNamesrvAddr(ADDRESS);
producer.start();
String[] tags = new String[] {"a", "b", "c"};
for (int i = 0; i < 10; i++) {
String tag = tags[i % tags.length];
Message message = new Message(TOPIC, tag, KEY, (IdKit.getSerialNo() + ":" + tag).getBytes());
message.putUserProperty("property", String.valueOf(i));
SendResult result = producer.send(message);
log.info("{}", result);
}
producer.shutdown();
} catch (Throwable cause) {
cause.printStackTrace();
}
}
}
package com.mrathena.rocket.mq.filter;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.MessageSelector;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.common.protocol.heartbeat.MessageModel;
import org.junit.Test;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Slf4j
public class ConsumerTest {
private static final String GROUP = "consumer";
private static final String ADDRESS = "116.62.162.48:9876";
private static final String TOPIC = "topic";
@Test
public void tagFilterTest() {
try {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(GROUP);
consumer.setNamesrvAddr(ADDRESS);
// 只要a和b两个tag的消息
consumer.subscribe(TOPIC, "a || b");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
System.out.println();
for (MessageExt msg : msgs) {
log.info("收到消息: {}", new String(msg.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
TimeUnit.DAYS.sleep(1);
} catch (Throwable cause) {
cause.printStackTrace();
}
}
@Test
public void sqlFilterTest() {
try {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(GROUP);
consumer.setNamesrvAddr(ADDRESS);
// 需要配置开启sql过滤, enablePropertyFilter=true
consumer.subscribe(TOPIC, MessageSelector.bySql("(TAGS is not null and TAGS = 'a') and (property is not null and property between 0 and 5)"));
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
System.out.println();
for (MessageExt msg : msgs) {
log.info("收到消息: {}, {}", new String(msg.getBody()), msg.getUserProperty("property"));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
TimeUnit.DAYS.sleep(1);
} catch (Throwable cause) {
cause.printStackTrace();
}
}
}
事务消息是RocketMQ提供的一个非常有特色的功能,官网的介绍是:事务消息是在分布式系统中保证最终一致性的两阶段提交的消息实现。他可以保证 本地事务执行
与 消息发送
两个操作的原子性,也就是这两个操作一起成功或者一起失败。
事务消息只保证消息发送者的本地事务与发消息这两个操作的原子性,因此,事务消息的示例只涉及到消息发送者,对于消息消费者来说,并没有什么特别的。
事务消息的关键是在TransactionMQProducer中指定了一个TransactionListener事务监听器,这个事务监听器就是事务消息的关键控制器。
事务消息机制的关键是在发送消息时,会将消息转为一个half半消息(其实也是一条普通消息, 就是放的地方不普通),并存入RocketMQ内部的一个 RMQ_SYS_TRANS_HALF_TOPIC 这个Topic,这样对消费者是不可见的。再经过一系列事务检查通过后,再将消息转存到目标Topic,这样对消费者就可见了。
事务消息只保证了发送者本地事务和发送消息这两个操作的原子性,但是并不保证消费者本地事务的原子性,所以,事务消息只保证了分布式事务的一半。但是即使这样,对于复杂的分布式事务,RocketMQ提供的事务消息也是目前业内最佳的降级方案。
举例说明: 返利, 返利成功发消息给客户, 返利失败不发消息. 常规来讲, 将返利和发送消息分开, 可能会有返利成功发送消息失败的场景, 通过事务消息可以避免这种情况, 即可以保证 本地事务执行 与 消息发送 两个操作的原子性,也就是这两个操作一起成功或者一起失败
package com.mrathena.rocket.mq.transaction;
import com.mrathena.toolkit.IdKit;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.client.producer.TransactionMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;
import org.junit.Test;
import java.util.concurrent.*;
@Slf4j
public class ProducerTest {
private static final String GROUP = "producer";
private static final String ADDRESS = "116.62.162.48:9876";
private static final String TOPIC = "topic";
private static final String KEY = "key";
/**
* 事务发送
*/
@Test
public void sendMessageInTransaction() {
try {
TransactionMQProducer producer = new TransactionMQProducer(GROUP);
producer.setNamesrvAddr(ADDRESS);
//
ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("producer-transaction-message-check-thread");
return thread;
}
});
producer.setExecutorService(executorService);
// 设置执行本地事务和查询本地事务的监听器
producer.setTransactionListener(new TransactionListener() {
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
String tags = msg.getTags();
if (tags.contains("a")) {
// a标签的消息直接提交
return LocalTransactionState.COMMIT_MESSAGE;
} else if (tags.contains("b")) {
// b标签的消息直接回滚扔掉
return LocalTransactionState.ROLLBACK_MESSAGE;
} else {
// 其他都未知,待后续查询
return LocalTransactionState.UNKNOW;
}
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
String tags = msg.getTags();
if (tags.contains("c")) {
// c标签的消息在后面查询时提交
return LocalTransactionState.COMMIT_MESSAGE;
} else if (tags.contains("d")) {
// d标签的消息在后面查询时回滚扔掉
return LocalTransactionState.ROLLBACK_MESSAGE;
} else {
// 其他都未知,待后续再查询, 直到15次后扔掉
return LocalTransactionState.UNKNOW;
}
}
});
producer.start();
// 发送消息
String[] tags = new String[] {"a", "b", "c", "d", "e"};
for (int i = 0; i < 5; i++) {
String tag = tags[i % tags.length];
Message message = new Message(TOPIC, tag, KEY, (IdKit.getSerialNo() + ":" + tag).getBytes());
// 发送事务消息
SendResult result = producer.sendMessageInTransaction(message, null);
log.info("{}", result);
}
// 保持生产者存活
TimeUnit.DAYS.sleep(1);
producer.shutdown();
} catch (Throwable cause) {
cause.printStackTrace();
}
}
}
package com.mrathena.rocket.mq.transaction;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;
import org.junit.Test;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Slf4j
public class ConsumerTest {
private static final String GROUP = "consumer";
private static final String ADDRESS = "116.62.162.48:9876";
private static final String TOPIC = "topic";
@Test
public void pushTest() {
try {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(GROUP);
consumer.setNamesrvAddr(ADDRESS);
consumer.subscribe(TOPIC, "*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
System.out.println();
for (MessageExt msg : msgs) {
log.info("收到消息: {}, {}", msg.getTags(), new String(msg.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
TimeUnit.DAYS.sleep(1);
} catch (Throwable cause) {
cause.printStackTrace();
}
}
}
权限控制(ACL)主要为RocketMQ提供Topic资源级别的用户访问控制。用户在使用RocketMQ权限控制时,可以在Client客户端通过 RPCHook注入AccessKey和SecretKey签名;同时,将对应的权限控制属性(包括Topic访问权限、IP白名单和AccessKey和SecretKey签名等)设置在$ROCKETMQ_HOME/conf/plain_acl.yml的配置文件中。Broker端对AccessKey所拥有的权限进行校验,校验不过,抛出异常;
<dependency>
<groupId>org.apache.rocketmqgroupId>
<artifactId>rocketmq-aclartifactId>
<version>4.7.1version>
dependency>
Broker端具体的配置信息可以参见源码包下docs/cn/acl/user_guide.md。主要是在broker.conf中打开acl的标志:aclEnable=true。然后就可以用plain_acl.yml来进行权限配置了。并且这个配置文件是热加载的,也就是说要修改配置时,只要修改配置文件就可以了,不用重启Broker服务。我们来简单分析下源码中的plan_acl.yml的配置:
#全局白名单,不受ACL控制
#通常需要将主从架构中的所有节点加进来
globalWhiteRemoteAddresses:
- 10.10.103.*
- 192.168.0.*
accounts:
#第一个账户
- accessKey: RocketMQ
secretKey: 12345678
whiteRemoteAddress:
admin: false
defaultTopicPerm: DENY #默认Topic访问策略是拒绝
defaultGroupPerm: SUB #默认Group访问策略是只允许订阅
topicPerms:
- topicA=DENY #topicA拒绝
- topicB=PUB|SUB #topicB允许发布和订阅消息
- topicC=SUB #topicC只允许订阅
groupPerms:
# the group should convert to retry topic
- groupA=DENY
- groupB=PUB|SUB
- groupC=SUB
#第二个账户,只要是来自192.168.1.*的IP,就可以访问所有资源
- accessKey: rocketmq2
secretKey: 12345678
whiteRemoteAddress: 192.168.1.*
# if it is admin, it could access all resources
admin: true
public static RPCHook getAclRPCHook() {
return new AclClientRPCHook(new SessionCredentials(ACL_ACCESS_KEY, ACL_SECRET_KEY));
}
DefaultMQProducer producer = new DefaultMQProducer(GROUP, getAclRPCHook());
producer.setNamesrvAddr(ADDRESS);
producer.start();
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(GROUP, getAclRPCHook(), new AllocateMessageQueueAveragely());
consumer.setNamesrvAddr(ADDRESS);
consumer.subscribe(TOPIC, "*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
for (MessageExt msg : msgs) {
log.info("收到消息: {}", new String(msg.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
DefaultMQPullConsumer consumer = new DefaultMQPullConsumer(GROUP, getAclRPCHook());
consumer.setNamesrvAddr(ADDRESS);
consumer.start();