一、普通消息
Producer对于消息的发送方式也有多种选择,不同的方式会产生不同的系统效果。
同步发送消息
同步发送消息是指,Producer发出⼀条消息后,会在收到MQ返回的ACK之后才发下⼀条消息。该方式的消息可靠性最高,但消息发送效率太低,因为需要同步刷盘,等待ACK的回应。
public class SyncProducer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("producer_A");
// NameServer
producer.setNamesrvAddr("localhost:9876");
// 设置当发送失败时重试发送的次数,默认是2次
producer.setRetryTimesWhenSendFailed(3);
// 设置发送超时时间,默认3秒
producer.setSendMsgTimeout(5000);
// 开启生产者
producer.start();
// 模拟发送100条消息
for (int i = 0; i < 100; i++) {
byte[] bytes = ("hi " + i).getBytes();
Message msg = new Message("topic_A", "tag_A", bytes);
// 为消息指定key
msg.setKeys("key-" + i);
// 发送消息
SendResult sendResult = producer.send(msg);
System.out.println(sendResult);
}
// 关闭生产者
producer.shutdown();
}
}
public class Consumer {
public static void main(String[] args) throws Exception {
// 定义一个push消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer();
// NameServer
consumer.setNamesrvAddr("localhost:9876");
// 指定从第一条消息开始消费
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
// 指定消费topic与tag
consumer.subscribe("topic_A", "*");
// 指定采用广播模式进行消费,默认为集群模式
consumer.setMessageModel(MessageModel.BROADCASTING);
// 注册消息监听器
consumer.registerMessageListener(new MessageListenerConcurrently() {
// 一旦broker中有了其订阅的消息就会触发该方法的执行,其返回值为当前consumer消费的状态
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list,
ConsumeConcurrentlyContext context) {
for (MessageExt msg : list) {
System.out.println(msg);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 开启消费者消费
consumer.start();
System.out.println("Consumer Started");
}
}
异步发送消息
异步发送消息是指,Producer发出消息后无需等待MQ返回ACK,直接发送下⼀条消息。该方式的消息可靠性可以得到保障,消息发送效率也可以(异步刷盘)。
public class AsyncProducer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("producer_A");
// NameServer
producer.setNamesrvAddr("localhost:9876");
// 指定异步发送失败后不进行重试发送
producer.setRetryTimesWhenSendAsyncFailed(0);
// 指定新创建的Topic的Queue数量为2,默认为4
producer.setDefaultTopicQueueNums(2);
// 开启生产者
producer.start();
// 模拟发送100条消息
for (int i = 0; i < 100; i++) {
byte[] bytes = ("hi " + i).getBytes();
try {
Message msg = new Message("topic_A", "tag_A", bytes);
// 异步发送,指定回调
producer.send(msg, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println(sendResult);
}
@Override
public void onException(Throwable throwable) {
throwable.printStackTrace();
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
// 异步发送,如果不执行sleep,则消息在发送之前,producer已经关闭,就会报错
TimeUnit.SECONDS.sleep(3);
// 关闭生产者
producer.shutdown();
}
}
单向发送消息
单向发送消息是指,Producer仅负责发送消息,不等待、不处理MQ的ACK。该发送方式时MQ也不返回ACK。该方式的消息发送效率最高,但消息可靠性较差。
public class OneWayProducer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("producer_A");
// NameServer
producer.setNamesrvAddr("localhost:9876");
// 开启生产者
producer.start();
for (int i = 0; i < 100; i++) {
byte[] bytes = ("hi " + i).getBytes();
Message msg = new Message("topic_A", "tag_A", bytes);
// 单向发送
producer.sendOneway(msg);
}
// 关闭生产者
producer.shutdown();
}
}
二、事务消息
(1)、正常事务消息的发送及提交
a、生产者发送half消息到Broker服务端(半消息);
半消息是一种特殊的消息类型,该状态的消息暂时不能被Consumer消费。当一条事务消息被成功投递到Broker上,但是Broker并没有接收到Producer发出的二次确认时,该事务消息就处于"暂时不可被消费"状态,该状态的事务消息被称为半消息。
b、Broker服务端将消息持久化之后,给生产者响应消息写入结果(ACK响应);
c、生产者根据发送结果(broker的响应结果)执行本地事务逻辑(如果写入失败,此时half消息对业务不可见,本地逻辑不执行);
d、生产者根据本地事务执行结果向Broker服务端提交二次确认(Commit 或是 Rollback),Broker服务端收到 Commit 状态则将半事务消息标记为可投递,订阅方最终将收到该消息;Broker服务端收到 Rollback 状态则删除半事务消息,订阅方将不会接收该消息;
(2)、事务消息的补偿流程
a、在网络闪断或者是应用重启的情况下,可能导致生产者发送的二次确认消息未能到达Broker服务端,经过固定时间后,Broker服务端将会对没有Commit/Rollback的事务消息(pending状态的消息)进行“回查”;
b、生产者收到回查消息后,检查回查消息对应的本地事务执行的最终结果;
c、生产者根据本地事务状态,再次提交二次确认给Broker,然后Broker重新对半事务消息Commit或者Rollback;
其中,补偿阶段用于解决消息Commit或者Rollback发生超时或者失败的情况。
事务消息共有三种状态,提交状态、回滚状态、中间状态:
-
- TransactionStatus.CommitTransaction:提交事务,它允许消费者消费此消息。
- TransactionStatus.RollbackTransaction:回滚事务,它代表该消息将被删除,不允许被消费。
- TransactionStatus.Unknown:中间状态,它代表需要回查本地事务状态来决定是提交还是回滚事务。
三、顺序消息
顺序消息指的是,严格按照消息的发送顺序进行消费的消息(FIFO)。默认情况下生产者会把消息以Round Robin轮询方式发送到不同的Queue分区队列;而消费消息时会从多个Queue上拉取消息,这种情况下的发送和消费是不能保证顺序的。如果将消息仅发送到同一个Queue中,消费时也只从这个Queue上拉取消息,就严格保证了消息的顺序性。
顺序消息分为全局顺序消息和部分顺序消息:
全局顺序消息:
当发送和消费参与的Queue只有一个时所保证的有序是整个Topic中消息的顺序, 称为全局有序。
在创建Topic时指定Queue的数量。有三种指定方式
1、在代码中创建Producer时,可以指定其自动创建的Topic的Queue数量。
2、在RocketMQ可视化控制台中手动创建Topic时指定Queue数量。
3、使用mqadmin命令手动创建Topic时指定Queue数量。
在RocketMQ中,如果使消息全局有序,为Topic设置一个消息队列,使用一个生产者单线程发送数据,消费者端也使用单线程进行消费,从而保证消息的全局有序,但是这种方式,没有并发的且效率低,一般不使用。
部分顺序消息
假设一个Topic分配了两个消息队列,生产者在发送消息的时候,可以对消息设置一个路由ID,比如想保证一个订单的相关消息有序,那么就使用订单ID当做路由ID,在发送消息的时候,通过订单ID对消息队列的个数取余,根据取余结果选择消息队列,这样同一个订单的数据就可以保证发送到一个消息队列中,消费者端使用MessageListenerOrderly处理有序消息,这就是RocketMQ的局部有序,保证消息在某个消息队列中有序。
要保证部分消息有序,需要发送端和消费端配合处理。在发送端,要做到把同一业务ID的消息发送 到同一个Message Queue;在消费过程中,要做到从同一个Message Queue读取的消息不被并发处理,这样才能达到部分有序。消费端通过使用MessageListenerOrderly类来解决单Message Queue的消息被并发处理的问题。
四、延迟消息
定时消息(延迟队列)是指消息发送到broker后,不会立即被消费,等待特定时间投递给真正的 topic。 broker有配置项messageDelayLevel,默认值为“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h”,18个level。可以配置自定义messageDelayLevel。
level有以下三种情况:
level == 0,消息为非延迟消息
1<=level<=maxLevel,消息延迟特定时间,例如level==1,延迟1s
level > maxLevel,则level== maxLevel,例如level==20,延迟2h
发消息时,设置delayLevel等级即可: msg.setDelayLevel(level)。
五、刷盘机制
RocketMQ 的所有消息都是持久化的,先写入系统 PageCache,然后刷盘,可以保证内存与磁盘 都有一份数据, 访问时,直接从内存读取。消息在通过Producer写入RocketMQ的时候,有两种写磁盘方式,分布式同步刷盘和异步刷盘。
同步刷盘
在消息达到Broker的内存之后,必须刷到commitLog日志文件中才算成功,然后返回Producer数据已经发送成功。
异步刷盘
异步刷盘是指消息达到Broker内存后就返回Producer数据已经发送成功,会唤醒一个线程去将数据持久化到CommitLog日志文件中。
优缺点分析
同步刷盘保证了消息不丢失,但是响应时间相对异步刷盘要多出10%左右,适用于对消息可靠性要求比较高的场景。异步刷盘的吞吐量比较高,RT小,但是如果broker断电了内存中的部分数据会丢失,适用于对吞吐量要求比较高的场景。
六、消息重试
顺序消息的重试
对于顺序消息,当消费者消费消息失败后,消息队列 RocketMQ 会自动不断进行消息重试(每次间隔时间为 1 秒),这时,应用会出现消息消费被阻塞的情况。因此,在使用顺序消息时,务必保证应用能够及时监控并处理消费失败的情况,避免阻塞现象的发生。
无序消息的重试
对于无序消息(普通、定时、延时、事务消息),当消费者消费消息失败时,您可以通过设置返回 状态达到消息重试的结果。无序消息的重试只针对集群消费方式生效;广播方式不提供失败重试特性,即消费失败后,失败消 息不再重试,继续消费新的消息。
消息队列 RocketMQ 默认允许每条消息最多重试 16 次,每次重试的间隔时间如下:
如果消息重试 16 次后仍然失败,消息将不再投递。
注意:
1) 消息最大重试次数的设置对相同 Group ID 下的所有 Consumer 实例有效。
2) 如果只对相同 Group ID 下两个 Consumer 实例中的其中一个设置了 MaxReconsumeTimes,那么该配置对两个 Consumer 实例均生效。
3) 配置采用覆盖的方式生效,即最后启动的 Consumer 实例会覆盖之前的启动实例的配置
七、死信队列
RocketMQ中消息重试超过一定次数后(默认16次)就会被放到死信队列中,在消息队列 RocketMQ 中,这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),存储死信 消息的特殊队列称为死信队列(Dead-Letter Queue)。
可视化工具:rocketmq-console下载地址:
https://github.com/apache/rocketmq-externals/archive/rocketmq-console-1.0.0.zip
死信消息特性:
1) 不会再被消费者正常消费。
2) 有效期与正常消息相同,均为 3 天,3 天后会被自动删除。因此,请在死信消息产生后的 3 天内及时处理。
死信队列特征:
1) 一个死信队列对应一个 Group ID, 而不是对应单个消费者实例。
2) 如果一个 Group ID 未产生死信消息,消息队列 RocketMQ 不会为其创建相应的死信队列。
3) 一个死信队列包含了对应 Group ID 产生的所有死信消息,不论该消息属于哪个 Topic。
一条消息进入死信队列,意味着某些因素导致消费者无法正常消费该消息,因此,通常需要您对其进行特殊处理。排查可疑因素并解决问题后,可以在消息队列 RocketMQ 控制台重新发送该消息,让消费者重新消费一次。
八、消息查询及优先级
消息查询
RocketMQ支持按照下面两种维度(“按照Message Id查询消息”、“按照Message Key查询消息”)进行消息查询。
按照MessageId查询消息:MsgId 总共 16 字节,包含消息存储主机地址(ip/port),消息 Commit Log offset。
按照Message Key查询消息:主要是基于RocketMQ的IndexFile索引文件来实现的。
消息优先级
有些场景,需要应用程序处理几种类型的消息,不同消息的优先级不同。RocketMQ是个先入先出的队列,不支持消息级别或者Topic级别的优先级。
1) 多个不同的消息类型使用同一个topic时,由于某一个种消息流量非常大,导致其他类型的消息无法及时消费,造成不公平,所以把流量大的类型消息在一个单独的 Topic,其他类型消息在另外一个 Topic,应用程序创建两个 Consumer,分别订阅不同的 Topic。创建一个 Topic, 设置Topic的 MessageQueue 数量超过 100 个,Producer根据订 单的门店号,把每个门店的订单写人 一 个 MessageQueue。 DefaultMQPushConsumer默认是采用 循环的方式逐个读取一个 Topic 的所有 MessageQueue,这样如果某家门店订单量大增,这家门店对 应的 MessageQueue 消息数增多,等待时间增长,但不会造成其他家门店等待时间增长。
2) 情况和第一种情况类似,但是不用创建大量的Topic。
3) 强制优先级 TypeA、 TypeB、 TypeC 三类消息 。TypeA 处于第一优先级,要确保只要有TypeA消息,必须优先处理; TypeB处于第二优先 级; TypeC 处于第三优先级 。