Springboot+RocketMQ+消息顺序发送与顺序消费及其事务控制

消息顺序发送

消息有序指的是可以按照消息的发送顺序来消费。
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]

运行消费者:

在这里插入图片描述

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
很抱歉,我之前理解有误,MQTT与RocketMQ并不是一样的东西,它们只是不同的消息传递协议。如果您想要在SpringBoot项目中实现RocketMQ和MQTT的消息发送和接收,需要分别集成RocketMQ和MQTT的客户端。 首先,您需要在SpringBoot项目中添加RocketMQ和MQTT客户端的依赖,可以在pom.xml文件中添加如下代码: ```xml <!-- RocketMQ --> <dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-client</artifactId> <version>4.8.0</version> </dependency> <!-- MQTT --> <dependency> <groupId>org.eclipse.paho</groupId> <artifactId>org.eclipse.paho.client.mqttv3</artifactId> <version>1.2.5</version> </dependency> ``` 接下来,您需要在application.properties中配置RocketMQ和MQTT的连接信息,例如: ```properties # RocketMQ rocketmq.name-server=127.0.0.1:9876 # MQTT mqtt.username=admin mqtt.password=admin mqtt.url=tcp://127.0.0.1:1883 ``` 然后,您可以通过注入RocketMQ的DefaultMQProducer和DefaultMQPushConsumer来实现消息发送和接收。例如: ```java // RocketMQ @Autowired private DefaultMQProducer producer; public void sendMessage(String topic, String message) throws Exception { Message msg = new Message(topic, message.getBytes(StandardCharsets.UTF_8)); SendResult sendResult = producer.send(msg); System.out.printf("%s%n", sendResult); } @Autowired private DefaultMQPushConsumer consumer; public void receiveMessage(String topic) throws Exception { consumer.subscribe(topic, "*"); consumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) { System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); consumer.start(); } // MQTT @Autowired private MqttClient mqttClient; public void sendMessage(String topic, String message) throws Exception { MqttMessage mqttMessage = new MqttMessage(message.getBytes()); mqttMessage.setQos(2); mqttClient.publish(topic, mqttMessage); } @MessageEndpoint public class MqttMessageReceiver { @Autowired private MyService myService; @ServiceActivator(inputChannel = "mqttInputChannel") public void receiveMessage(String message) { myService.handleMessage(message); } } ``` 以上代码中,sendMessage方法用于发送消息,receiveMessage方法用于接收消息。使用DefaultMQProducer和DefaultMQPushConsumer可以很方便地发送和接收RocketMQ消息,使用MqttClient和MqttMessageReceiver可以很方便地发送和接收MQTT消息。 这就是SpringBoot集成RocketMQ和MQTT实现消息发送和接收的基本流程,希望对您有所帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值