RocketMQ消息转发模型
消息模型(Message Model)
RocketMQ主要由 Producer, Broker, Consumer 三部分组成, 其中Producer 负责生产消息, Consumer 负责消费消息, Broker 负责存储消息. Broker 在实际 部署过程中对应一台服务器, 每个 Broker 可以存储多个Topic的消息, 每个Topic 的消息也可以分片存储于不同的 Broker. Message Queue 用于存储消息的物理地 址, 每个Topic中的消息地址存储于多个 Message Queue 中. ConsumerGroup 由多个Consumer 实例构成.
消息生产者(Producer)
负责生产消息, 一般由业务系统负责生产消息. 一个消息生产者会把业务应用系 统里产生的消息发送到broker服务器. RocketMQ提供多种发送方式, 同步发送, 异步发送, 顺序发送, 单向发送. 同步和异步方式均需要Broker返回确认信息, 单 向发送不需要.
生产者中, 会把同一类Producer组成一个集合, 叫做生产者组. 同一组的 Producer被认为是发送同一类消息且发送逻辑一致.
消息消费者(Consumer)
负责消费消息, 一般是后台系统负责异步消费. 一个消息消费者会从Broker服务 器拉取消息, 并将其提供给应用程序. 从用户应用的角度而言提供了两种消费形 式:拉取式消费, 推动式消费.
- 拉取式消费的应用通常主动调用Consumer的拉消息方法从Broker服务器拉消 息, 主动权由应用控制. 一旦获取了批量消息, 应用就会启动消费过程.
- 推动式消费模式下Broker收到数据后会主动推送给消费端, 该消费模式一般实时 性较高.
消费者同样会把同一类Consumer组成一个集合, 叫做消费者组, 这类 Consumer通常消费同一类消息且消费逻辑一致. 消费者组使得在消息消费方面, 实现负载均衡和容错的目标变得非常容易. 要注意的是, 消费者组的消费者实例必 须订阅完全相同的Topic. RocketMQ 支持两种消息模式:集群消费 (Clustering)和广播消费(Broadcasting).
- 集群消费模式下, 相同Consumer Group的每个Consumer实例平均分摊消息.
- 广播消费模式下, 相同Consumer Group的每个Consumer实例都接收全量的消 息.
主题(Topic)
表示一类消息的集合, 每个主题包含若干条消息, 每条消息只能属于一个主题, 是RocketMQ进行消息订阅的基本单位.
Topic只是一个逻辑概念, 并不实际保存消息. 同一个Topic下的消息, 会分片保 存到不同的Broker上, 而每一个分片单位, 就叫做MessageQueue. MessageQueue是一个具有FIFO特性的队列结构, 生产者发送消息与消费者消费消 息的最小单位.
代理服务器(Broker Server)
消息中转角色, 负责存储消息, 转发消息. 代理服务器在RocketMQ系统中负责 接收从生产者发送来的消息并存储, 同时为消费者的拉取请求作准备. 代理服务器 也存储消息相关的元数据, 包括消费者组, 消费进度偏移和主题和队列消息等.
Broker Server是RocketMQ真正的业务核心, 包含了多个重要的子模块:
- Remoting Module:整个Broker的实体, 负责处理来自clients端的请求.
- Client Manager:负责管理客户端(Producer/Consumer)和维护Consumer的 Topic订阅信息
- Store Service:提供方便简单的API接口处理消息存储到物理硬盘和查询功能.
- HA Service:高可用服务, 提供Master Broker 和 Slave Broker之间的数据同步 功能.
- Index Service:根据特定的Message key对投递到Broker的消息进行索引服 务, 以提供消息的快速查询.
而Broker Server要保证高可用需要搭建主从集群架构. RocketMQ中有两种Broker 架构模式:
- 普通集群:
这种集群模式下会给每个节点分配一个固定的角色, master负责响应客户端的请 求, 并存储消息. slave则只负责对master的消息进行同步保存, 并响应部分客户端 的读请求. 消息同步方式分为同步同步和异步同步. 这种集群模式下各个节点的角色无法进行切换, 也就是说, master节点挂了, 这一 组Broker就不可用了.
- Dledger高可用集群:
Dledger是RocketMQ自4.5版本引入的实现高可用集群的一项技术. 这个模式下的 集群会随机选出一个节点作为master, 而当master节点挂了后, 会从slave中自动 选出一个节点升级成为master. Dledger技术做的事情:1, 从集群中选举出master节点 2, 完成master节点往 slave节点的消息同步.
Dledger技术做的事情:
- 从集群中选举出master节点
- 完成master节点往slave节点的消息同步.
Name Server
名称服务充当路由消息的提供者. Broker Server会在启动时向所有的Name Server注册自己的服务信息, 并且后续通过心跳请求的方式保证这个服务信息的实 时性. 生产者或消费者能够通过名字服务查找各主题相应的Broker IP列表. 多个 Namesrv实例组成集群, 但相互独立, 没有信息交换.
这种特性也就意味着NameServer中任意的节点挂了, 只要有一台服务节点正 常, 整个路由服务就不会有影响. 当然, 这里不考虑节点的负载情况.
消息(Message)
消息系统所传输信息的物理载体, 生产和消费数据的最小单位, 每条消息必须属 于一个主题Topic. RocketMQ中每个消息拥有唯一的Message ID, 且可以携带具 有业务标识的Key. 系统提供了通过Message ID和Key查询消息的功能.
并且Message上有一个为消息设置的标志, Tag标签. 用于同一主题下区分不同 类型的消息. 来自同一业务单元的消息, 可以根据不同业务目的在同一主题下设置 不同标签. 标签能够有效地保持代码的清晰度和连贯性, 并优化RocketMQ提供的 查询系统. 消费者可以根据Tag实现对不同子主题的不同消费逻辑, 实现更好的扩展 性.
RocketMQ的消息样例
基本样例
同步发送简单消息
同步发送信息, 将会有个SendResut返回值, 意思是生产者同步地给broker发送信息, broker收到信息后会回复给生产者, 生产者发送后会等待这个返回值
DefaultMQProducer producer = new DefaultMQProducer("simpleProducerGroup");
producer.setNamesrvAddr("192.168.1.54:9876");
producer.start();
//for (int i = 0; i < 128; i++) {
for (int i = 0; i < Integer.MAX_VALUE; i++) {
Message message = new Message("simpleTopic", "tag" + i, "userId", ("hello workd" + i).getBytes(StandardCharsets.UTF_8));
// 同步发送信息, 将会有个SendResut返回值, 意思是生产者同步地给broker发送信息, broker收到信息后会回复给生产者, 生产者发送后会等待这个返回值
SendResult send = producer.send(message);
System.out.printf("%s%n", send);
}
producer.shutdown();
单向发送消息
关键点就是使用producer.sendOneWay方式来发送消息. 这个方法没有返回值, 也没有回调. 就是只管把消息发出去就行了
DefaultMQProducer producer = new DefaultMQProducer("sendOneWay");
producer.setNamesrvAddr("localhost:9876");
producer.start();
for (int i = 0; i < 128; i++) {
Message message = new Message("sendOneWayTopic", "tag" + i, "sendOneWayKey" + i, "sendOneWayMessage".getBytes(StandardCharsets.UTF_8));
producer.sendOneway(message);
}
producer.shutdown();
异步发送消息
异步发送消息需要一个回调, 回调需要时限onSuccess和onError方法, 消息发送成功/失败会回调此方法
DefaultMQProducer producer = new DefaultMQProducer("asyncProducerGroup");
producer.setNamesrvAddr("localhost:9876");
producer.start();
int nums = 20;
CountDownLatch countDownLatch = new CountDownLatch(nums);
for (int i = 0; i < 20; i++) {
Message message = new Message("asyncTopic", "asyncTag" + i, "asyncKey" + i, "asyncMessageBody".getBytes(StandardCharsets.UTF_8));
// 异步发送消息, 参数是一个SendCallback, 实现其onSuccess和onException方法, 生产者使用完send方法后不会去管broker是否成功收到消息, 只是提供2个回调函数. 等broker确认收到消息的时候, 回调这个函数
// 如果消息没发送到broker, 会回调失败方法.
producer.send(message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
int queueId = sendResult.getMessageQueue().getQueueId();
String topic = sendResult.getMessageQueue().getTopic();
String brokerName = sendResult.getMessageQueue().getBrokerName();
String msgId = sendResult.getMsgId();
SendStatus sendStatus = sendResult.getSendStatus();
System.out.println(String.format("queueId: %s, topic: %s, brokerName: %s, msgId: %s, sendStatus: %s", queueId, topic, brokerName, msgId, sendStatus));
countDownLatch.countDown();
}
@Override
public void onException(Throwable throwable) {
System.out.println("发送失败: " + throwable.getMessage());
countDownLatch.countDown();
}
});
}
countDownLatch.await();
producer.shutdown();
顺序消息
RocketMQ保证的是消息的局部有序, 而不是全局有序.
实际上, RocketMQ也只保证了每个OrderID的所有消息有序(发到了同一个queue), 而并不能保证所有消息都有序. 所以这就涉及到了RocketMQ消息有序的原理. 要保证最终消费到的消息是有序的, 需要从Producer, Broker, Consumer三个步骤都保证消息有序才行.
-
首先在发送者端:在默认情况下, 消息发送者会采取Round Robin轮询方式把消息发送到不同的MessageQueue(分区队列), 而消费者消费的时候也从多个MessageQueue上拉取消息, 这种情况下消息是不能保证顺序的. 而只有当一组有序的消息发送到同一个MessageQueue上时, 才能利用MessageQueue先进先出的特性保证这一组消息有序.
-
而Broker中一个队列内的消息是可以保证有序的.
-
然后在消费者端:消费者会从多个消息队列上去拿消息. 这时虽然每个消息队列上的消息是有序的, 但是多个队列之间的消息仍然是乱序的. 消费者端要保证消息有序, 就需要按队列一个一个来取消息, 即取完一个队列的消息后, 再去取下一个队列的消息. 而给consumer注入的MessageListenerOrderly对象, 在RocketMQ内部就会通过锁队列的方式保证消息是一个一个队列来取的. MessageListenerConcurrently这个消息监听器则不会锁队列, 每次都是从多个Message中取一批数据(默认不超过32条). 因此也无法保证消息有序.
生产者代码
DefaultMQProducer producer = new DefaultMQProducer("orderProducerGroup");
producer.setNamesrvAddr("47.110.130.198:9876");
producer.start();
String[] tags = new String[]{"tagA", "tagB", "tagC", "tagD", "tagF"};
String[] steps = new String[]{"1", "2", "3", "4", "5"};
int orderId = 1;
int step = 1;
for (int i = 1; i <= 100; i++) {
Message message = new Message("orderedTopic", tags[(i - 1) % tags.length], "key" + i, ("[" + orderId + "]订单第" + steps[(i - 1) % tags.length] + "步").getBytes(StandardCharsets.UTF_8));
// RocketMQ本质上是不存在顺序消息的, 所谓顺序消息是根据MQ特性, 人为地控制发送消息的方式
// 对于每个topic来说, 有多个MessageQueue, 队列的特性为先进先出, 因此只要让某几个想顺序消费的消息固定发送到同一个MessageQueue中就可以保证消费的时候是顺序的
// 比如id为1的订单固定发送到1号MessageQueue, id为2的订单固定发送到2号MessageQueue.
producer.send(message, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
// 通过orderId的hash, 对list取模, 获得一个MessageQueue返回, 那么本次发消息就会发送到这个指定的MessageQueue中
int index = (o.hashCode() > 0 ? o.hashCode() : -o.hashCode()) % list.size();
return list.get(index);
}
}, orderId);
if (i % 5 == 0) {
orderId++;
}
}
producer.shutdown();
消费者代码
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("orderConsumerGroup");
consumer.setNamesrvAddr("47.110.130.198:9876");
consumer.subscribe("orderedTopic", "*");
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> list, ConsumeOrderlyContext consumeOrderlyContext) {
for (MessageExt messageExt: list){
Thread thread = Thread.currentThread();
int queueId = messageExt.getQueueId();
String msg = String.format("queueId: %s, consumerThread: %s, topic: %s, keys: %s, tags: %s, body: %s", queueId, thread.getName(), messageExt.getTopic(), messageExt.getKeys(), messageExt.getTags(), new String(messageExt.getBody(), StandardCharsets.UTF_8));
System.out.println(msg);
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
consumer.start();
运行结果:
广播消息
广播消息和生产者无关, 生产者只管发送消息, 消费者使用广播模式消费即可.
广播模式就是把消息发给了所有订阅了对应主题的消费者, 而不管消费者是不是同一个消费者组.
生产者代码
DefaultMQProducer producer = new DefaultMQProducer("broadcastProducer");
producer.setNamesrvAddr("localhost:9876");
producer.start();
for (int i = 0; i < 20; i++) {
Message message = new Message("broadcastTopic", "tag" + i, "userId", ("这是[" + i + "]广播消息, 所有消费者都应该收到").getBytes(StandardCharsets.UTF_8));
// 同步发送信息, 将会有个SendResut返回值, 意思是生产者同步地给broker发送信息, broker收到信息后会回复给生产者, 生产者发送后会等待这个返回值
SendResult send = producer.send(message);
System.out.printf("%s%n", send);
}
producer.shutdown();
消费者代码
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("broadcastConsumer");
consumer.setNamesrvAddr("localhost:9876");
consumer.subscribe("broadcastTopic", "*");
// 广播消息就是生产者发送消息到broker的某个topic中, 那么所有订阅该topic的消费者都能收到消息, 不论是不是在同一个消费者组
// 需要设置消息模型为broadcasting, 默认为cluster: 同一条消息只能被同一个消费者组消费一次
consumer.setMessageModel(MessageModel.BROADCASTING);
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt messageExt : list) {
Thread thread = Thread.currentThread();
int queueId = messageExt.getQueueId();
String msg = String.format("queueId: %s, consumerThread: %s, topic: %s, keys: %s, tags: %s, body: %s", queueId, thread.getName(), messageExt.getTopic(), messageExt.getKeys(), messageExt.getTags(), new String(messageExt.getBody(), StandardCharsets.UTF_8));
System.out.println(msg);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
批量消息
如果批量消息大于1MB就不要用一个批次发送, 而要拆分成多个批次消息发送. 也就是说, 一个批次消息的大小不要超过1MB
实际使用时, 这个1MB的限制可以稍微扩大点, 实际最大的限制是4194304字节, 大概4MB. 但是使用批量消息时, 这个消息长度确实是必须考虑的一个问题. 而且批量消息的使用是有一定限制的, 这些消息应该有相同的Topic, 相同的waitStoreMsgOK. 而且不能是延迟消息, 事务消息等.
生产者代码
DefaultMQProducer producer = new DefaultMQProducer("batchProducerGroup");
producer.setNamesrvAddr("localhost:9876");
producer.start();
String topic = "batchTopic";
List<Message> msgs = new ArrayList<>();
msgs.add(new Message(topic, "tag1", "userId1", "这是批量消息的第一条".getBytes(StandardCharsets.UTF_8)));
msgs.add(new Message(topic, "tag2", "userId2", "这是批量消息的第二条".getBytes(StandardCharsets.UTF_8)));
msgs.add(new Message(topic, "tag3", "userId3", "这是批量消息的第三条".getBytes(StandardCharsets.UTF_8)));
// 如果msgs中的消息过大, 就会抛异常, 无法一次性发送这么多的信息: Exception in thread "main" org.apache.rocketmq.client.exception.MQClientException: CODE: 13 DESC: the message body size over max value, MAX: 4194304
// msgs中的消息最大值为4194304字节, 也就是4MB
/*for (int i = 0; i < 1000 * 1000; i++) {
msgs.add(new Message(topic, "tag" + i, "userId" + i, ("这是批量消息的第" + (i + 1) + "条").getBytes(StandardCharsets.UTF_8)));
}*/
producer.send(msgs);
producer.shutdown();
事务消息
事务消息是在分布式系统中保证最终一致性的两阶段提交的消息实现, 它可以保证本地事务执行和消息发送这两个操作的原子性, 也就是这两个操作一起成功或者失败
事务消息只保证消息发送者的本地事务与发消息的这两个操作的原子性, 因此事务消息的示例只涉及消息发送者, 对于消费者来说并没有什么特别
事务消息的限制:
-
事务消息不支持延迟消息和批量消息.
-
为了避免单个消息被检查太多次而导致班队列消息积累, rocketmq将单个消息的检查次数限制为15次, 但是用户可以通过broker配置文件的 transactionCheckMax参数来修改此限制. 如果已经检查某条消息超过N次的话, 则broker将丢弃此消息, 并在默认情况下同时打印错误日志. 用户可以通过重写AbstractTransactionCheckListener类来修改此行为.
-
事务消息将在broker配置文件中的参数transactionMsgTimeout这样的特定时间长度之后被检查. 当发送事务消息的时候, 用户还可以通过 设置用户属性CHECK_IMMUNITY_TIME_IN_SECONDS来改变这个限制, 该参数优先于transactionMsgTimeout参数.
-
事务消息可能不止一次被检查或消费.
-
提交给用户的目标主题消息可能会失败, 目前这依据日志的记录而定, 它的高可用性通过rocketMQ本身的高可用机制来保证, 如果希望确保事 务消息不会丢失/事务完整性得到保证, 建议使用同步的双重写入机制.
-
事务消息的生产者ID不能与其它类型消息的生产者ID共享. 与其它类型的消息不同, 事务消息允许反向查询/MQ服务器能通过它们的生产者ID查 询到消费者.
-
事务消息机制的关键是在于发送消息时, 会将消息转为一个half半消息, 并存入rocketMQ内部的一个RMQ_SYS_TRANS_HALF_TOPIC这个top- ic中, 这样对消费者是不可见的. 再经过一系列事务检查通过后, 再将消息转存到目标topic, 这消费者就可见了.
-
事务消息只保证了发送者本地事务和发送消息这两个操作的原子性, 但是并不保证消费者本地事务的原子性. 所以, 事务消息只保证了分布式事 务的一半, 但是即使这样, 对于复杂的分布式事务, rocketMQ提供的事务消息也是目前业内最佳的降级方案.
生产者代码
TransactionMQProducer producer = new TransactionMQProducer("transactionProducerGroup");
producer.setNamesrvAddr("localhost:9876");
producer.setTransactionListener(new TransactionListener() {
@Override
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
String tags = message.getTags();
System.out.println(String.format("第一阶段: 执行本地事务, tag: %s", tags));
if (tags.contains("A")) {
// 如果tag包含了A, 就提交消息
return LocalTransactionState.COMMIT_MESSAGE;
} else if (tags.contains("B")) {
// 如果tag包含了B, 就回滚消息(丢弃)
return LocalTransactionState.ROLLBACK_MESSAGE;
}
// 其它情况(本例中为tags包含了CDE), 就设置状态为unknow, 进行第二次校验
return LocalTransactionState.UNKNOW;
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
// 第二次校验, 将不会存在AB了, 并且第二阶段的第二次以后, 也不会出现CD了
String tags = messageExt.getTags();
System.out.println(String.format("第二阶段: 回查消息, tag: %s", tags));
if (tags.contains("C")) {
// 如果tags包含了C, 就提交消息
return LocalTransactionState.COMMIT_MESSAGE;
} else if (tags.contains("D")) {
// 如果tags包含了D, 就回滚消息(丢弃)
return LocalTransactionState.ROLLBACK_MESSAGE;
}
// 其它情况(本例中为tags包含了E), 就设置状态为unknow
return LocalTransactionState.UNKNOW;
}
});
ExecutorService executor = new ThreadPoolExecutor(2, 4, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(50), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("client-transaction-msg-check-thread");
return thread;
}
});
producer.setExecutorService(executor);
producer.start();
String[] tags = new String[]{"A", "B", "C", "D", "E"};
for (int i = 0; i < 20; i++) {
Message message = new Message("transactionTopic", tags[i % tags.length], "userId", ("hello workd" + i).getBytes(StandardCharsets.UTF_8));
message.putUserProperty("CHECK_IMMUNITY_TIME_IN_SECONDS", 1 + "");
// 同步发送信息, 将会有个SendResut返回值, 意思是生产者同步地给broker发送信息, broker收到信息后会回复给生产者, 生产者发送后会等待这个返回值
SendResult send = producer.sendMessageInTransaction(message, null);
System.out.printf("%s%n", send);
}
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
producer.shutdown();
消费者代码
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("transactionConsumerGroup");
consumer.setNamesrvAddr("localhost:9876");
consumer.subscribe("transactionTopic", "*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt messageExt : list) {
Thread thread = Thread.currentThread();
int queueId = messageExt.getQueueId();
String msg = String.format("queueId: %s, consumerThread: %s, topic: %s, keys: %s, tags: %s, body: %s", queueId, thread.getName(), messageExt.getTopic(), messageExt.getKeys(), messageExt.getTags(), new String(messageExt.getBody(), StandardCharsets.UTF_8));
System.out.println(msg);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
过滤消息
所谓过滤消息, 就是生产者在发送消息的时候, 带上一些特有的标记, 消费者消费的时候, 指定消费某些标记的消息
tag过滤
生产者发送消息的时候, subExpression设置一些tag, 消费者消费的时候指定订阅主题下的某几个tag, 多个用||隔开
生产者代码
DefaultMQProducer producer = new DefaultMQProducer("tagFilterProducerGroup");
producer.setNamesrvAddr("localhost:9876");
producer.start();
String[] tags = new String[]{"A", "B", "C", "D", "E"};
String topic = "tagFilterTopic";
for (int i = 0; i < 20; i++) {
String tag = tags[i % tags.length];
Message message = new Message(topic, tag, "tagFilterKey" + i, ("这是tag过滤的第[ " + (i + 1) + "]条消息").getBytes(StandardCharsets.UTF_8));
producer.send(message);
}
producer.shutdown();
消费者代码
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("tagFilterConsumerGroup");
consumer.setNamesrvAddr("localhost:9876");
consumer.subscribe("tagFilterTopic", "A || C || D");
consumer.registerMessageListener((MessageListenerConcurrently) (list, consumeConcurrentlyContext) -> {
for (MessageExt messageExt : list) {
Thread thread = Thread.currentThread();
int queueId = messageExt.getQueueId();
String msg = String.format("queueId: %s, consumerThread: %s, topic: %s, keys: %s, tags: %s, body: %s", queueId, thread.getName(), messageExt.getTopic(), messageExt.getKeys(), messageExt.getTags(), new String(messageExt.getBody(), StandardCharsets.UTF_8));
System.out.println(msg);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
consumer.start();
sql过滤
sql过滤需要遵循sql92语法, 而且并不常用, tag过滤已经满足绝大多数需求了.
生产者代码
DefaultMQProducer producer = new DefaultMQProducer("sqlFilterProducerGroup");
producer.setNamesrvAddr("localhost:9876");
producer.start();
String topic = "sqlFilterTopic";
String[] tags = new String[]{"A", "B", "C", "D", "E"};
for (int i = 0; i < 50; i++) {
int orderId = (i + 1);
String tag = tags[i % tags.length];
Message message = new Message(topic, tag, "tagFilterKey" + i, ("这是sql过滤的第[ " + (i + 1) + "]条消息").getBytes(StandardCharsets.UTF_8));
// 添加用户属性, 用于sql过滤
message.putUserProperty("orderId", orderId + "");
producer.send(message);
}
producer.shutdown();
消费者代码
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("sqlFilterConsumerGroup");
consumer.setNamesrvAddr("localhost:9876");
// rocketMQ支持SQL92语法, 默认支持如下几种操作
/**
* 数值比较, 比如:>, >=, <, <=, BETWEEN, =;
* 字符比较, 比如:=, <>, IN;
* IS NULL 或者 IS NOT NULL;
* 逻辑符号 AND, OR, NOT;
* 常量支持类型为:
* 数值, 比如:123, 3.1415;
* 字符, 比如:'abc', 必须用单引号包裹起来;
* NULL, 特殊的常量
* 布尔值, TRUE 或 FALSE
*/
consumer.subscribe("sqlFilterTopic", MessageSelector.bySql("(TAGS is not null and TAGS in ('A', 'C', 'D')) " +
"and (orderId is not null and orderId between 5 and 20)"));
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt messageExt : list) {
Thread thread = Thread.currentThread();
int queueId = messageExt.getQueueId();
String msg = String.format("queueId: %s, consumerThread: %s, topic: %s, keys: %s, tags: %s, orderId: %s, body: %s", queueId, thread.getName(), messageExt.getTopic(), messageExt.getKeys(), messageExt.getTags(), messageExt.getUserProperty("orderId"), new String(messageExt.getBody(), StandardCharsets.UTF_8));
System.out.println(msg);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
rocketmq常见问题
使用RocketMQ如何保证消息不丢失?
哪些环节会有丢消息的可能
其中, 1, 2, 4三个场景都是跨网络的, 而跨网络就肯定会有丢消息的可能. 然后关于3这个环节, 通常MQ存盘时都会先写入操作系统的缓存page cache中, 然后再由操作系统异步的将消息写入硬盘. 这个中间有个时间差, 就可能会造成消息丢失. 如果服务挂了, 缓存中还没有来得及写入硬盘的消息就会丢失.
RocketMQ消息零丢失方案
- 事务消息
- 消费者端不要使用异步消费机制
正常情况下, 消费者端都是需要先处理本地事务, 然后再给MQ一个ACK响应, 这时MQ就会修改Offset, 将消息标记为已消费, 从而不再往其他消费者推送消息. 所以在Broker的这种重新推送机制下, 消息是不会在传输过程中丢失的.
- Broker配置同步刷盘+Dledger主从架构
使用RocketMQ如何快速处理积压消息
如何确定RocketMQ有大量的消息积压
在正常情况下, 使用MQ都会要尽量保证他的消息生产速度和消费速度整体上是平衡的, 但是如果部分消费者系统出现故障, 就会造成大量的消息积累. 这类问题通常在实际工作中会出现得比较隐蔽. 例如某一天一个数据库突然挂了, 大家大概率就会集中处理数据库的问题. 等好不容易把数据库恢复过来了, 这时基于这个数据库服务的消费者程序就会积累大量的消息. 或者网络波动等情况, 也会导致消息大量的积累. 这在一些大型的互联网项目中, 消息积压的速度是相当恐怖的. 所以消息积压是个需要时时关注的问题.
对于消息积压, 如果是RocketMQ或者kafka还好, 他们的消息积压不会对性能造成很大的影响. 而如果是RabbitMQ的话, 那就惨了, 大量的消息积压可以瞬间造成性能直线下滑.
对于RocketMQ来说, 有个最简单的方式来确定消息是否有积压. 那就是使用web控制台, 就能直接看到消息的积压情况. 在Web控制台的主题页面, 可以通过 Consumer管理 按钮实时看到消息的积压情况.
另外, 也可以通过mqadmin指令在后台检查各个Topic的消息延迟情况.
还有RocketMQ也会在他的 ${storePathRootDir}/config 目录下落地一系列的json文件, 也可以用来跟踪消息积压情况.
如何处理大量积压的消息
如果Topic下的MessageQueue配置得是足够多的, 那每个Consumer实际上会分配多个MessageQueue来进行消费. 这个时候, 就可以简单的通过增加Consumer的服务节点数量来加快消息的消费, 等积压消息消费完了, 再恢复成正常情况. 最极限的情况是把Consumer的节点个数设置成跟MessageQueue的个数相同. 但是如果此时再继续增加Consumer的服务节点就没有用了.
而如果Topic下的MessageQueue配置得不够多的话, 那就不能用上面这种增加Consumer节点个数的方法了. 这时怎么办呢? 这时如果要快速处理积压的消息, 可以创建一个新的Topic, 配置足够多的MessageQueue. 然后把所有消费者节点的目标Topic转向新的Topic, 并紧急上线一组新的消费者, 只负责消费旧Topic中的消息, 并转储到新的Topic中, 这个速度是可以很快的. 然后在新的Topic上, 就可以通过增加消费者个数来提高消费速度了. 之后再根据情况恢复成正常情况.