目录

  • 二、RocketMQ快速入门
  • 1、消息生产和消费介绍
  • 2、工程创建
  • 3、RockerMQ普通消息消费者
  • 1、消息消费
  • 4、RocketMQ顺序消息
  • 1、服务端
  • 2、消费端
  • 5、RocketMQ事务消息
  • 1、RocketMQ事务消息流程
  • 2、事务消息生产者
  • 3、事务消息消费者
  • 4、RocketMQ实现分布式事务流程
  • 6、消息广播/批量发送
  • 1、消息生产者
  • 2、消费端


二、RocketMQ快速入门

1、消息生产和消费介绍

  1. 使用RocketMQ可以发送普通消息、顺序消息、事务消息,顺序消息能实现有序消费,事务消息可以解决分布式事务实现数据最终一致。
  2. RocketMQ有两种常见的消费模式,分别时DefaultMQPushConsumer和DefaultMQPullConsumer模式,这2种模式仔面理解一个是推送消息,一个是拉去消息。这里有个误区,其实无论是Push还是Pull,其本质都是拉去消息,只是实现机制不一样。
  1. DefaultMQPushCOnsumer其实并部署broker主动向consumer推送消息,而是consumer向broker发出请求,保持了一种长链接,broker会每5秒检测一次是否有消息,如果有消息,则将消息推送给consumer,使用DefauleMQPushCOnsumer实现消息消费,broker会主动记录消息消费的偏移量
  2. DefauleMQPullConsumer是消费方主动取broker拉去数量,一般会在本地使用定时任务实现,使用它获取消息状态方便、负载均衡性能可控,单消息的及时性差,而且需要手动记录消息消费的偏移量信息,所以在工作中多数情况推荐使用Push模式
  1. RocketMQ发送的消息默认会存储到4个队列中,当然创建几个队列存储数据,可以自己定义

RocketMQ作为MQ消息中间件,ack机制必不可少,在RocketMQ种常见的应答状态如下

//主要针对事务消息的应答状态
public enum LocalTransactionState{
    COMMIT_MESSAGE,		//消息提交
    ROLLBACK_MESSAGE,	//消息回滚
    UNKNOW;				//未知状态,一般用于处理超时等现象
}

//主要针对消息消费的应答状态
public enum ConsumeConcurrentlyStatus{
    //消息消费成功
    CONSUME_SUCCESS,
    
    //消息重试,一般消息消费失败后,RocketMQ为了保证数据的可靠性,具有重试机制
    RECONSUME_LATER;
}

重发时间是:(broker.log中有)

messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

2、工程创建

我们先实现一个最基本的消息发送,先创建一个工程,引入依赖

<dependency>
	<groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-client</artifactId>
    <version>4.4.0</version>
</dependency>

消息发送有这么几个步骤

  1. 创建DefaultMQPRoducer
  2. 设置Namesrv地址
  3. 开启DefaultMQProducer
  4. 创建消息Message
  5. 发送消息
  6. 关闭defaultMQProducer

我们创建一个Producer类,按照上面步骤实现消息发送,代码如下

public class Producer{
    //指定Namesrv地址
    private static String NAMESRV_ADDRESS = "192.168.179.128:9876";
    
    public static void main(String[] args) throws Exception{
        //创建一个DefaultMQProducer,需要指定消息发送组
        DefaultMQProducer producer = new DefaultMQProducer("Test_Quick_Producer_Name");
        //指定NameSrv地址
        producer.setNamesrvAddr(NAMESRV_ADDRESS);
        //启动Producer
        producer.start();
        
        //创建消息
        Message message = new Message(
        	"Test_Quick_Topic",			//主题
            "TagA",						//标签,可以用来做过滤
            "KeyA",						//唯一标识,可以用来查找消息
            "hello rocketmq".getBytes()	//要发送的消息字节数组
        );
        //发送消息
        SendResult result = producer.send(message);
        //关闭DefaultMQProducer
        producer.shutdown();
    }
}

3、RockerMQ普通消息消费者

1、消息消费

消费者消费消息有这么几个步骤

  1. 创建DefaultMQPushConsumer
  2. 设置namesrv地址
  3. 设置subscribe,这里是要读取的主题信息
  4. 创建消息监听MessageListener
  5. 获取消息信息
  6. 返回消息读取状态

创建Consumer类,按照上面不住实现消息消费,代码如下

public class Consumer{
    //指定Namesrv地址
    private static String NAMESRV_ADDRESS = "192.168.179.128:9876";
    
    public static void main(String[] args) throws Exception{
        //创建一个DefaultMQProducer,需要指定消息发送组
        DefaultMQConsumer consumer = new DefaultMQConsumer("Test_Quick_Consumer_Name");
        //指定NameSrv地址
        consumer.setNamesrvAddr(NAMESRV_ADDRESS);
  
        //设置要读取的topic
        consumer.subscribe(
        	"Test_Quick_Topic",	//指定要读取的消息主题
            "TagA");			//指定要读取的消息过滤信息,多个标签数据,则可以输入"tag1 || tag2"
        
        //创建消息监听
        consumer.setMessageListener(new MEssageListenerConcurrently(){
           @Override
           public ConsumeConcurrentlyStatus consumeMessage(
              		 List<MessageExt> msgs,ConsumeConcurrentlyContext context){
           		//迭代消息信息
               for(MessageExt msg : msgs){
                   try{
                       //获取主题
                       String topic = msg.getTopic();
                       //获取标签
                       String tags = msg.getTags();
                       //获取信息
                       String result = 
                           new String(msg.getBody(),RomtingHelper.DEFALUT_CHARSET);
                       System.out.println("Consumer消费信息------topic:"
                           +topic+",tags:"+tags+",result:"+result);
                   }catch (UnsupportedEncodingException e){
                       e.printStackTrace();
                       //消息重试
                       return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                   }
               }
               //返回消息读取状态
               //消息消费完成
               return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
           }
        });
        //开启Consumer
		consumer.start();
    }
}

一个主题在MQ上默认会有4个Queue队列来存储该主题上的消息,Queue的数量也可以在创建主题时指定

4、RocketMQ顺序消息

消息有序指的是可以按照消息的发送顺序来消费。**RocketMQ可以严格的保证消息有序。但这个顺序,不是全局顺序,只是分区(queue)顺序。要全局顺序只能一个分区。**之所以出现你这个场景看起来不是顺序的,是因为发送消息的时候,消息发送默认是会采用轮询的方式发送到不通的queue(分区)。如图:

rocketmq幂等 redis rocket mq java_rocketmq幂等 redis

如何保证顺序

在MQ的模型中,顺序需要由3个阶段去保障

  1. 消息被发送时保持顺序
  2. 消息备存储时保持和发送的顺序一致
  3. 消息被消费时保持和存储的顺序一致

发送时保持顺序意味着对于有顺序要求的消息,用户应该在同一个线程中采用同步的方式发送。存储保持和发送的顺序一致则要求在同一线程中被发送出来的消息A和B,存储时在空间上A一定在B之前。而消费保持和存储一致则要求消息A、B到达Consumer之后必须按照先A后B的顺序被处理

rocketmq幂等 redis rocket mq java_队列_02

1、服务端

// 发送消息
// 第一个参数:发送的消息信息
// 第二个参数:选中指定的消息队列对象(会将所有消息队列传入进来)
// 第三个参数:指定对应的队列下标
SendResult result = producer.send(
	message,
    new MessageQueueSelector(){
        @Override
        public MessageQueue select(List<MessageQueue> mqs,Message msg,Object arg){
            //获取队列的下标
            Integer index = (Integer)arg;
            //获取对应下标的队列
            return mqs.get(index);
        }
    },0
);
System.out.println(result);

2、消费端

// 创建消息监听MessageListener
consumer.setMessageListener(new MessageListenerOrderly(){
   @override
   public ConsumeOrderlyStatus consumeMessage(List<MessageExt msgs,
            ConsumeOrderlyContext context) {
     	//迭代消息信息
               for(MessageExt msg : msgs){
                   try{
                       //获取主题
                       String topic = msg.getTopic();
                       //获取标签
                       String tags = msg.getTags();
                       //获取信息
                       String result = 
                           new String(msg.getBody(),RomtingHelper.DEFALUT_CHARSET);
                       System.out.println("Consumer消费信息------topic:"
                           +topic+",tags:"+tags+",result:"+result);
                   }catch (UnsupportedEncodingException e){
                       e.printStackTrace();
                       //消息重试
                       return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
                   }
               }
               //返回消息读取状态
               //消息消费完成
               return ConsumeOrderlyStatus.SUCCESS;
           }
        });
        //开启Consumer
		consumer.start();
});

5、RocketMQ事务消息

在RocketMQ4.3.0版本后,开放了事务消息这一特性,对于分布式事务而言,最常说的还是二阶段提交协议。

1、RocketMQ事务消息流程

RocketMQ的事务消息,主要是通过消息的异步处理,可以保证本地事务和消息发送同时成功执行或失败,从而保证数据的最终一致性,这里我们先看看一条事务消息从诞生到结束的整个事件线流程

rocketmq幂等 redis rocket mq java_System_03

事务消息的成功投递是需要经历三个Topic的,分别是:
	Half Topic:用于记录所有的prepare消息
	Op Half Topic:记录已经提交了状态的prepare消息
	Real Topic:事务消息真正的Topic,在Commit后才会将消息写入该Topic,从而进行消息的投递

2、事务消息生产者

我们创建一个事务消息生产者TransactionProducer,事务消息发送消息对象是TransactionMQProducer,为了实现本地事务操作和回查,我们需要创建一个监听器,监听器需要实现TransactionListener接口,实现代码如下

public class TransactionListenerImpl implements TransactionListener{
    //存储当前线程对应的事务状态
    private ConcurrentHashMap<String,Integer> localTrans = new ConcurrentHashMap<>();
    
    /**
     * 发送prepare消息成功后回调该方法用于执行本地事务
     * @Param msg:回传的消息,利用即可获取到该消息的唯一ID
     * @Param arg:调用send方法时传递的参数,当send时候若有额外的参数可以传递到send方法中,这里能获取到
     */
    @Override
    public LocalTransactionState executeLocalTransaction(message msg,Object arg){
        //获取线程ID
        String transactionId = msg.getTransactionId();
        //初始状态为0
        localTrans.put(transactionId,0);
        //处理本地事务,这一块可以调用service层,实现本地相关操作
        try{
            //此处执行本地事务操作
            System.out.println("...执行本地事务");
            Thread.sleep(70000);
            System.out.println("...执行完成本地事务");
        }catch(InterruptedException e){
            e.printStackTrace();
            localTrans.put(transactionId,2);
            return LocalTransactionState.UNKNOW;
        }
        
        //修改状态
        localTrans.put(transactionId,1);
        System.out.println("executeLocalTransaction------------状态为1");
        //本地事务操作如果成功了,则提交该消息,让该消息可见
        return LocalTransactionState.UNKNOW;
    }
    
    /**
     * 消息回查
     * @param msg
     */
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg){
        //获取事务Id
        String transactionid = msg.getTransactionId();
        
        //通过事务Id获取对应的本地事务执行状态
        Integer status = localTrans.get(transactionid);
        System.out.println("消息回查-----"+status);
        switch(status){
            //消息回查,当本地事务超时,broker会做消息回查操作
            case 0:
                return LocalTransactionState.UNKNOW;
            case 1:
                return LocalTransactionState.COMMIT_MESSAGE;
            case 2:
                return LocalTransactionState.ROLLBACK_MESSAGE;
            default:
                return LocalTransactionState.UNKNOW;
    }
}

创建消息发送者

public class TransactionProducer{
    
    //nameserver地址
    private static String namesrvaddress = "192.168.179.128:9876";
    
    public static void main(String[] args) throws Exception{
        //创建事务消息发送对象
        TransactionMQProducer producer = 
            new TransactionMQProducer("transaction_producer_group_name");
        //设置namesrv地址
        producer.setNamesrcAddr(namesrvaddress);
        //创建监听器
        TransactionListener transactionListener = new TransactionListenerImpl();
        //创建线程池
        ExecutorService executorService = new ThreadPoolExecutor(
        	2,
            5,
            100,
            TimeUnit.SECONTS,
            new ArrayBlockingQueue<Runnable>(2000)
            new ThreadFactory(){
                @Overrid
                public Thread new Thread(Runnable runnable){
                    Thread thread = new Trhead(runnable);
                    thread.setName("client-transaction-msg-check-thread");
                    return thread;
                }
            }
        );
        //设置线程池
        producer.setExecutorService(executorService);
        //设置监听器
        producet.setTransactionListener(transactionListener);
        //启动producer
        producer.start();
        
        //创建消息
        Message message = new Message(
        	"TopicTxt_demo",
            "TagTx",
            "KeyTx1",
            "hello".getBytes(RomtingHelper.DEFAULT_CHARSET));
        
        //发送事务消息,此时消息不可见
        TransactionSendResult transactionSendResult = producer.sendMessageInTransaction(
            message,"发送消息,回传所需数据!");
        System.out.println(transactionSendResult);
        
        //休眠
        Thread.sleep(120000);
        //关闭
        producer.shutdown();
    }
}

3、事务消息消费者

public class TransactionConsumer{
    
    //namesrv地址
    private static String namesrcaddress="192.168.179.128:9876";
    
    public static void main(String[] args) throws MQClientExcept{
        //创建DefaultMQPushConsumer
        DefaultMQPushConsumer consume = new 
            DefaultMQPushCOnsumer("transaction_consumer_group_name");
        //设置namesrv地址
        consumer.setNamesrcAddr(namesrcaddress);
        //设置每次拉取的消息个数
        consumer.subscribe("TopicTxt_Demo","TagTx");
        //消息监听
        consumer.registerMessageListener(new MessageListenerConcurrently(){
           @Override
           public ConsumeConcurrentlyStatus consumeMessage(
               		List<MessageExt> msgs,ConsumeConcurrentlyContext context){
               try{
                   for(Message msg : msgs){
                       String topic = msg.getTopic();
                       String tags = msg.getTags();
                       String keys = msg.getKeys();
                       String body = 
                           new String(msg.getBody(),RemotingHelper.DEFAULT_CHARSET);
                       System.out.println("topic:"+topic+",tags:"+tags
                                        +",keys:"+keys+",body:"+body);
                   }
               }catch(UnsupportedEncodingException e){
                   e.printStackTrace();
               }
               return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
           }
        });
    }
}

4、RocketMQ实现分布式事务流程

MQ事务消息解决分布式事务问题,但第三方MQ支持事务消息的中间件不多,比如RocketMQ,他们支持事务消息的方式也是类似于采用的二阶段提交。

以阿里的RocketMQ中间件为例,其思路大致为:

  1. 第一阶段Prepared消息,会拿到消息的地址。
  2. 第二阶段执行本地事务,第三阶段通过第一阶段拿到的地址去访问消息,并修改状态

也就是说在业务方法内想要消息队列提交两次请求,一次发送消息和一次确认消息,如果确认消息发送失败了RocketMQ会定期扫描消息集群中的事务消息,这时候发现了Prepared消息,他会向消息发送者确认,所以生产方需要实现一个check接口,RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送和本地事务同时成功或同时失败。

rocketmq幂等 redis rocket mq java_System_04

6、消息广播/批量发送

上面发送消息,我们测试的时候,可以发现消息只有一个消费者能收到,如果我们想要实现消息广播,让每个消费者都能收到消息也是可以实现的,而且上面发送消息的时候,每次都是发送单条Message对象,能否批量发送呢,答案是可以的

1、消息生产者

public class BroadcastingProducer{
    
    //namesrv地址
    private static String namesrvaddress="192.168.179.128:9876";
    
    public static void main(String[] args) throws Exception{
        //创建DefaultMQProducer
        DefauleMQProducer producer = new DefauleMQProducer("broadcasting_producer_group");
        //指定nameserver地址
        producer.setNamesrcAddr(namesrvaddress);
        
        //启动
        producer.start();
        
        //创建消息
        List<Message> messages = new ArrayList<Message>();	//创建消息,将消息存入到集合中
        for(int i =0; i<20; i++){
            Message message = new Messasge(
            	"Topic_broadcasting",
                "TagBroad",
                "KeyBroad"+i,
                (i+"---hello broadcasting").getBytes(RemotingHelper.DEFAULT_CHARSET);
            );
            message.add(message);
        }
    }
}

2、消费端

//默认是集群模式。改为广播模式即可	其他代码一样
consumer.setMessageModel(MessageModel.BROADCASTING);

RocketMQ4.4笔记 (一)入门以及安装RocketMQ4.4笔记 (二)java操作rocketmq入门RocketMQ4.4笔记 (三)springboot操作rocketmqRocketMQ4.4笔记 (四)RocketMQ集群