本博客主要是以代码示例来了解顺序消费的相关内容,建议在此之前先了解下顺序消费的原理。

注:RocketMQ可以严格的保证消息有序,但这个顺序,不是全局顺序,只是分区(queue)顺序,如果想要全局顺序,那么需要保证只有一个分区。

顺序消费简介

1.普通顺序消费

顺序消费的一种,正常情况下可以保证完全的顺序消息,但是一旦发生通信异常,Broker重启,由于队列总数法还是能变化,哈希取模后定位的队列会变化,产生短暂的消息顺序不一致。

2.严格顺序消息

顺序消息的一种,无论正常异常情况都能保证顺序,但是牺牲了分布式failover特性,即broker集群中要有一台机器不可用,则整个集群都不可用,服务可用性大大降低。如果服务器部署为同步双写模式,此缺陷可通过备机自动切换为主避免,不过仍然会存在几分钟的服务不可用。

目前已知的应用只有数据库binlog同步强依赖严格顺序消息,其他应用绝大部分都可以容忍短暂乱序,推荐使用普通的顺序消息。

1.producer


 

1. package com.gwd.rocketmq;
2.  
3. import java.io.IOException;
4. import java.text.SimpleDateFormat;
5. import java.util.ArrayList;
6. import java.util.Date;
7. import java.util.List;
8.  
9. import com.alibaba.rocketmq.client.exception.MQBrokerException;
10. import com.alibaba.rocketmq.client.exception.MQClientException;
11. import com.alibaba.rocketmq.client.producer.DefaultMQProducer;
12. import com.alibaba.rocketmq.client.producer.MessageQueueSelector;
13. import com.alibaba.rocketmq.client.producer.SendResult;
14. import com.alibaba.rocketmq.common.message.Message;
15. import com.alibaba.rocketmq.common.message.MessageQueue;
16. import com.alibaba.rocketmq.remoting.exception.RemotingException;
17.  
18. /** 
19. * @FileName Producer.java
20. * @Description:
21. * @author gu.weidong
22. * @version V1.0
23. * @createtime 2018年7月3日 上午9:59:38 
24. * 修改历史:
25. * 时间 作者 版本 描述
26. *==================================================== 
27. *
28. */
29. /**
30. * Producer,发送顺序消息
31. */
32. public class Producer {
33.  
34. public static void main(String[] args) throws IOException {
35. try {
36. DefaultMQProducer producer = new DefaultMQProducer("sequence_producer");
37.  
38. producer.setNamesrvAddr("192.168.159.128:9876;192.168.159.129:9876");
39.  
40. producer.start();
41.  
42. String[] tags = new String[] { "TagA", "TagC", "TagD" };
43.  
44. // 订单列表
45. List<OrderDO> orderList = new Producer().buildOrders();
46.  
47. Date date = new Date();
48. SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
49. String dateStr = sdf.format(date);
50. for (int i = 0; i < 10; i++) {
51. // 加个时间后缀
52. String body = dateStr + " Hello RocketMQ " + orderList.get(i).getOrderId()+orderList.get(i).getDesc();
53. Message msg = new Message("SequenceTopicTest", tags[i % tags.length], "KEY" + i, body.getBytes());
54.  
55. SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
56. @Override
57. public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
58. Long id = Long.valueOf((String)arg);
59. long index = id % mqs.size();
60. return mqs.get((int)index);
61. }
62. }, orderList.get(i).getOrderId());//通过订单id来获取对应的messagequeue
63.  
64. System.out.println(sendResult + ", body:" + body);
65. }
66.  
67. producer.shutdown();
68.  
69. } catch (MQClientException e) {
70. e.printStackTrace();
71. } catch (RemotingException e) {
72. e.printStackTrace();
73. } catch (MQBrokerException e) {
74. e.printStackTrace();
75. } catch (InterruptedException e) {
76. e.printStackTrace();
77. }
78. System.in.read();
79. }
80.  
81. /**
82. * 生成模拟订单数据 
83. */
84. private List<OrderDO> buildOrders() {
85. List<OrderDO> orderList = new ArrayList<OrderDO>();
86.  
87. OrderDO OrderDO = new OrderDO();
88. OrderDO.setOrderId("15103111039");
89. OrderDO.setDesc("创建");
90. orderList.add(OrderDO);
91.  
92. OrderDO = new OrderDO();
93. OrderDO.setOrderId("15103111065");
94. OrderDO.setDesc("创建");
95. orderList.add(OrderDO);
96.  
97. OrderDO = new OrderDO();
98. OrderDO.setOrderId("15103111039");
99. OrderDO.setDesc("付款");
100. orderList.add(OrderDO);
101.  
102. OrderDO = new OrderDO();
103. OrderDO.setOrderId("15103117235");
104. OrderDO.setDesc("创建");
105. orderList.add(OrderDO);
106.  
107. OrderDO = new OrderDO();
108. OrderDO.setOrderId("15103111065");
109. OrderDO.setDesc("付款");
110. orderList.add(OrderDO);
111.  
112. OrderDO = new OrderDO();
113. OrderDO.setOrderId("15103117235");
114. OrderDO.setDesc("付款");
115. orderList.add(OrderDO);
116.  
117. OrderDO = new OrderDO();
118. OrderDO.setOrderId("15103111065");
119. OrderDO.setDesc("完成");
120. orderList.add(OrderDO);
121.  
122. OrderDO = new OrderDO();
123. OrderDO.setOrderId("15103111039");
124. OrderDO.setDesc("推送");
125. orderList.add(OrderDO);
126.  
127. OrderDO = new OrderDO();
128. OrderDO.setOrderId("15103117235");
129. OrderDO.setDesc("完成");
130. orderList.add(OrderDO);
131.  
132. OrderDO = new OrderDO();
133. OrderDO.setOrderId("15103111039");
134. OrderDO.setDesc("完成");
135. orderList.add(OrderDO);
136. return orderList;
137. }
138. }

此处需要注意,producer.send(msg, new MessageQueueSelector()),如果需要全局有序,只需要使new MessageQueueSelector().select(List<MessageQueue> mqs, Message msg, Object arg)方法返回值唯一且不变,例如:


 


1. SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
2. @Override
3. public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
4. Long id = Long.valueOf((String)arg);
5. long index = id % mqs.size();
6. return mqs.get((int)index);
7. }
8. }, orderList.get(0).getOrderId());//通过订单id来获取对应的messagequeue

这边获取到的queue永远都是唯一的且确定的(此处只是举个简单的例子,orderList.get(i).getOrderId()改为0亦可)

2.错误的Consumer


 

1. package com.gwd.rocketmq;
2.  
3. import java.util.List;
4. import java.util.Random;
5. import java.util.concurrent.TimeUnit;
6.  
7. import com.alibaba.rocketmq.client.consumer.DefaultMQPushConsumer;
8. import com.alibaba.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
9. import com.alibaba.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
10. import com.alibaba.rocketmq.client.consumer.listener.MessageListenerConcurrently;
11. import com.alibaba.rocketmq.client.exception.MQClientException;
12. import com.alibaba.rocketmq.common.consumer.ConsumeFromWhere;
13. import com.alibaba.rocketmq.common.message.MessageExt;
14.  
15. /** 
16. * @FileName WrongConsumer.java
17. * @Description:
18. * @author gu.weidong
19. * @version V1.0
20. * @createtime 2018年7月3日 下午3:13:16 
21. * 修改历史:
22. * 时间 作者 版本 描述
23. *==================================================== 
24. *
25. */
26. public class WrongConsumer {
27. public static void main(String[] args) throws MQClientException {
28. DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_3");
29. consumer.setNamesrvAddr("192.168.159.128:9876;192.168.159.129:9876");
30. /**
31. * 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费<br>
32. * 如果非第一次启动,那么按照上次消费的位置继续消费
33. */
34. consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
35.  
36. consumer.subscribe("SequenceTopicTest", "TagA || TagC || TagD");
37.  
38. consumer.registerMessageListener(new MessageListenerConcurrently() {
39.  
40. Random random = new Random();
41.  
42. @Override
43. public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
44. System.out.print(Thread.currentThread().getName() + " Receive New Messages: " );
45. for (MessageExt msg: msgs) {
46. System.out.println(msg + ", content:" + new String(msg.getBody()));
47. }
48. try {
49. //模拟业务逻辑处理中...
50. TimeUnit.SECONDS.sleep(random.nextInt(10));
51. } catch (Exception e) {
52. e.printStackTrace();
53. }
54. return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
55. }
56. });
57.  
58. consumer.start();
59.  
60. System.out.println("Consumer Started.");
61. }
62. }

注意:要想要有顺序,那么这边吃监听器就不能是MessageListenerConcurrently了,其显示效果如下:

Java 消费rocketmq 配置topic_java

 

3.正确的Consumer


 

1. package com.gwd.rocketmq;
2.  
3. import java.util.List;
4. import java.util.Random;
5. import java.util.concurrent.TimeUnit;
6.  
7. import com.alibaba.rocketmq.client.consumer.DefaultMQPushConsumer;
8. import com.alibaba.rocketmq.client.consumer.listener.ConsumeOrderlyContext;
9. import com.alibaba.rocketmq.client.consumer.listener.ConsumeOrderlyStatus;
10. import com.alibaba.rocketmq.client.consumer.listener.MessageListenerOrderly;
11. import com.alibaba.rocketmq.client.exception.MQClientException;
12. import com.alibaba.rocketmq.common.consumer.ConsumeFromWhere;
13. import com.alibaba.rocketmq.common.message.MessageExt;
14.  
15. /** 
16. * @FileName Consumer.java
17. * @Description:
18. * @author gu.weidong
19. * @version V1.0
20. * @createtime 2018年7月3日 上午10:05:26 
21. * 修改历史:
22. * 时间 作者 版本 描述
23. *==================================================== 
24. *
25. */
26. /**
27. * 顺序消息消费,带事务方式(应用可控制Offset什么时候提交)
28. */
29. public class Consumer {
30.  
31. public static void main(String[] args) throws MQClientException {
32. DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_3");
33. consumer.setNamesrvAddr("192.168.159.128:9876;192.168.159.129:9876");
34. /**
35. * 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费<br>
36. * 如果非第一次启动,那么按照上次消费的位置继续消费
37. */
38. consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
39.  
40. consumer.subscribe("SequenceTopicTest", "TagA || TagC || TagD");
41.  
42. consumer.registerMessageListener(new MessageListenerOrderly() {
43.  
44. Random random = new Random();
45.  
46. @Override
47. public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
48. context.setAutoCommit(true);
49. System.out.print(Thread.currentThread().getName() + " Receive New Messages: " );
50. for (MessageExt msg: msgs) {
51. System.out.println(msg + ", content:" + new String(msg.getBody()));
52. }
53. try {
54. //模拟业务逻辑处理中...
55. TimeUnit.SECONDS.sleep(random.nextInt(10));
56. } catch (Exception e) {
57. e.printStackTrace();
58. }
59. return ConsumeOrderlyStatus.SUCCESS;
60. }
61. });
62.  
63. consumer.start();
64. System.out.println("Consumer Started.");
65. }
66. }

这边的Consumer和上面的最明显的区别在于对应的监听器是MessageListenerOrderly,MessageListenerOrderly是能够保证顺序消费的。

显示结果:

Java 消费rocketmq 配置topic_java_02

4.多个消费者

那如果有多个消费者呢?因为消息发送时被分配到多个队列,这些队列又会被分别发送给消费者唯一消费,现在启动两个消费者,其消费情况如下图:

Java 消费rocketmq 配置topic_List_03

Java 消费rocketmq 配置topic_List_04

结论:多个消费者时,各个消费者的消息依旧是顺序消费,且不会重复消费