消息有序:
指的是可以按照消息的發送順序來消費(FIFO)。RocketMQ可以嚴格的保證消息有序,可以分爲分區有序或者全局有序。
順序消費的原理解析:
在默認的情況下消息發送會採取Round Robin輪詢方式把消息發送到不同的queue(分區隊列);而消費消息的時候從多個queue上拉取消息,這種情況發送和消費是不能保證順序。
如果控制發送的順序消息只依次發送到同一個queue 中,消費的時候只從這個queue上依次拉取,則就保證了順序。
(1)當發送和消費參與的 queue只有一個,則是全局有序;
(2)如果多個queue參與,則爲分區有序,即相對每個 queue,消息都是有序的。
我個人的理解就是:
默認情況下,發送消息是採用輪詢的方式去發送消息,發送不同的queue(隊列)中,假如數據庫有一張user表中沒有任何數據,此時新增一條數據:id=1,name="張三";再修改爲id=1,name="李四";最後再刪除id=1,name="李四";也就是id爲1的這條數據,經過了三次數據庫操作,如果數據庫的每一次操作都需要發送消息到隊列當中,假設有queueA,queueB,queueC,這時候會將三條消息發送到三個不同的隊列當中,消費的話,也是在三個queue中進行拉取,rocketmq這麼做,你在業務處理是沒有辦法,判斷是先新增呢,還是先修改,還是先刪除,這個順序你就沒有辦法去保證;
有兩種辦法去解決,<1>如果不考慮性能的話,全局消息應該是最容易想到的一種辦法,因爲隊列天然的先進先出的優勢就可以滿足這點;新增一條數據:id=1,name="張三";再修改爲id=1,name="李四";最後再刪除id=1,name="李四";進去隊列是這個順序,取出來也肯定是這個順序。<2>分區消息:就是根據某一個key,比如orderId,userId等這種sharding-key來做分區,拿上面的例子來說,這個id就可以作爲一個sharding-key,根據不同的sharding-key發送到不同的隊列當中,因此在每個隊列當中,這個數據都是有序的。
下面來一張分區消息原理的圖片:
這個原理圖是採用訂單的創建、付款、完成的例子,根據orderId這個key,將orderId=2001訂單的一系列操作發送到其中一個queue,將orderId=3001訂單的一系列操作發送到另一個queue中;這個分配的規則,可以通過取模;
例子:張三和李四到銀行去存取錢,然後銀行發送短信告訴用戶,餘額多少?
分析:肯定是先存錢,賬戶有錢後,才能取錢,取完錢或者存完錢後,銀行給用戶發送短信,這個順序是不能亂的,總不可能用戶賬戶沒有錢,還沒有存,就可以取錢了或者就收到了存錢或者取錢短信了。
生產者代碼:
/**
* @author lucifer
* @date 2020/4/14 14:15
* @description 順序消息 -生產者
*/
public class OrderedProducer {
public static final String NAME_SERVER_ADDR = "192.168.160.131:9876";
public static void main(String[] args) throws MQClientException, UnsupportedEncodingException, RemotingException, InterruptedException, MQBrokerException {
//1.創建生產者對象,並指定組名
DefaultMQProducer defaultMQProducer = new DefaultMQProducer("GROUP_TEST");
//2.指定NameServer地址
defaultMQProducer.setNamesrvAddr(NAME_SERVER_ADDR);
//3.啓動生產者
defaultMQProducer.start();
//設置異步發送失敗重試次數,默認爲2次
defaultMQProducer.setRetryTimesWhenSendAsyncFailed(0);
//4.定義消息隊列選擇器
MessageQueueSelector messageQueueSelector = new MessageQueueSelector() {
/**
* 消息隊列選擇器,保證同一條業務數據的消息在同一隊列當中
* @param list topic中所有隊列的集合
* @param message 發送的消息
* @param o 此參數是本示例中defaultMQProducer.send()中的第三個參數
* @return
*/
@Override
public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
Integer id = (Integer) o;
//通過id與topic中所有隊列的集合的大小進行取模,求出索引值index
int index = id % list.size();
//分區順序:同一個index的消息在同一個隊列當中
return list.get(index);
//全局消息,放在同一個queue
//return list.get(0);
}
};
String[] tags = new String[]{"TagA", "TagB", "TagC"};
List<Map> bizData = getBizData();
//5.循環發送消息
for (int i = 0; i < bizData.size(); i++) {
Map bizMap = bizData.get(i);
// keys:業務數據的ID,比如用戶ID、訂單編號等等
Message message = new Message("TopicTest", tags[i % tags.length],
bizMap.get("userId") + "",
bizMap.toString().getBytes(RemotingHelper.DEFAULT_CHARSET));
//發送有序消息
SendResult sendResult = defaultMQProducer.send(message, messageQueueSelector, bizMap.get("userId"));
System.out.printf("%s, body:%s%n", sendResult, bizMap);
}
}
private static List<Map> getBizData() {
List<Map> orders = new ArrayList<>();
HashMap orderData = new HashMap();
orderData.put("userId", 1000);
orderData.put("userName", "張三");
orderData.put("description", "銀行存錢,存1000");
orders.add(orderData);
orderData = new HashMap();
orderData.put("userId", 1000);
orderData.put("userName", "張三");
orderData.put("description", "短信提示:存1000,賬戶餘額1000");
orders.add(orderData);
orderData = new HashMap();
orderData.put("userId", 1000);
orderData.put("userName", "張三");
orderData.put("description", "銀行取錢,取300");
orders.add(orderData);
orderData = new HashMap();
orderData.put("userId", 1000);
orderData.put("userName", "張三");
orderData.put("description", "短信提示:取300,賬戶餘額700");
orders.add(orderData);
orderData = new HashMap();
orderData.put("userId", 1000);
orderData.put("userName", "張三");
orderData.put("description", "銀行存錢,存400");
orders.add(orderData);
orderData = new HashMap();
orderData.put("userId", 1000);
orderData.put("userName", "張三");
orderData.put("description", "短信提示:存款400,賬戶餘額1100");
orders.add(orderData);
orderData = new HashMap();
orderData.put("userId", 1001);
orderData.put("userName", "李四");
orderData.put("description", "銀行存錢,存600");
orders.add(orderData);
orderData = new HashMap();
orderData.put("userId", 1001);
orderData.put("userName", "李四");
orderData.put("description", "短信提示:存款600,賬戶餘額600");
orders.add(orderData);
return orders;
}
}
啓動生產者,控制檯打印:
(1).如果是全局消息,這裏的queueId是一樣的,放在了同一個queue中;
SendResult [sendStatus=SEND_OK, msgId=24098A1E7A3ECE402C3D4AADC1A10079000018B4AAC2464469920000, offsetMsgId=C0A8A08300002A9F000000000000C1CB, messageQueue=MessageQueue [topic=TopicTest, brokerName=localhost.localdomain, queueId=0], queueOffset=26], body:{description=銀行存錢,存1000, userName=張三, userId=1000}
SendResult [sendStatus=SEND_OK, msgId=24098A1E7A3ECE402C3D4AADC1A10079000018B4AAC2464469980001, offsetMsgId=C0A8A08300002A9F000000000000C2E6, messageQueue=MessageQueue [topic=TopicTest, brokerName=localhost.localdomain, queueId=0], queueOffset=27], body:{description=短信提示:存1000,賬戶餘額1000, userName=張三, userId=1000}
SendResult [sendStatus=SEND_OK, msgId=24098A1E7A3ECE402C3D4AADC1A10079000018B4AAC24644699B0002, offsetMsgId=C0A8A08300002A9F000000000000C414, messageQueue=MessageQueue [topic=TopicTest, brokerName=localhost.localdomain, queueId=0], queueOffset=28], body:{description=銀行取錢,取300, userName=張三, userId=1000}
SendResult [sendStatus=SEND_OK, msgId=24098A1E7A3ECE402C3D4AADC1A10079000018B4AAC24644699D0003, offsetMsgId=C0A8A08300002A9F000000000000C52E, messageQueue=MessageQueue [topic=TopicTest, brokerName=localhost.localdomain, queueId=0], queueOffset=29], body:{description=短信提示:取300,賬戶餘額700, userName=張三, userId=1000}
SendResult [sendStatus=SEND_OK, msgId=24098A1E7A3ECE402C3D4AADC1A10079000018B4AAC24644699F0004, offsetMsgId=C0A8A08300002A9F000000000000C65A, messageQueue=MessageQueue [topic=TopicTest, brokerName=localhost.localdomain, queueId=0], queueOffset=30], body:{description=銀行存錢,存400, userName=張三, userId=1000}
SendResult [sendStatus=SEND_OK, msgId=24098A1E7A3ECE402C3D4AADC1A10079000018B4AAC2464469A10005, offsetMsgId=C0A8A08300002A9F000000000000C776, messageQueue=MessageQueue [topic=TopicTest, brokerName=localhost.localdomain, queueId=0], queueOffset=31], body:{description=短信提示:存款400,賬戶餘額1100, userName=張三, userId=1000}
SendResult [sendStatus=SEND_OK, msgId=24098A1E7A3ECE402C3D4AADC1A10079000018B4AAC2464469A30006, offsetMsgId=C0A8A08300002A9F000000000000C8A4, messageQueue=MessageQueue [topic=TopicTest, brokerName=localhost.localdomain, queueId=0], queueOffset=32], body:{description=銀行存錢,存600, userName=李四, userId=1001}
SendResult [sendStatus=SEND_OK, msgId=24098A1E7A3ECE402C3D4AADC1A10079000018B4AAC2464469A50007, offsetMsgId=C0A8A08300002A9F000000000000C9C0, messageQueue=MessageQueue [topic=TopicTest, brokerName=localhost.localdomain, queueId=0], queueOffset=33], body:{description=短信提示:存款600,賬戶餘額600, userName=李四, userId=1001}
(1).如果是分區消息,這裏的queueId是不一樣的,放在了不同queue中;
SendResult [sendStatus=SEND_OK, msgId=24098A1E7A3ECE402C3D4AADC1A10079000018B4AAC24648E14E0000, offsetMsgId=C0A8A08300002A9F000000000000E653, messageQueue=MessageQueue [topic=TopicTest, brokerName=localhost.localdomain, queueId=8], queueOffset=42], body:{description=銀行存錢,存1000, userName=張三, userId=1000}
SendResult [sendStatus=SEND_OK, msgId=24098A1E7A3ECE402C3D4AADC1A10079000018B4AAC24648E1540001, offsetMsgId=C0A8A08300002A9F000000000000E76E, messageQueue=MessageQueue [topic=TopicTest, brokerName=localhost.localdomain, queueId=8], queueOffset=43], body:{description=短信提示:存1000,賬戶餘額1000, userName=張三, userId=1000}
SendResult [sendStatus=SEND_OK, msgId=24098A1E7A3ECE402C3D4AADC1A10079000018B4AAC24648E1560002, offsetMsgId=C0A8A08300002A9F000000000000E89C, messageQueue=MessageQueue [topic=TopicTest, brokerName=localhost.localdomain, queueId=8], queueOffset=44], body:{description=銀行取錢,取300, userName=張三, userId=1000}
SendResult [sendStatus=SEND_OK, msgId=24098A1E7A3ECE402C3D4AADC1A10079000018B4AAC24648E1590003, offsetMsgId=C0A8A08300002A9F000000000000E9B6, messageQueue=MessageQueue [topic=TopicTest, brokerName=localhost.localdomain, queueId=8], queueOffset=45], body:{description=短信提示:取300,賬戶餘額700, userName=張三, userId=1000}
SendResult [sendStatus=SEND_OK, msgId=24098A1E7A3ECE402C3D4AADC1A10079000018B4AAC24648E15C0004, offsetMsgId=C0A8A08300002A9F000000000000EAE2, messageQueue=MessageQueue [topic=TopicTest, brokerName=localhost.localdomain, queueId=8], queueOffset=46], body:{description=銀行存錢,存400, userName=張三, userId=1000}
SendResult [sendStatus=SEND_OK, msgId=24098A1E7A3ECE402C3D4AADC1A10079000018B4AAC24648E15F0005, offsetMsgId=C0A8A08300002A9F000000000000EBFE, messageQueue=MessageQueue [topic=TopicTest, brokerName=localhost.localdomain, queueId=8], queueOffset=47], body:{description=短信提示:存款400,賬戶餘額1100, userName=張三, userId=1000}
SendResult [sendStatus=SEND_OK, msgId=24098A1E7A3ECE402C3D4AADC1A10079000018B4AAC24648E1620006, offsetMsgId=C0A8A08300002A9F000000000000ED2C, messageQueue=MessageQueue [topic=TopicTest, brokerName=localhost.localdomain, queueId=9], queueOffset=45], body:{description=銀行存錢,存600, userName=李四, userId=1001}
SendResult [sendStatus=SEND_OK, msgId=24098A1E7A3ECE402C3D4AADC1A10079000018B4AAC24648E1650007, offsetMsgId=C0A8A08300002A9F000000000000EE48, messageQueue=MessageQueue [topic=TopicTest, brokerName=localhost.localdomain, queueId=9], queueOffset=46], body:{description=短信提示:存款600,賬戶餘額600, userName=李四, userId=1001}
消費者代碼:
/**
* @author lucifer
* @date 2020/4/14 14:50
* @description 順序消息 -消費者
*/
public class OrderedConsumer {
public static final String NAME_SERVER_ADDR = "192.168.160.131:9876";
public static void main(String[] args) throws MQClientException {
//1.創建消費者
DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer("GROUP_TEST");
//2.設置NameServer地址
defaultMQPushConsumer.setNamesrvAddr(NAME_SERVER_ADDR);
//設置Consumer第一次啓動是從隊列頭部開始消費還是隊列尾部開始消費
// 如果不是第一次啓動,那麼按照上次消費的位置繼續消費
defaultMQPushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
//3.訂閱對應的主題和Tag
defaultMQPushConsumer.subscribe("TopicTest", "TagA || TagB || TagC");
defaultMQPushConsumer.setMaxReconsumeTimes(-1);
//順序消費消息
defaultMQPushConsumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> list,
ConsumeOrderlyContext consumeOrderlyContext) {
consumeOrderlyContext.setAutoCommit(true);
doBiz(list.get(0));
return ConsumeOrderlyStatus.SUCCESS;
}
});
// 5. 啓動消費者(必須在註冊完消息監聽器後啓動,否則會報錯)
defaultMQPushConsumer.start();
System.out.println("已啓動消費者");
}
/**
* 模擬處理業務
*
* @param message
*/
public static void doBiz(Message message) {
try {
System.out.printf("線程:%-25s 接收到新消息 %s --- %s %n", Thread.currentThread().getName(), message.getTags(), new String(message.getBody(), RemotingHelper.DEFAULT_CHARSET));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
}
啓動消費者,控制檯打印:
已啓動消費者
線程:ConsumeMessageThread_1 接收到新消息 TagA --- {description=銀行存錢,存1000, userName=張三, userId=1000}
線程:ConsumeMessageThread_1 接收到新消息 TagB --- {description=短信提示:存1000,賬戶餘額1000, userName=張三, userId=1000}
線程:ConsumeMessageThread_1 接收到新消息 TagC --- {description=銀行取錢,取300, userName=張三, userId=1000}
線程:ConsumeMessageThread_1 接收到新消息 TagA --- {description=短信提示:取300,賬戶餘額700, userName=張三, userId=1000}
線程:ConsumeMessageThread_1 接收到新消息 TagB --- {description=銀行存錢,存400, userName=張三, userId=1000}
線程:ConsumeMessageThread_1 接收到新消息 TagC --- {description=短信提示:存款400,賬戶餘額1100, userName=張三, userId=1000}
線程:ConsumeMessageThread_1 接收到新消息 TagA --- {description=銀行存錢,存600, userName=李四, userId=1001}
線程:ConsumeMessageThread_1 接收到新消息 TagB --- {description=短信提示:存款600,賬戶餘額600, userName=李四, userId=1001}