RocketMQ——順序消息

轉載自:RocketMQ——順序消息

消息有序指的是可以按照消息的發送順序來消費。
RocketMQ可以嚴格的保證消息有序。但這個順序,不是全局順序,只是分區(queue)順序。要全局順序只能一個分區。

之所以出現你這個場景看起來不是順序的,是因爲發送消息的時候,消息發送默認是會採用輪詢的方式發送到不通的queue(分區)。如圖:

而消費端消費的時候,是會分配到多個queue的,多個queue是同時拉取提交消費。如圖:

但是同一條queue裏面,RocketMQ的確是能保證FIFO的。那麼要做到順序消息,應該怎麼實現呢——把消息確保投遞到同一條queue。

下面用訂單進行示例。一個訂單的順序流程是:創建、付款、推送、完成。訂單號相同的消息會被先後發送到同一個隊列中,消費時,同一個OrderId獲取到的肯定是同一個隊列。

rocketmq消息生產端示例代碼如下:


/**
 * Producer,發送順序消息
 */
public class Producer {
    
    public static void main(String[] args) throws IOException {
        try {
            DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
 
            producer.setNamesrvAddr("10.11.11.11:9876;10.11.11.12:9876");
 
            producer.start();
 
            String[] tags = new String[] { "TagA", "TagC", "TagD" };
            
            // 訂單列表
            List<OrderDemo> orderList =  new Producer().buildOrders();
            
            Date date = new Date();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String dateStr = sdf.format(date);
            for (int i = 0; i < 10; i++) {
                // 加個時間後綴
                String body = dateStr + " Hello RocketMQ " + orderList.get(i);
                Message msg = new Message("TopicTestjjj", tags[i % tags.length], "KEY" + i, body.getBytes());
 
                SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
                    @Override
                    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                        Long id = (Long) arg;
                        long index = id % mqs.size();
                        return mqs.get((int)index);
                    }
                }, orderList.get(i).getOrderId());//訂單id
 
                System.out.println(sendResult + ", body:" + body);
            }
            
            producer.shutdown();
 
        } catch (MQClientException e) {
            e.printStackTrace();
        } catch (RemotingException e) {
            e.printStackTrace();
        } catch (MQBrokerException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.in.read();
    }
    
    /**
     * 生成模擬訂單數據 
     */
    private List<OrderDemo> buildOrders() {
        List<OrderDemo> orderList = new ArrayList<OrderDemo>();
 
        OrderDemo orderDemo = new OrderDemo();
        orderDemo.setOrderId(15103111039L);
        orderDemo.setDesc("創建");
        orderList.add(orderDemo);
        
        orderDemo = new OrderDemo();
        orderDemo.setOrderId(15103111065L);
        orderDemo.setDesc("創建");
        orderList.add(orderDemo);
        
        orderDemo = new OrderDemo();
        orderDemo.setOrderId(15103111039L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);
        
        orderDemo = new OrderDemo();
        orderDemo.setOrderId(15103117235L);
        orderDemo.setDesc("創建");
        orderList.add(orderDemo);
        
        orderDemo = new OrderDemo();
        orderDemo.setOrderId(15103111065L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);
        
        orderDemo = new OrderDemo();
        orderDemo.setOrderId(15103117235L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);
        
        orderDemo = new OrderDemo();
        orderDemo.setOrderId(15103111065L);
        orderDemo.setDesc("完成");
        orderList.add(orderDemo);
        
        orderDemo = new OrderDemo();
        orderDemo.setOrderId(15103111039L);
        orderDemo.setDesc("推送");
        orderList.add(orderDemo);
        
        orderDemo = new OrderDemo();
        orderDemo.setOrderId(15103117235L);
        orderDemo.setDesc("完成");
        orderList.add(orderDemo);
        
        orderDemo = new OrderDemo();
        orderDemo.setOrderId(15103111039L);
        orderDemo.setDesc("完成");
        orderList.add(orderDemo);
        
        return orderList;
    }
輸出:


從圖中紅色框可以看出,orderId等於15103111039的訂單被順序放入queueId等於7的隊列。queueOffset同時在順序增長。

發送時有序,接收(消費)時也要有序,才能保證順序消費。如下這段代碼演示了普通消費(非有序消費)的實現方式。


/**
 * 普通消息消費
 */
public class Consumer {
 
    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_3");
        consumer.setNamesrvAddr("10.11.11.11:9876;10.11.11.12:9876");
        /**
         * 設置Consumer第一次啓動是從隊列頭部開始消費還是隊列尾部開始消費<br>
         * 如果非第一次啓動,那麼按照上次消費的位置繼續消費
         */
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
 
        consumer.subscribe("TopicTestjjj", "TagA || TagC || TagD");
 
        consumer.registerMessageListener(new MessageListenerConcurrently() {
 
            Random random = new Random();
 
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                System.out.print(Thread.currentThread().getName() + " Receive New Messages: " );
                for (MessageExt msg: msgs) {
                    System.out.println(msg + ", content:" + new String(msg.getBody()));
                }
                try {
                    //模擬業務邏輯處理中...
                    TimeUnit.SECONDS.sleep(random.nextInt(10));
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
 
        consumer.start();
 
        System.out.println("Consumer Started.");
    }
}

輸出:


可見,訂單號爲15103111039的訂單被消費時順序完成亂了。所以用MessageListenerConcurrently這種消費者是無法做到順序消費的,採用下面這種方式就做到了順序消費:


/**
 * 順序消息消費,帶事務方式(應用可控制Offset什麼時候提交)
 */
public class ConsumerInOrder {
 
    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_3");
        consumer.setNamesrvAddr("10.11.11.11:9876;10.11.11.12:9876");
        /**
         * 設置Consumer第一次啓動是從隊列頭部開始消費還是隊列尾部開始消費<br>
         * 如果非第一次啓動,那麼按照上次消費的位置繼續消費
         */
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
 
        consumer.subscribe("TopicTestjjj", "TagA || TagC || TagD");
 
        consumer.registerMessageListener(new MessageListenerOrderly() {
 
            Random random = new Random();
 
            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                context.setAutoCommit(true);
                System.out.print(Thread.currentThread().getName() + " Receive New Messages: " );
                for (MessageExt msg: msgs) {
                    System.out.println(msg + ", content:" + new String(msg.getBody()));
                }
                try {
                    //模擬業務邏輯處理中...
                    TimeUnit.SECONDS.sleep(random.nextInt(10));
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });
 
        consumer.start();
 
        System.out.println("Consumer Started.");
    }
}
輸出:

MessageListenerOrderly能夠保證順序消費,從圖中我們也看到了期望的結果。圖中的輸出是隻啓動了一個消費者時的輸出,看起來訂單號還是混在一起,但是每組訂單號之間是有序的。因爲消息發送時被分配到了三個隊列(參見前面生產者輸出日誌),那麼這三個隊列的消息被這唯一消費者消費。

如果啓動2個消費者呢?那麼其中一個消費者對應消費2個隊列,另一個消費者對應消費剩下的1個隊列。

如果啓動3個消費者呢?那麼每個消費者都對應消費1個隊列,訂單號就區分開了。輸出變爲這樣:

消費者1輸出:

消費者2輸出:

消費者3輸出:

很完美,有木有?!

按照這個示例,把訂單號取了做了一個取模運算再丟到selector中,selector保證同一個模的都會投遞到同一條queue。即: 相同訂單號的--->有相同的模--->有相同的queue。最後就會類似這樣:

總結:

rocketmq的順序消息需要滿足2點:

1.Producer端保證發送消息有序,且發送到同一個隊列。
2.consumer端保證消費同一個隊列。

部分內容圖片引用自https://www.zhihu.com/question/30195969
--------------------- 
作者:earthhour 
來源:CSDN 
原文:https://blog.csdn.net/earthhour/article/details/78323026 
版權聲明:本文爲博主原創文章,轉載請附上博文鏈接!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章