一、概述
咱们书接上文,上文主要讲解了RocketMQ的基本概念,对RocketMQ基本概念还不熟悉的小伙伴可以的学习一下(RocketMQ基础概念),本文主要探讨RocketMQ消息发送几种类型,将分别对普通消息发送、顺序消息发送、延迟消息发送、批量消息发送、事务消息发送以及各自适用的应用场景对大家一一介绍。
二、普通消息 (Normal Message)
普通消息是 RocketMQ 中最常见的一种消息类型,通常用于点对点的消息传递。消息的发送和接收比较简单,符合典型的消息队列模型。
消息发送
RocketMQ可用于以三种方式发送消息:同步、异步和单向传输。前两种消息类型是可靠的,因为无论它们是否成功发送都有响应。
同步发送
同步发送是最常用的方式,是指消息发送方发出一条消息后,会在收到服务端同步响应之后才发下一条消息的通讯方式,可靠的同步传输被广泛应用于各种场景,如重要的通知消息、短消息通知等。
编辑
代码示例:
java
public class SyncProducer {
public static void main(String[] args) throws Exception {
// 初始化一个producer并设置Producer group name
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); //(1)
// 设置NameServer地址
producer.setNamesrvAddr("localhost:9876"); //(2)
// 启动producer
producer.start();
for (int i = 0; i < 100; i++) {
// 创建一条消息,并指定topic、tag、body等信息,tag可以理解成标签,对消息进行再归类,RocketMQ可以在消费端对tag进行过滤
Message msg = new Message("TopicTest" /* Topic */,
"TagA" /* Tag */,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
); //(3)
// 利用producer进行发送,并同步等待发送结果
SendResult sendResult = producer.send(msg); //(4)
System.out.printf("%s%n", sendResult);
}
// 一旦producer不再使用,关闭producer
producer.shutdown();
}
}
异步发送
异步发送是指发送方发出一条消息后,不等服务端返回响应,接着发送下一条消息的通讯方式。异步发送需要实现异步发送回调接口(SendCallback)。
异步发送的应用场景:
- 高并发消息发送:当需要高吞吐量时,异步发送可以显著提高系统性能,减少消息发送的阻塞时间。
- 实时处理任务:对于一些实时性要求较高的任务,异步发送可以减少等待时间,使得任务能够尽早开始处理。
- 减少延迟:在多线程或分布式系统中,异步操作能够减少线程的等待时间,从而降低延迟。
编辑
代码示例:
java
public class AsyncProducer {
public static void main(String[] args) throws Exception {
// 初始化一个producer并设置Producer group name
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
// 设置NameServer地址
producer.setNamesrvAddr("localhost:9876");
// 启动producer
producer.start();
producer.setRetryTimesWhenSendAsyncFailed(0);
int messageCount = 100;
final CountDownLatch countDownLatch = new CountDownLatch(messageCount);
for (int i = 0; i < messageCount; i++) {
try {
final int index = i;
// 创建一条消息,并指定topic、tag、body等信息,tag可以理解成标签,对消息进行再归类,RocketMQ可以在消费端对tag进行过滤
Message msg = new Message("TopicTest",
"TagA",
"Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
// 异步发送消息, 发送结果通过callback返回给客户端
producer.send(msg, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.printf("%-10d OK %s %n", index,
sendResult.getMsgId());
countDownLatch.countDown();
}
@Override
public void onException(Throwable e) {
System.out.printf("%-10d Exception %s %n", index, e);
e.printStackTrace();
countDownLatch.countDown();
}
});
} catch (Exception e) {
e.printStackTrace();
countDownLatch.countDown();
}
}
//异步发送,如果要求可靠传输,必须要等回调接口返回明确结果后才能结束逻辑,否则立即关闭Producer可能导致部分消息尚未传输成功
countDownLatch.await(5, TimeUnit.SECONDS);
// 一旦producer不再使用,关闭producer
producer.shutdown();
}
}
单向发送
发送方只负责发送消息,不等待服务端返回响应且没有回调函数触发,即只发送请求不等待应答。此方式发送消息的过程耗时非常短,一般在微秒级别。适用于某些耗时非常短,但对可靠性要求并不高的场景,例如日志收集。
编辑
代码示例:
java
public class OnewayProducer {
public static void main(String[] args) throws Exception{
// 初始化一个producer并设置Producer group name
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
// 设置NameServer地址
producer.setNamesrvAddr("localhost:9876");
// 启动producer
producer.start();
for (int i = 0; i < 100; i++) {
// 创建一条消息,并指定topic、tag、body等信息,tag可以理解成标签,对消息进行再归类,RocketMQ可以在消费端对tag进行过滤
Message msg = new Message("TopicTest" /* Topic */,
"TagA" /* Tag */,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
);
// 由于在oneway方式发送消息时没有请求应答处理,如果出现消息发送失败,则会因为没有重试而导致数据丢失。若数据不可丢,建议选用可靠同步或可靠异步发送方式。
producer.sendOneway(msg);
}
// 一旦producer不再使用,关闭producer
producer.shutdown();
}
}
三、顺序消息 (Ordered Message)
顺序消息是一种对消息发送和消费顺序有严格要求的消息。RocketMQ 保证同一队列内的消息消费顺序,但是在不同队列之间,消息的顺序不能得到保证。
RocketMQ 顺序消息的顺序关系通过消息组(MessageGroup)判定和识别,发送顺序消息时需要为每条消息设置归属的消息组,相同消息组的多条消息之间遵循先进先出的顺序关系,不同消息组、无消息组的消息之间不涉及顺序性。
应用场景
顺序消息适用于那些必须按顺序处理的业务场景,典型的应用场景包括:
- 订单处理:例如支付订单、商品库存扣减等操作,需要严格按照顺序进行。
- 流水处理:如银行交易流水、日志记录等,也需要按顺序处理每一条记录。
- 事件驱动处理:当多个事件有严格的顺序依赖关系时,顺序消息可以确保事件按照期望的顺序被处理。
如何保证消息的顺序性
RocketMQ 的消息的顺序性分为两部分,生产顺序性和消费顺序性。
(1) 生产的顺序性
如需保证消息生产的顺序性,则必须满足以下条件:
- 单一生产者:消息生产的顺序性仅支持单一生产者,不同生产者分布在不同的系统,即使设置相同的消息组,不同生产者之间产生的消息也无法判定其先后顺序
- 串行发送:RocketMQ 生产者客户端支持多线程安全访问,但如果生产者使用多线程并行发送,则不同线程间产生的消息将无法判定其先后顺序。
满足以上条件的生产者,将顺序消息发送至RocketMQ 后,会保证设置了同一消息组的消息,按照发送顺序存储在同一队列中。服务端顺序存储逻辑如下:
- 同消息组的消息按照先后顺序被存储在同一个队列。
- 不同消息组的消息可以混合在同一个队列中,且不保证连续。
(2) 消费顺序性
RocketMQ 通过消费者和服务端的协议保障消息消费严格按照存储的先后顺序来处理。
如需保证消息消费的顺序性,则必须满足以下条件:
- 投递顺序: RocketMQ 通过客户端SDK和服务端通信协议保障消息按照服务端存储顺序投递,但业务方消费消息时需要严格按照接收---处理---应答的语义处理消息,避免因异步处理导致消息乱序。
- 有限重试: RocketMQ 顺序消息投递仅在重试次数限定范围内,即一条消息如果一直重试失败,超过最大重试次数后将不再重试,跳过这条消息消费,不会一直阻塞后续消息处理。
代码示例:
ini
public class Producer {
public static void main(String[] args) throws UnsupportedEncodingException {
try {
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
producer.start();
String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
for (int i = 0; i < 100; i++) {
int orderId = i % 10;
Message msg =
new Message("TopicTest", tags[i % tags.length], "KEY" + i,
("Hello RocketMQ " + i).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);
}
producer.shutdown();
} catch (MQClientException | RemotingException | MQBrokerException | InterruptedException e) {
e.printStackTrace();
}
}
}
四、延迟消息 (Delayed Message)
RocketMQ 的 延迟消息 是指在发送消息时指定消息的延迟时间,消息会在指定的延迟时间后才被消费者消费。RocketMQ 延迟消息的实现是通过使用 消息定时任务 的方式,使得消息在未到达指定时间时不会被消费者消费。
在 RocketMQ 5.x 版本中,延迟消息的实现不再完全依赖于原来的延迟队列机制,而是采用了 延迟调度任务 的方式进行消息的延迟投递。这种方式更符合高效和低延迟的要求。
相比较与4.x版本,5.x主要变化有:
- 延迟时间级别:在 RocketMQ 5.x 中,延迟时间的精度得到了提升,支持更灵活的延迟时间配置。
- 消息调度改进:RocketMQ 5.x 引入了基于调度框架的延迟消息处理,优化了延迟消息的存储和投递过程。
- 更高效的存储:RocketMQ 5.x 在存储上进行了优化,延迟消息不再占用额外的内存空间,而是通过持久化机制来减少系统的负担。
延迟消息的应用场景
- 定时任务:例如,用户在某个时间段才会看到某个消息,使用延迟消息可以控制消息在某个时间后才被消费者接收。
- 重试机制:在消息消费失败后,可以使用延迟消息让消费者稍后再试。
- 订单系统:比如用户下单后,系统需要等待几分钟后才能自动确认订单的有效性,这时可以使用延迟消息。
- 活动倒计时:例如活动即将开始,提前发送一条延迟消息,活动开始时触发相关逻辑。
延迟消息代码示例:
java
public class ScheduledMessageProducer {
public static void main(String[] args) throws Exception {
// Instantiate a producer to send scheduled messages
DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup");
// Launch producer
producer.start();
int totalMessagesToSend = 100;
for (int i = 0; i < totalMessagesToSend; i++) {
Message message = new Message("TestTopic", ("Hello scheduled message " + i).getBytes());
// This message will be delivered to consumer 10 seconds later.
// 4.x版本
//1,5,10,30,60,120,180,240,300,600,1200,1800,2400,3000,3600,4800,6000,7200
message.setDelayTimeLevel(3);
// 5.x版本
// 设置消息的定时投递时间(精确到时间戳)
long deliveryTime = System.currentTimeMillis() + 10000; // 10秒后投递
message.setDeliveryTimestamp(deliveryTime);
// Send the message
producer.send(message);
}
// Shutdown producer after use.
producer.shutdown();
}
}
五、批量消息 (Batch Message)
在 RocketMQ 中,批量消息(Batch Message)是指将多个消息打包成一个消息批次一起发送,这样可以减少网络开销和消息传输的延迟,从而提高消息发送的性能。批量消息不仅可以提高吞吐量,还能减少单次消息发送的次数,降低系统的资源消耗。特别是在高并发、大量消息的场景下,批量消息的优势尤为明显。
代码示例:
ini
public class SimpleBatchProducer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("BatchProducerGroup");
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
// 创建消息列表
List<Message> messages = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Message msg = new Message("TopicTest", "TagA", ("Message " + i).getBytes());
messages.add(msg);
}
// 批量发送消息
SendResult sendResult = producer.send(messages);
System.out.println("Send result: " + sendResult);
// 关闭生产者
producer.shutdown();
}
}
六、事务消息 (Transactional Message)
在一些对数据一致性有强需求的场景,可以用RocketMQ 事务消息来解决,从而保证上下游数据的一致性。事务消息是RocketMQ 提供的一种高级消息类型,支持在分布式场景下保障消息生产和本地事务的最终一致性。
应用场景
(1) 分布式事务的诉求
分布式系统调用的特点为一个核心业务逻辑的执行,同时需要调用多个下游业务进行处理。因此,如何保证核心业务和多个下游业务的执行结果完全一致,是分布式事务需要解决的主要问题。
(2) 传统XA事务方案:性能不足
基于XA分布式事务的方案可以满足业务处理结果的正确性,但最大的缺点是多分支环境下资源锁定范围大,并发度低,随着下游分支的增加,系统性能会越来越差。
(3) 基于普通消息方案:一致性保障困难
将上述基于XA事务的方案进行简化,将订单系统变更作为本地事务,剩下的系统变更作为普通消息的下游来执行,事务分支简化成普通消息+表事务,充分利用消息异步化的能力缩短链路,提高并发度。
(4) 基于RocketMQ分布式事务消息:支持最终一致性
上述普通消息方案中,普通消息和表事务无法保证一致的原因,本质上是由于普通消息无法像单机数据库事务一样,具备提交、回滚和统一协调的能力。
而基于RocketMQ实现的分布式事务消息功能,在普通消息基础上,支持二阶段的提交能力。将二阶段提交和本地事务绑定,实现全局提交结果的一致性。
事务消息示例代码:
java
public class TransactionProducer {
public static void main(String[] args) throws MQClientException, InterruptedException {
// 这里会实现执行本地事务,回调查询等逻辑
TransactionListener transactionListener = new TransactionListenerImpl();
// 创建一个支持事务的producer
TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name");
ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100,
TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("client-transaction-msg-check-thread");
return thread;
}
});
// 给事务消息生产者设置对象的线程池,负责执行RocketMQ回调请求
producer.setExecutorService(executorService);
// 给事务消息生产者设置对应的回调函数
producer.setTransactionListener(transactionListener);
producer.setNamesrvAddr("192.168.127.153:9876");
producer.start();
String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
for (int i = 0; i < 10; i++) {
try {
// 构造消息
Message msg =
new Message("TopicTest1234", tags[i % tags.length], "KEY" + i,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
// 将消息作为half消息的模式发送出去
SendResult sendResult = producer.sendMessageInTransaction(msg, null);
System.out.printf("%s%n", sendResult);
} catch (MQClientException | UnsupportedEncodingException e) {
e.printStackTrace();
}
}
producer.shutdown();
}
}
java
public class TransactionListenerImpl implements TransactionListener {
private AtomicInteger transactionIndex = new AtomicInteger(0);
private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>();
// 如果half消息发送成功了
// 就会在这个回调这个函数,此时我们就可以执行本地事务
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try{
// 如果本地事务执行成功,返回commit
int value = transactionIndex.getAndIncrement();
int status = value % 3;
localTrans.put(msg.getTransactionId(), status);
return LocalTransactionState.COMMIT_MESSAGE;
}catch (Exception e){
// 本地事务执行失败回滚一切执行过的操作
// 如果本地事务执行失败,返回rollback,标记half消息无效
e.printStackTrace();
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
// 如果因为各种原因,没有返回commit或者rollback,则执行下面方法
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
// 查询本地事务是否执行成功
Integer status = localTrans.get(msg.getTransactionId());
// 根据本地事务的情况去选择执行commit or rollback
if (null != status) {
switch (status) {
case 0:
return LocalTransactionState.UNKNOW;
case 1:
return LocalTransactionState.COMMIT_MESSAGE;
case 2:
return LocalTransactionState.ROLLBACK_MESSAGE;
default:
return LocalTransactionState.COMMIT_MESSAGE;
}
}
return LocalTransactionState.COMMIT_MESSAGE;
}
}