前序
问题引出:
假设现在有这么一个业务,上游系统通过消息队列,发送一个订单的状态操作信息,上游先发生操作订单价格乘以2倍,再操作订单价格减去10元,但是系统发送的两条消息时间相差不大,下游系统如何接受消息,并且需要严格保证订单状态的操作顺序性?
针对这个问题,我们来介绍下rocketmq的消息机制中的顺序消费实现。
一、rocketMq的消费模式划分
1.并发消费模式
当同一类消息(一般指同一个topic)业务设计上不需要有序的消费信息,这时候我们可以使用高性能的并发消费模式。并发消费模式下,一个消费端可以同时消费多个数据队列(queue)的多个消息(批量拉取默认32条),消费端中的每个线程也可以一次消费多条消息。
2.顺序消费模式
顺序消费,分为全局顺序消费,和局部顺序消费。
全局顺序消费:只能有一个数据队列(queue),和一个消费者实例。原因是RocketMQ只提供在单个queue上使用FIFO顺序的有序消息。多个queue之间并不能保证消息的严格先后性。
局部顺序消费:通常在实际应用中,我们需要将同一个订单号的相关操作,按照规则(可以是hash或取模等)发送到同一个queue上(使用MessageQueueSelector ),然后消费者实例,使用顺序消费模式消费消息(使用MessageListenerOrderly)。
二、顺序消费代码实例
1.生产端demo
public class OrderedProducer {
public static void main(String[] args) throws Exception {
//初始化生成端
MQProducer producer = new DefaultMQProducer("example_group_name");
//启动
producer.start();
//对消息进行 tag标签划分
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));
//借助MessageQueueSelector 选择queue发送
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();
}
}
2.消费端demo
public class OrderedConsumer {
public static void main(String[] args) throws Exception {
//声明一个推模式的消费端(rocketmq分为 服务器broker push推消息模式,和生产端主动pull拉模式)
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("example_group_name");
//设置消费起点: CONSUME_FROM_FIRST_OFFSET
//一个新的订阅组第一次启动从队列的最后位置开始消费<br>* 后续再启动接着上次消费的进度开始消费
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
//这里的这个实例 只消费 tag a,c,d 的消息
consumer.subscribe("TopicTest", "TagA || TagC || TagD");
//采用顺序消费模式 MessageListenerOrderly
consumer.registerMessageListener(new MessageListenerOrderly() {
AtomicLong consumeTimes = new AtomicLong(0);
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeOrderlyContext context) {
context.setAutoCommit(false);
System.out.printf(Thread.currentThread().getName() + " Receive New Messages: " + msgs + "%n");
this.consumeTimes.incrementAndGet();
if ((this.consumeTimes.get() % 2) == 0) {
return ConsumeOrderlyStatus.SUCCESS;
} else if ((this.consumeTimes.get() % 3) == 0) {
return ConsumeOrderlyStatus.ROLLBACK;
} else if ((this.consumeTimes.get() % 4) == 0) {
return
ConsumeOrderlyStatus.COMMIT;
} else if ((this.consumeTimes.get() % 5) == 0) {
context.setSuspendCurrentQueueTimeMillis(3000);
return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
//消费开启
consumer.start();
System.out.printf("Consumer Started.%n");
}
}
三.局部有序消费详细流程说明
1.生产端
多线程发送的消息无法保证有序性,因此,需要业务方在发送时,针对同一个业务编号(如同一笔订单)的消息需要保证在一个线程内顺序发送,在上一个消息发送成功后,在进行下一个消息的发送。对应到mq中,消息发送方法就得使用同步发送,异步发送无法保证顺序性,而且rocketmq的每个topic下会存在多个queue,要保证消息的顺序性,同一个订单相关的操作消息需要被发送到同一个queue中。代码里,需要使用MessageQueueSelector来选择要发送的queue
2.服务端
mq broker端的每个queue都是采用的FIFO模式,即采用的队列存储,可以保证顺行性。
3.消费端
rocketmq默认消费逻辑:
负载均衡,指定消费者负责某些队列;当前消费者开启多个线程开始同时消费这个队列,远程拉取消息。从消费逻辑中可以看到,如果要保证消息有序消费,就要解决这两个问题。在broker端,如果使用MessageListenerOrderly(顺序消费)的模式,broker通过锁定MessageQueue的方式,来保证同一时刻,只能有一个消费者进行消费。在消费端,在DefaultMQPushConsumer 中的pullMessage方法中,我们进入this.consumeMessageService.submitConsumeRequest方法,看看顺序消费与并发消费的区别。ConsumeMessageOrderlyService(顺序消费)ConsumeRequest(线程类)中的run方法
ConsumeMessageConcurrentlyService(并发消费)ConsumeRequest(线程类)类中的run方法。
我们能清晰看到,顺序消费的线程 会对 messageQueue进行一个锁操作,只有获取到锁之后才会处理消息体,保证同时,只有一个线程消费一个queue。
问题解决:
相信大家看到这,就能够对前序中提出的问题,就能解答出来了,只要上游系统发送的时候把相同订单的状态消息发送到同一个queue,然后下游系统使用顺序消费模式,就能严格保证订单的操作顺序了。