在SpringBoot中实现RocketMQ的消息收发

2,312 阅读4分钟

一、SpringBoot整合RocketMQ

1、整合RocketMQ

1-1、引入rocketmq的pom依赖

在使用SpringBoot的starter集成包时,要特别注意版本。因为SpringBoot集成RocketMQ的starter依赖是由Spring社区提供的,目前正在快速迭代的过程当中,不同版本之间的差距非常大,甚至基础的底层对象都会经常有改动。例如如果使用rocketmq-spring-boot-starter:2.0.4版本开发的代码,升级到目前最新的rocketmq-spring-boot-starter:2.1.1后,基本就用不了了。

我们创建一个maven工程,引入关键依赖:

此处引入的rocketmq由社区提供,因为其springboot版本较低,因此需要使用exclusions将rocketmq中的springboot相关包排除掉,然后自己再引入项目的实际springboot版本

<dependencies>
    <dependency>
        <groupId>org.apache.rocketmq</groupId>-
        <artifactId>rocketmq-spring-boot-starter</artifactId>
        <version>2.1.1</version>
        <!--排除sprinboot相关包-->
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter</artifactId>
            </exclusion>
            <exclusion>
                <groupId>org.springframework</groupId>
                <artifactId>spring-core</artifactId>
            </exclusion>
            <exclusion>
                <groupId>org.springframework</groupId>
                <artifactId>spring-webmvc</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <!--引入项目实际的springboot版本-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>2.7.8</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>

</dependencies>

1-2、配置application.yml的rocket配置信息

使用原生api的时候,配置nameServer要么在代码中进行配置,要么就使用环境变量进行配置,而springboot只需要在配置文件中进行如下配置即可

server:
  port: 8899
rocketmq:
  #配置nameServer
  name-server: 192.168.253.131:9876
  #配置生产者的默认组
  producer:
    group: springBootGroup

除了配置nameServer和group之外,还提供了如下配置,比较熟悉的配置如配置消息生产者的超时时间,可以看到默认是3s,如果RocketMQ的响应较慢可以将此配置的参数调大一些。

image.png

server:
  port: 8899
rocketmq:
  #配置nameServer
  name-server: 192.168.253.131:9876
  #配置生产者的默认组
  producer:
    group: springBootGroup
    send-message-timeout: 8000

2、发送基本消息

通过以上配置就完成了RocketMQ的基本配置,接下来就可以正式发送消息了

2-1、发送普通消息

RocketMQTemplate注入,然后调用sendOneWay(topicName,message)就可以完成一条单向消息的发送

@RestController
public class ProducerController {

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    private final String topic="springTopic";

    /**
     * 单向发送
     * @param message
     */
    @GetMapping("/send")
    public void sendMessage(String message){
        rocketMQTemplate.sendOneWay(topic,message);
    }

2-2、发送同步消息

同步消息主要使用syncSend来进行消息发送,会同步发挥SendResult发送结果信息

/**
 * 同步发送
 * @param message
 * @return
 */
@GetMapping("/sendSync")
public SendResult sendSyncMessage(String message){
    User user=new User();
    user.setUserName("张三");
    user.setAge(19);
    user.setSex("男");
    SendResult sendResult = rocketMQTemplate.syncSend(topic, user);
    return sendResult;
}

2-3、发送异步消息

异步消息使用asyncSend进行消息发送,不会立即返回发送结果,会在SendCallback中进行回调,成功调用onSuccess()方法,失败调用onException()方法

/**
 * 异步发送
 * @param message
 */
@GetMapping("/sendAsync")
public void sendAsyncMessage(String message){
    rocketMQTemplate.asyncSend(topic, message, new SendCallback() {
        @Override
        public void onSuccess(SendResult sendResult) {
            System.out.println("发送成功:"+sendResult);
        }
        @Override
        public void onException(Throwable throwable) {
            System.out.println("发送失败:"+throwable);
        }
    });

}

打开RocketMQ控制台,就可以看到刚刚发送的消息

image.png

点击状态就可以看到,刚刚发送的消息

image.png

2-4 消费者

消费者需要配置一个@RocketMQMessageListener注解,此注解可以配置消费者的topic、消费组、过滤的tag、或者sql过滤等相关条件,同时需要实现RocketMQListener接口,这样就可以在onMessage中接收消息

@Component
@RocketMQMessageListener(consumerGroup = "MyConsumerGroup", topic = "springTopic")
public class SpringConsumer implements RocketMQListener {
    @Override
    public void onMessage(Object o) {
        System.out.println(o.toString());
    }
}

重启项目之后,就可以看到生产者刚刚发送的消息已收到

image.png

然后进入控制台,进入springTopic的CONSUMER管理就可以看到消息已消费

image.png

如下差值为0,代表消息已消费

image.png

3、过滤消息

过滤消息和原生一样,分为tag和sql过滤

3-1、tag消息过滤

3-1-1、发送消息

使用tag进行消息过滤,需要在topic后面添加“:”,然后后面为tag信息

@RestController 
public class FilterProducerController {
    private final String topic="filterTopic";

    @Autowired private RocketMQTemplate rocketMQTemplate; 

    @GetMapping("/filtersync") 
    public SendResult sendTag(String message,String tag){ 
        return rocketMQTemplate.syncSend(topic + ":" + tag, message); 
    } 
}

3-1-2、接收消息

接收消息依然使用@RocketMQMessageListener注解,使用selectorExpression参数来进行设置,selectorType指定通过tag还是sql进行过滤,默认tag,因此selectorExpression写需要消费消息的tag就可以

@Component
@RocketMQMessageListener(consumerGroup = "filterGroup",topic = "filterTopic",selectorExpression = "tag0")
public class FilterTagConsumer implements RocketMQListener {
    @Override
    public void onMessage(Object o) {
        System.out.println("tagfilter:"+o.toString());
    }
}

如上发送到topic为filterTopic并且tag为tag0就可以收到消息了

3-2、sql消息过滤

3-2-1、发送消息

sql过滤的内容,原生使用msg.putUserProperty(key,value)来实现,而springboot中则使用header来进行设置,header可以通过MessageBuilder进行发送,也可以作为map进行发送

3-2-1-1、使用MessageBuilder发送header信息
@GetMapping("/filterSqlsync") 
public SendResult filterSql(String message,Integer age,String tag){ 
    Message<String> messAge = MessageBuilder.withPayload(message).setHeader("age", age).build(); return rocketMQTemplate.syncSend(topic + ":" + tag, messAge); 
}
3-2-1-2、使用Map发送header信息
@GetMapping("/filterSqlsync")
    public void filterSql(String message,Integer age,String tag){
        //使用MessageBuilder设置信息
//        Message<String> messAge = MessageBuilder.withPayload(message).setHeader("age", age).build();
//        return rocketMQTemplate.syncSend(topic + ":" + tag, messAge);
        //使用Map发送Header信息
        Map<String, Object> headers = new HashMap<>() ;
        headers.put("pack", "abc") ;
        headers.put("age", age) ;
        rocketMQTemplate.convertAndSend(topic + ":" + tag,message,headers);
    }

3-2-2、过滤信息

通过sql不仅可以对header中的信息过滤,还支持tag过滤,如下:

这样只有tag为:tagab/tag123 并且age在18到30直接才可以收到信息

@Component
@RocketMQMessageListener(consumerGroup = "sqlFilterGroup",topic = "filterTopic",
        selectorType = SelectorType.SQL92,
        selectorExpression = "(TAGS is not null and TAGS in ('tagab', 'tag123')) and (age is not null and age between 18 and 30)")
public class FilterSqlConsumer implements RocketMQListener {

    @Override
    public void onMessage(Object o) {
        System.out.println("sqlfilter:"+o.toString());
    }
}

4、顺序消息

4-1、发送消息

依然以订单举例,第一次for为10个订单,第二次for为订单的固定步骤,然后需要使用syncSendOrderly(),进行发送消息,最后一个参数为hashKey,会根据这个hashKey将某个订单消息发送到固定的队列中

RocketMQTemplate给我们提供了SendOrderly方法(有多個重载),来实现发送顺序消息;包括以下:

syncSendOrderly,发送同步顺序消息;

asyncSendOrderly,发送异步顺序消息;

sendOneWayOrderly,发送单向顺序消息;

一般我们用syncSendOrderly方法发送同步顺序消息。

各方法发送顺序消息有如下几种方式:

image.png

@RestController
public class OrderProduceController {

    private final static String topic="orderTopic";
    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    @GetMapping("/orderSend")
    public void orderSend(String message){
        for (int i=0;i<10;i++){
            int orderId=i;
            for (int j = 0; j < 5; j++) {
                rocketMQTemplate.syncSendOrderly(topic,message+"-"+i+"-"+j,"order_"+i);
            }
        }
    }
}

4-2、接收消息

接收消息需要在@RocketMQMessageListener注解中添加consumeMode = ConsumeMode.ORDERLY,告诉消费端使用顺序模式进行消费

@Component
@RocketMQMessageListener(consumerGroup = "orderGroup",topic = "orderTopic",consumeMode = ConsumeMode.ORDERLY)
public class OrderConsumer implements RocketMQListener {
    @Override
    public void onMessage(Object o) {
        System.out.println(o.toString());
    }
}

执行结果,可以看到虽然第二位订单顺序是错乱的,但是后面第三位订单的步骤是按照0-4的顺序消费的

image.png

5、延迟消息

延迟消息在各个消息发送方法中会有一个delayLevel的参数,传入需要延迟的顺序号即可,这就不做演示了

6、批量发送消息

6-1、发送消息

创建一个Message的集合,然后将消息放到集合中,传入的发送方法中

@RestController
public class BatchProducerController {

    private final String topic = "batchTopic";

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    @GetMapping("/batchsend")
    public void batchsend() {
        List<Message> msgs = new ArrayList<Message>();
        for (int i = 0; i < 10; i++) {
            msgs.add(MessageBuilder.withPayload("Hello RocketMQ Batch Msg#" + i).
                    setHeader(RocketMQHeaders.KEYS, "KEY_" + i).build());
        }

        SendResult sr = rocketMQTemplate.syncSend(topic, msgs);

        System.out.printf("--- Batch messages send result :" + sr);
    }
}

通过下面执行结果,可以看到这10条消息是一起批量发送到服务端的

--- Batch messages send result :SendResult [sendStatus=SEND_OK, msgId=C0A800104EC418B4AAC21EAE2E490000,C0A800104EC418B4AAC21EAE2E490001,C0A800104EC418B4AAC21EAE2E490002,C0A800104EC418B4AAC21EAE2E490003,C0A800104EC418B4AAC21EAE2E490004,C0A800104EC418B4AAC21EAE2E490005,C0A800104EC418B4AAC21EAE2E490006,C0A800104EC418B4AAC21EAE2E490007,C0A800104EC418B4AAC21EAE2E490008,C0A800104EC418B4AAC21EAE2E490009, offsetMsgId=C0A8FD8400002A9F00000000000722DA,C0A8FD8400002A9F0000000000072421,C0A8FD8400002A9F0000000000072568,C0A8FD8400002A9F00000000000726AF,C0A8FD8400002A9F00000000000727F6,C0A8FD8400002A9F000000000007293D,C0A8FD8400002A9F0000000000072A84,C0A8FD8400002A9F0000000000072BCB,C0A8FD8400002A9F0000000000072D12,C0A8FD8400002A9F0000000000072E59, messageQueue=MessageQueue [topic=batchTopic, brokerName=broker-a, queueId=0], queueOffset=0]

7、事务消息

7-1、发送消息

7-1-1、消息生产者

发送事务消息,RocketMQTemplate,提供了sendMessageInTransaction()方法,通过该方法就可以发送事务消息

@RestController
public class TransactionProducerController {

    private final static String topic = "transactionTopic";
    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    @GetMapping("/transend")
    public void tranSend(String message) {
        String[] tags = new String[]{"TagA", "TagB", "TagC", "TagD", "TagE"};
        for (int i = 0; i < 10; i++) {
            Message message1 = MessageBuilder.withPayload(message).setHeader("tag", tags[i % tags.length]).build();
            TransactionSendResult transactionSendResult = rocketMQTemplate.sendMessageInTransaction(topic, message1, null);
            System.out.println(transactionSendResult.toString());
        }
    }
}

7-1-2、事务消息监听器

事务监听器需要通过以下几个步骤:

1、添加@RocketMQTransactionListener注解,并通过设置rocketMQTemplateBeanName = "rocketMQTemplate"来实现监听的处理,默认值为rocketMQTemplate

2、实现RocketMQLocalTransactionListener接口,并重写executeLocalTransaction()和checkLocalTransaction()

@RocketMQTransactionListener(rocketMQTemplateBeanName = "rocketMQTemplate")
public class TransactionListenerImpl implements RocketMQLocalTransactionListener {
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {
        String tags=message.getHeaders().get("tag").toString();
        //TagA的消息会立即被消费者消费到
        if(StringUtils.contains(tags,"TagA")){
            return RocketMQLocalTransactionState.COMMIT;
            //TagB的消息会被丢弃
        }else if(StringUtils.contains(tags,"TagB")){
            return RocketMQLocalTransactionState.ROLLBACK;
            //其他消息会等待Broker进行事务状态回查。
        }else{
            return RocketMQLocalTransactionState.UNKNOWN;
        }

    }

    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
        String tags=message.getHeaders().get("tag").toString();
        //TagC的消息过一段时间会被消费者消费到
        if(StringUtils.contains(tags,"TagC")){
            return RocketMQLocalTransactionState.COMMIT;
            //TagD的消息也会在状态回查时被丢弃掉
        }else if(StringUtils.contains(tags,"TagD")){
            return RocketMQLocalTransactionState.ROLLBACK;
            //剩下TagE的消息会在多次状态回查后最终丢弃
        }else{
            return RocketMQLocalTransactionState.UNKNOWN;
        }
    }
}

7-2、多事务处理

通过RocketMQTemplate发送事务消息,在消息监听器中在@RocketMQTransactionListener注解中通过设置RocketMQTemplate的bean名称来实现事务监听,这样所有通过rocketMQTemplate发送的事务消息就都可以被监听到。

那如果项目中有多个消息事务要处理该怎么办,虽然可以在消息生产者端通过设置Header参数来确定是哪个事务,但是长期以往这个消息监听器代码就会变得非常臃肿,后期维护升级也会有较多麻烦。

为了方便处理就可以安装一下方式处理:

  • 1、自定义RocketMQTemplate继承本来的RocketMQTemplate,需要添加@ExtRocketMQTemplateConfiguration注解
@ExtRocketMQTemplateConfiguration 
public class MyRocketMQTemplate extends RocketMQTemplate { 
}
  • 2、发送消息的时候使用上面自定义MyRocketMQTemplate进行消息发送

  • 3、消息监听的时候,改为自定Template名称,bean名称首字母要小写

@RocketMQTransactionListener(rocketMQTemplateBeanName = "myRocketMQTemplate")
public class MyTransactionListenerImpl implements RocketMQLocalTransactionListener {
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {
        return null;
    }

    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
        return null;
    }
}

通过以上步骤就实现了多个事务监听器的处理

8、总结

  • SpringBoot 引入org.apache.rocketmq:rocketmq-spring-boot-starter依赖后,就可以通过内置的RocketMQTemplate来与RocketMQ交互。相关属性都以rockemq.开头。具体所有的配置信息可以参见org.apache.rocketmq.spring.autoconfigure.RocketMQProperties这个类。
  • SpringBoot依赖中的Message对象和RocketMQ-client中的Message对象是两个不同的对象,这在使用的时候要非常容易弄错。例如RocketMQ-client中的Message里的TAG属性,在SpringBoot依赖中的Message中就没有。Tag属性被移到了发送目标中,与Topic一起,以Topic:Tag的方式指定。
  • 最后强调,一定要注意版本。rocketmq-spring-boot-starter的更新进度一般都会略慢于RocketMQ的版本更新,并且版本不同会引发很多奇怪的问题。apache有一个官方的rocketmq-spring示例,地址:github.com/apache/rock… 以后如果版本更新了,可以参考下这个示例代码。