消息顺序发送
消息有序指的是可以按照消息的发送顺序来消费。
RocketMQ可以严格的保证消息有序。但这个顺序,不是全局顺序,只是分区(queue)顺序。要全局顺序只能一个分区。
之所以出现你这个场景看起来不是顺序的,是因为发送消息的时候,消息发送默认是会采用轮询的方式发送到不通的queue(分区)。如图:
话不多说看代码:
首先是顺序消息的生产者
package com.rocketmq.rocketmq.mq.order;
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.MessageQueueSelector;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.remoting.common.RemotingHelper;
import org.apache.rocketmq.remoting.exception.RemotingException;
import java.io.UnsupportedEncodingException;
import java.util.List;
/**
* author wyt 2019-12-10
* 按一定顺序发消息
*/
public class OrderProducer {
public static void main(String[] args) throws MQClientException, UnsupportedEncodingException, RemotingException, InterruptedException, MQBrokerException {
//1.创建消息生产者
DefaultMQProducer defaultMQProducer=new DefaultMQProducer("demo_producer_group");//指定消息发送组
//2.设置Nameser 的地址
defaultMQProducer.setNamesrvAddr("localhost:9876");
//3.开启defaultMQProducer
defaultMQProducer.start();//此处有异常需要抛出
//5.发送消息(顺序发消息 需要制定消息队列 不认无法保持顺序)
//第一个参数是发送的消息信息
//第二个参数是选中指定的消息队列对象(会传入所有的消息队列)
//第三个参数是指定对应的消息队列的下标
/**
* 循环多次发送验证
*/
for (int i = 0; i <6 ; i++) {
//4.创建新消息
//注意选择 导入这个包的:org.apache.rocketmq.common.message.Message;
//public Message(String topic, String tags, String keys, byte[] body)
//body:就是你需要发送的消息
//RemotingHelper.DEFAULT_CHARSET 设置UTF_8的编码格式
Message message =new Message(
"Topic_Order_Demo",//topic:主题
"Tags_Order_Demo",//tags:标签(主要用于消息过滤作用)
"Keys_Order_1",//消息的唯一值
("hello_Order!"+i).getBytes(RemotingHelper.DEFAULT_CHARSET)); //body:就是你需要发送的消息
SendResult result = defaultMQProducer.send(
message,
new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
//你想要的指定的队列的下标 就是下面的数值1
Integer index=(Integer) o;
//返回队列
return list.get(index);
}
},
1
);
System.out.println("Order有序消息发送结果 :"+result);
}
//6.关闭消息发送对像
defaultMQProducer.shutdown();
}
}
在这里我循环发送多条消息:
Order有序消息发送结果 :SendResult [sendStatus=SEND_OK, msgId=C0A801E1302818B4AAC231F59F020000, offsetMsgId=C0A801E100002A9F0000000000002184, messageQueue=MessageQueue [topic=Topic_Order_Demo, brokerName=USER-20170315QN, queueId=1], queueOffset=30]
Order有序消息发送结果 :SendResult [sendStatus=SEND_OK, msgId=C0A801E1302818B4AAC231F59F1C0001, offsetMsgId=C0A801E100002A9F0000000000002257, messageQueue=MessageQueue [topic=Topic_Order_Demo, brokerName=USER-20170315QN, queueId=1], queueOffset=31]
Order有序消息发送结果 :SendResult [sendStatus=SEND_OK, msgId=C0A801E1302818B4AAC231F59F1E0002, offsetMsgId=C0A801E100002A9F000000000000232A, messageQueue=MessageQueue [topic=Topic_Order_Demo, brokerName=USER-20170315QN, queueId=1], queueOffset=32]
Order有序消息发送结果 :SendResult [sendStatus=SEND_OK, msgId=C0A801E1302818B4AAC231F59F240003, offsetMsgId=C0A801E100002A9F00000000000023FD, messageQueue=MessageQueue [topic=Topic_Order_Demo, brokerName=USER-20170315QN, queueId=1], queueOffset=33]
Order有序消息发送结果 :SendResult [sendStatus=SEND_OK, msgId=C0A801E1302818B4AAC231F59F250004, offsetMsgId=C0A801E100002A9F00000000000024D0, messageQueue=MessageQueue [topic=Topic_Order_Demo, brokerName=USER-20170315QN, queueId=1], queueOffset=34]
Order有序消息发送结果 :SendResult [sendStatus=SEND_OK, msgId=C0A801E1302818B4AAC231F59F260005, offsetMsgId=C0A801E100002A9F00000000000025A3, messageQueue=MessageQueue [topic=Topic_Order_Demo, brokerName=USER-20170315QN, queueId=1], queueOffset=35]
可以看到,因为我在代码里指定了队列,所以发送的所有消息全部放在了queueId=1这个队列。
消息的顺序消费
消费端消费的时候,是会分配到多个queue的,多个queue是同时拉取提交消费。如图:
顺序消息的消费者会按照发送的先后顺序来消费消息(可能有点拗口大家理解下)
package com.rocketmq.rocketmq.mq.order;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.*;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.remoting.common.RemotingHelper;
import java.io.UnsupportedEncodingException;
import java.util.List;
/**
* author wyt 2019-12-10
* 按一定顺序接受消息
*/
public class OrderConsumer {
public static void main(String[] args) throws MQClientException {
// 1.创建一个DefaultMQPushConsumer
DefaultMQPushConsumer defaultMQPushConsumer=new DefaultMQPushConsumer("demo_producer_group");
// 2.设置NameSerADD地址
defaultMQPushConsumer.setNamesrvAddr("localhost:9876");
// 3.设置subscribe ,这里要读取主题信息
defaultMQPushConsumer.subscribe(
"Topic_Order_Demo",//指定要消费的消息主题
"Tags_Order_Demo" //过滤规则
);
// 4.创建消息监听MessageListener
// 设置消息拉去上限
defaultMQPushConsumer.setConsumeMessageBatchMaxSize(10);
defaultMQPushConsumer.setMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> list, ConsumeOrderlyContext consumeOrderlyContext) {
{
// 5.获取消息信息
//迭代消息信息
for (MessageExt mes:list
) {
try {
//获取主题
String Topic=mes.getTopic();
//获取标签
String tags=mes.getTags();
//获取消息
byte[] body=mes.getBody();
String message=new String(body, RemotingHelper.DEFAULT_CHARSET);
System.out.println("Order_Consumer 有序消费信息---topic : "+Topic+"tags : "+tags+" message: "+message);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
//发生消费消息异常 进行从事机制
return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
}
}
// 6.返回消息读取状态
//说明完成消息消费
return ConsumeOrderlyStatus.SUCCESS;
}
}
});
// 开启消息
defaultMQPushConsumer.start();
}
}
启动消费端,控制台打印结果如下:
Order_Consumer 有序消费信息---topic : Topic_Order_Demotags : Tags_Order_Demo message: hello_Order!0
Order_Consumer 有序消费信息---topic : Topic_Order_Demotags : Tags_Order_Demo message: hello_Order!1
Order_Consumer 有序消费信息---topic : Topic_Order_Demotags : Tags_Order_Demo message: hello_Order!2
Order_Consumer 有序消费信息---topic : Topic_Order_Demotags : Tags_Order_Demo message: hello_Order!3
Order_Consumer 有序消费信息---topic : Topic_Order_Demotags : Tags_Order_Demo message: hello_Order!4
Order_Consumer 有序消费信息---topic : Topic_Order_Demotags : Tags_Order_Demo message: hello_Order!5
以上可以直观的看到,消息确实被消费者顺序消费了
消息的事务处理
分布式消息队列RocketMQ–事务消息–解决分布式事务的最佳实践
说到分布式事务,就会谈到那个经典的”账号转账”问题:2个账号,分布处于2个不同的DB,或者说2个不同的子系统里面,A要扣钱,B要加钱,如何保证原子性?
一般的思路都是通过消息中间件来实现“最终一致性”:A系统扣钱,然后发条消息给中间件,B系统接收此消息,进行加钱。
但这里面有个问题:A是先update DB,后发送消息呢? 还是先发送消息,后update DB?
假设先update DB成功,发送消息网络失败,重发又失败,怎么办?
假设先发送消息成功,update DB失败。消息已经发出去了,又不能撤回,怎么办?
所以,这里下个结论: 只要发送消息和update DB这2个操作不是原子的,无论谁先谁后,都是有问题的。
那这个问题怎么解决呢??
为了能解决该问题,同时又不和业务耦合,RocketMQ提出了“事务消息”的概念。
具体来说,就是把消息的发送分成了2个阶段:Prepare阶段和确认阶段。
具体来说,上面的2个步骤,被分解成3个步骤:
(1) 发送Prepared消息
(2) update DB
(3) 根据update DB结果成功或失败,Confirm或者取消Prepared消息。
可能有人会问了,前2步执行成功了,最后1步失败了怎么办?这里就涉及到了RocketMQ的关键点:RocketMQ会定期(默认是1分钟)扫描所有的Prepared消息,询问发送方,到底是要确认这条消息发出去?还是取消此条消息?
具体实现逻辑如下:
(具体事务操作根据自己项目实际情况来写,在这里不做详细说明)
生产者:
package com.rocketmq.rocketmq.mq.transaction;
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.*;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.common.RemotingHelper;
import org.apache.rocketmq.remoting.exception.RemotingException;
import java.io.UnsupportedEncodingException;
import java.util.concurrent.*;
/**
* 消息发送的事务管理
* author wyt 2019-12-10
*/
public class TransactionProducer {
public static void main(String[] args) throws MQClientException, UnsupportedEncodingException, RemotingException, InterruptedException, MQBrokerException {
//1.创建消息生产者
// DefaultMQProducer defaultMQProducer=new DefaultMQProducer("demo_producer_group");//指定消息发送组
TransactionMQProducer producer=new TransactionMQProducer("demo_producer_transaction_group");//指定消息发送组
//2.设置Nameser 的地址
producer.setNamesrvAddr("localhost:9876");
//指定消息监听对象,用于执行本地事务和消息回查
TransactionListener transactionListener=new TransactionListenerImpl();
//监听对象给生产者
producer.setTransactionListener(transactionListener);
//线程池
ExecutorService executorService=new ThreadPoolExecutor(
2,
5,
100,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(
2000),
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread=new Thread(r);
thread.setName("线程池事务处理");
return null;
}
}
);
//把线程池给生产者
producer.setExecutorService(executorService);
//3.开启defaultMQProducer
producer.start();//此处有异常需要抛出
//4.创建新消息
//注意选择 导入这个包的:org.apache.rocketmq.common.message.Message;
//public Message(String topic, String tags, String keys, byte[] body)
//body:就是你需要发送的消息
//RemotingHelper.DEFAULT_CHARSET 设置UTF_8的编码格式
Message message =new Message(
"Topic_Demo_Transaction",//topic:主题
"Tags_Demo_Transaction",//tags:标签(主要用于消息过滤作用)
"Keys_1",//消息的唯一值
"hello!-Transaction".getBytes(RemotingHelper.DEFAULT_CHARSET)); //body:就是你需要发送的消息
//5.发送事务消息
TransactionSendResult transactionSendResult = producer.sendMessageInTransaction(message,"hello_transaction");
System.out.println("消息发送结果 :"+transactionSendResult);
//6.关闭消息发送对像
producer.shutdown();
}
}
事务监听:
package com.rocketmq.rocketmq.mq.transaction;
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.concurrent.ConcurrentHashMap;
public class TransactionListenerImpl implements TransactionListener {
private ConcurrentHashMap<String,Integer> localTransac=new ConcurrentHashMap<String,Integer>();
/**
* 执行本地事务
* @param message
* @param o
* @return
*/
@Override
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
//回调
String transactionId = message.getTransactionId();//获取 事务ID
//设置事务状态 0:表示执行中 状态未知 1:表示本地事务执行成功 2:本地事务执行失败
localTransac.put(transactionId,0);
//业务执行,处理本地事务 service
System.out.println("hello--执行本地事务");
try {
System.out.println("正在执行本地事务----------------");
Thread.sleep(75000);
System.out.println("本地事务执行成功----------------");
localTransac.put(transactionId,1);
} catch (InterruptedException e) {
e.printStackTrace();
localTransac.put(transactionId,2);//本地事务执行失败
return LocalTransactionState.ROLLBACK_MESSAGE;//进行回滚
}
return LocalTransactionState.COMMIT_MESSAGE;//本地事务执行完成 进行提交
}
/**
* 消息回查 及回查消息的执行状态
* @param messageExt
* @return
*/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
String transactionId = messageExt.getTransactionId();//获取 事务ID
//获取对应事务ID的执行状态
Integer statu=localTransac.get(transactionId);
System.out.println("消息回查 ----- "+transactionId+" ---- 消息状态 ----- "+statu);
//0:表示执行中 状态未知 1:表示本地事务执行成功 2:本地事务执行失败
switch(statu){
case 0:
return LocalTransactionState.UNKNOW;
case 1:
return LocalTransactionState.COMMIT_MESSAGE;
case 2:
return LocalTransactionState.ROLLBACK_MESSAGE;
}
return LocalTransactionState.UNKNOW;
}
}
消费者:
package com.rocketmq.rocketmq.mq.transaction;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.remoting.common.RemotingHelper;
import java.io.UnsupportedEncodingException;
import java.util.List;
public class TransactionConsumer {
public static void main(String[] args) throws MQClientException {
// 1.创建一个DefaultMQPushConsumer
DefaultMQPushConsumer defaultMQPushConsumer=new DefaultMQPushConsumer("demo_producer_transaction_group");
// 2.设置NameSerADD地址
defaultMQPushConsumer.setNamesrvAddr("127.0.0.1:9876");
// 3.设置subscribe ,这里要读取主题信息
defaultMQPushConsumer.subscribe(
"Topic_Demo_Transaction",//指定要消费的消息主题
"Tags_Demo_Transaction" //过滤规则
);
// 4.创建消息监听MessageListener
// 设置消息拉去上限
defaultMQPushConsumer.setConsumeMessageBatchMaxSize(2);
defaultMQPushConsumer.setMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
// 5.获取消息信息
//迭代消息信息
for (MessageExt mes:list
) {
try {
//获取主题
String Topic=mes.getTopic();
//获取标签
String tags=mes.getTags();
//获取消息
byte[] body=mes.getBody();
String message=new String(body, RemotingHelper.DEFAULT_CHARSET);
System.out.println("Consumer消费带事务的信息---topic : "+Topic+"tags : "+tags+" message: "+message);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
//发生消费消息异常 进行从事机制
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
}
// 6.返回消息读取状态
//说明完成消息消费
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 开启消息
defaultMQPushConsumer.start();
}
}
首先运行生产者:
com.rocketmq.rocketmq.mq.transaction.TransactionProducer
hello--执行本地事务
正在执行本地事务----------------
本地事务执行成功----------------
消息发送结果 :SendResult [sendStatus=SEND_OK, msgId=C0A801E116B818B4AAC235E08A780000, offsetMsgId=null, messageQueue=MessageQueue [topic=Topic_Demo_Transaction, brokerName=USER-20170315QN, queueId=0], queueOffset=34]
运行消费者: