前言
- 在项目中使用MQ消息队列时,某些业务场景可能需要保证消息的顺序消费执行。
- 比如在订单流程场景中:创建订单、支付订单、订单完成这三个订单状态需要保证顺序执行,不能先支付订单,再创建订单。也不能订单完成,再去支付订单。
- 在项目中如果使用默认的普通消息,那么就算生产者按照顺序发布消息后,消费端也有可能会不会按照顺序进行消费消息。
RocketMQ
- RocketMQ在其官方文档中指出,除了支持无序消息以外,还支持顺序消息。
- 无序消息:普通消息,事务消息,延迟消息和定时消息
- 顺序消息:全局顺序消息,分区顺序消息
顺序消息
- RocketMQ顺序消息(FIFO消息)提供的一种严格按照顺序发布和消费的消息。顺序发布和消费是指对于同一个topic,生产者按照先后顺序发布消息,消费者按照既定顺序订阅消费消息,即先发布的消息一定会被消费端先消费。
- RocketMQ顺序消息2.0特性:
- 高并发:无主一致性架构提高了顺序消息的吞吐量
- 高可用:无主一致性架构拥有更完善的负载均衡策略和故障节点自适应恢复能力
- 无热点:完善的集群负载均衡策略解决了顺序消息的热点问题
- 注:如果要严格保证顺序消息消费,那么生产者不能适用异步和多线程进行发布消息,消费端的并发消费量设置为1
全局顺序消息
- 对于指定的同一个topic,所有消息都按照FIFO的顺序进行发布和消费
- 适用于性能要求不高,对数据有严格一致性发布和消费的场景。
分区顺序消息
- 对于指定的同一个topic,所有消息根据Sharding Key进行区块分区。同一个分区内的消息会严格按照FIFO顺序进行发布和消费。而Sharding Key是顺序消息中用来区分不同区的关键字,和普通消息中key不是同一个概念
- 适用于对性能要求高,在同一个区块中要求FIFO进行发布和消费的场景。
- 例如以订单编号或者订单ID为Sharding Key,在同一个订单内需要严格保证消费顺序。但不同订单中,可能订单A先发布,再发布订单B。在消费是先消费订单B,再消费订单A也是可以的。只要订单内部是顺序执行即可。
全局消息和分区消息对比
消息类型对比:
topic消息类型 | 是否支持事务 | 是否支持定时或延迟 | 性能 |
---|
无序消息(普通、事务、定时、延迟) | 是 | 是 | 最高 |
分区顺序消息 | 否 | 否 | 较高 |
全局顺序消息 | 否 | 否 | 低 |
消息发送方式对比:
topic消息类型 | 是否支持可靠同步发送 | 是否支持异步发送 | 是否支持oneway发送 |
---|
无序消息(普通、事务、定时、延迟) | 是 | 是 | 最高 |
分区顺序消息 | 是 | 否 | 否 |
全局顺序消息 | 是 | 否 | 否 |
- 消息类型不可重复叠加。即顺序消息、订单消息、事务消息不能在同一个topic中使用。
- 全局顺序消息因为会对整个topic队列中消息进行阻塞。即上一条消息如果未消费成功,那其他消息将会一直阻塞在队列里,所以其性能TPS最低。
发送顺序消息官方demo
package com.aliyun.openservices.ons.example.order;
import com.aliyun.openservices.ons.api.Message;
import com.aliyun.openservices.ons.api.ONSFactory;
import com.aliyun.openservices.ons.api.PropertyKeyConst;
import com.aliyun.openservices.ons.api.SendResult;
import com.aliyun.openservices.ons.api.order.OrderProducer;
import java.util.Properties;
public class ProducerClient {
public static void main(String[] args) {
Properties properties = new Properties();
// 您在消息队列RocketMQ版控制台创建的Group ID。
properties.put(PropertyKeyConst.GROUP_ID,"XXX");
// AccessKey ID阿里云身份验证,在阿里云RAM控制台创建。
properties.put(PropertyKeyConst.AccessKey,"XXX");
// AccessKey Secret阿里云身份验证,在阿里云RAM控制台创建。
properties.put(PropertyKeyConst.SecretKey,"XXX");
// 设置TCP接入域名,进入消息队列RocketMQ版控制台实例详情页面的接入点区域查看。
properties.put(PropertyKeyConst.NAMESRV_ADDR,"XXX");
OrderProducer producer = ONSFactory.createOrderProducer(properties);
// 在发送消息前,必须调用start方法来启动Producer,只需调用一次即可。
producer.start();
for (int i = 0; i < 1000; i++) {
String orderId = "biz_" + i % 10;
Message msg = new Message(
// Message所属的Topic。
"Order_global_topic",
// Message Tag,可理解为Gmail中的标签,对消息进行再归类,方便Consumer指定过滤条件在消息队列RocketMQ版的服务器过滤。
"TagA",
// Message Body,可以是任何二进制形式的数据,消息队列RocketMQ版不做任何干预,需要Producer与Consumer协商好一致的序列化和反序列化方式。
"send order global msg".getBytes()
);
// 设置代表消息的业务关键属性,请尽可能全局唯一。
// 以方便您在无法正常收到消息情况下,可通过消息队列RocketMQ版控制台查询消息并补发。
// 注意:不设置也不会影响消息正常收发。
msg.setKey(orderId);
// 分区顺序消息中区分不同分区的关键字段,Sharding Key与普通消息的key是完全不同的概念。
// 全局顺序消息,该字段可以设置为任意非空字符串。
String shardingKey = String.valueOf(orderId);
try {
SendResult sendResult = producer.send(msg, shardingKey);
// 发送消息,只要不抛异常就是成功。
if (sendResult != null) {
System.out.println(new Date() + " Send mq message success. Topic is:" + msg.getTopic() + " msgId is: " + sendResult.getMessageId());
}
}
catch (Exception e) {
// 消息发送失败,需要进行重试处理,可重新发送这条消息或持久化这条数据进行补偿处理。
System.out.println(new Date() + " Send mq message failed. Topic is:" + msg.getTopic());
e.printStackTrace();
}
}
// 在应用退出前,销毁Producer对象。
// 注意:如果不销毁也没有问题。
producer.shutdown();
}
}
订阅顺序消息官方demo
package com.aliyun.openservices.ons.example.order;
import com.aliyun.openservices.ons.api.Message;
import com.aliyun.openservices.ons.api.ONSFactory;
import com.aliyun.openservices.ons.api.PropertyKeyConst;
import com.aliyun.openservices.ons.api.order.ConsumeOrderContext;
import com.aliyun.openservices.ons.api.order.MessageOrderListener;
import com.aliyun.openservices.ons.api.order.OrderAction;
import com.aliyun.openservices.ons.api.order.OrderConsumer;
import java.util.Properties;
public class ConsumerClient {
public static void main(String[] args) {
Properties properties = new Properties();
// 您在消息队列RocketMQ版控制台创建的Group ID。
properties.put(PropertyKeyConst.GROUP_ID,"XXX");
// AccessKey ID阿里云身份验证,在阿里云RAM控制台创建。
properties.put(PropertyKeyConst.AccessKey,"XXX");
// AccessKey Secret阿里云身份验证,在阿里云RAM控制台创建。
properties.put(PropertyKeyConst.SecretKey,"XXX");
// 设置TCP接入域名,进入消息队列RocketMQ版控制台实例详情页面的接入点区域查看。
properties.put(PropertyKeyConst.NAMESRV_ADDR,"XXX");
// 顺序消息消费失败进行重试前的等待时间,单位(毫秒),取值范围: 10毫秒~30,000毫秒。
properties.put(PropertyKeyConst.SuspendTimeMillis,"100");
// 消息消费失败时的最大重试次数。
properties.put(PropertyKeyConst.MaxReconsumeTimes,"20");
// 在订阅消息前,必须调用start方法来启动Consumer,只需调用一次即可。
OrderConsumer consumer = ONSFactory.createOrderedConsumer(properties);
consumer.subscribe(
// Message所属的Topic。
"Order_global_topic",
// 订阅指定Topic下的Tags:
// 1. * 表示订阅所有消息。
// 2. TagA || TagB || TagC表示订阅TagA或TagB或TagC的消息。
"*",
new MessageOrderListener() {
/**
* 1. 消息消费处理失败或者处理出现异常,返回OrderAction.Suspend。
* 2. 消息处理成功,返回OrderAction.Success。
*/
@Override
public OrderAction consume(Message message, ConsumeOrderContext context) {
System.out.println(message);
return OrderAction.Success;
}
});
consumer.start();
}
}
最后