spring boot 整合阿里云Rocketmq

1,137 阅读6分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情

之前说到,由于甲方之前的技术栈是使用了rocketmq,所以在本次项目中我为了技术栈的统一也使用了rocketmq。可是开发到一半才发现有个功能需求需要用到延时队列,而这个延时队列的延时时间需要在后台管理系统中可以自定义配置。换句话说,就是我们的延时队列需要自定义延时时间。然而在上一篇spring boot整合rocketmq中,我们已经了解到,开源版的rocketmq是没有自定义延时时间这个功能的,它只能根据源码中默认的时间等级来设置延时队列。这个时候只能依赖于阿里云收费版的rocketmq了。

阿里云版本的rocketmq是可以支持自定义延时时间的。那么我们就一起来看下,如何将spring boot和阿里云rocketmq整合到一起进行开发吧。

首先我们需要使用阿里云提供给我们的依赖。这一次我们使用的是

<dependency>
    <groupId>com.aliyun.openservices</groupId>
    <artifactId>ons-client</artifactId>
    <version>1.8.8.3.Final</version>
</dependency>

添加了依赖之后我们需要打开阿里云的控制台,在控制台中创建实例和group。由于我的这个消息服务是允许在公网上进行的所以在开通rocketmq之后,选择公网在实例列表上点击创建实例, 然后创建topic主题,

截屏2022-04-05 下午11.47.29.png

这里我们可以按照我们的需求来选择使用哪种类型的消息队列,我这里主要是使用了普通队列和延时队列。创建完topic之后我们需要创建group,这个group主要是消费者的分组。目前阿里云提供了两种协议的SDK,在选择group时我们需要区分是使用TCP协议还是Http协议。我这边是选择了TCP协议进行开发。创建完group之后我们就需要配置我们在工程中的rocketmq了。 首先需要配置我们的yml文件。在末尾添加

rocketmq:
    accessKey: 123
    secretKey: 123
    nameSrvAddr: http://MQ_INST.mq-internet-access.mq-internet.aliyuncs.com:80
    group_id: GID_123
    delay_group_id: GID_123_delay

前两个分别是我们阿里云账号的key和秘钥,当然规范和安全的做法是使用子账号并授予子账号操作rocketmq的权限,具体的授权操作可以再阿里云的文档中找到。

第三个是我们服务的地址,这个可以在阿里云控制台,我们服务实例的详情页面中,选择接入点查看到地址信息。最后两个是我们消费者集群组的名称,只要把先前我们在阿里云控制台上创建的group名称照搬过来就 可以了。 接下来需要在代码中配置我们的生产者和消费者。

@Data
@Component
@ConfigurationProperties(prefix = "rocketmq")
public class RocketMqConfig {

    /**
     * accessKey
     **/
    private String accessKey;

    /**
     * 秘钥
     **/
    private String secretKey;

    /**
     * tcp协议接入点
     **/
    private String nameSrvAddr;

    /**
     * 分组id
     **/
    private String groupId;

    /**
     * 延时消费组
     **/
    private String delayGroupId;


    public Properties getMqProperties() {
        Properties properties = new Properties();
        properties.setProperty(PropertyKeyConst.AccessKey, this.accessKey);
        properties.setProperty(PropertyKeyConst.SecretKey, this.secretKey);
        properties.setProperty(PropertyKeyConst.NAMESRV_ADDR, this.nameSrvAddr);
        properties.setProperty(PropertyKeyConst.GROUP_ID, this.delayGroupId);
        return properties;
    }

}

我们声明了一个配置类里面定义了之前我们在yml文件里锁配置的参数,然后构造一个方法组长一个Properties,这个是为了待会儿在创建生产者和消费者时需要注入的参数。

@Configuration
public class RocketMqProducerClient {

    @Autowired
    private RocketMqConfig rocketMqConfig;


    @Bean(initMethod = "start", destroyMethod = "shutdown")
    public Producer buildProducer(){
        Producer producer = ONSFactory.createProducer(rocketMqConfig.getMqProperties());
        return producer;
    }
}

我们创建一个类用户声明一个生产者的bean,同时定义它的生命周期,当程序结束时销毁该bean。 然后我们需要创建消息投递的方法,这里我是这样做的。

@Autowired
private RocketMqProducerClient rocketMqClient;

public void sendInternalMessage(String message){
    //构建消息实体,定义了消息投递的主题和标签参数。
    Message msg = new Message(MqConstants.TOPIC,
            MqConstants.MESSAGE_TAG,
            message.getBytes());
    //发送消息
    try {
        SendResult result = rocketMqClient.buildProducer().send(msg);
        if (null != result) {
            log.info("投递成功,topic:{},messageId:{}",result.getTopic(),result.getMessageId());
        }
    }catch (Exception e){
        log.error("投递失败");
        log.error(e.getMessage(),e);
    }
}

这里我使用的是同步消息,也就是投递消息后会等待消息投递到broker的返回结果。 另外一种生产者就是使用了延时,需要这样修改

Message msg = new Message(MqConstants.DELAY_TOPIC,
        MqConstants.POST_CHECK_TAG,
        message.getBytes());
final long delayTime = System.currentTimeMillis() + (postPublishTime * 1000 * 3600);
log.info("预计发送事件:{}",delayTime);
msg.setStartDeliverTime(delayTime);

在原来的消息实体对象中增加一个startDeliverTime参数。这里的延时时间是自定义的毫秒时间,指的是当消息被成功投递到broker后会暂存给定时间后再投递给消费者。 然后是消费者,

@Configuration
public class ForumRocketMqConsumerClient {

    @Autowired
    private RocketMqConfig rocketMqConfig;
    @Autowired
    private DelayMessageListener delayMessageListener;

    @Bean(initMethod = "start",destroyMethod = "shutdown")
    public Consumer buildConsumer(){
        Consumer consumer = ONSFactory.createConsumer(rocketMqConfig.getMqProperties());
        //订阅延时审核
        consumer.subscribe(MqConstants.DELAY_TOPIC,MqConstants.POST_CHECK_TAG + "||"
                + MqConstants.REPLY_CHECK_TAG,delayMessageListener);

        return consumer;
    }
}

这里同样是创建了一个配置类,声明了一个Consumer的bean,在这个bean中实例化了消费者对象,然后开始订阅主题。这里有一个需要注意的点。之前提到了我的项目中使用了两个不同的topic分别对应普通消息和延时消息。开始我以为这个subscribe方法是类似于往list中add一样的原理,所以写了两遍subscribe,分别订阅了延时topic下的两个不同的tag,而现实就是先订阅的tag不生效,后订阅的tag可以接受到消息。

进入源码我们可以发现,它的订阅是将topic,tag,listener,放入一个map中而map的key是我们的topic,这就解释了前面一个订阅会失效,因为后者将其覆盖了。

所以如果我们一个topic需要处理的tag有多个的话,那么只能将tag用"||"连接起来。这里你可能会问那能否创建多个consumer来对应处理多个tag。这个会涉及到订阅关系一致的问题。在文档中提出了三种正确的做法。

一个group下的队列都只订阅一个topic下的一个tag;

一个group下的队列只订阅一个topic,允许订阅多个tag,但是队列之间订阅tag必须相同;

一个group下的队列可以订阅多个topic,也可以订阅这些topic下的多个tag,但是每个队列订阅的topic和tag必须相互一致。

总结来说就是group内的消费者,大家订阅的东西必须保持一致,这样才是一个集群。

然后是消费者的监听器

@Slf4j
@Component
public class ForumDelayMessageListener implements MessageListener {

    @Autowired
    private ForumMessageListenerService forumMessageListenerService;

    @Override
    public Action consume(Message message, ConsumeContext consumeContext) {
        String s = new String(message.getBody());
        log.info("接受到延时审核消息: topic:{},tag:{},megId:{},key:{},body:{}",message.getTopic(),
                message.getTag(),message.getMsgID(),message.getKey(),s);
        if (message.getTag().equals(MqConstants.FORUM_POST_CHECK_TAG)){
            return this.delayCheckPost(s);
        }else if(message.getTag().equals(MqConstants.FORUM_REPLY_CHECK_TAG)){
            return this.delayCheckReply(s);
        }else {
            return Action.CommitMessage;
        }
    }
  }

这里统一订阅了该topic,然后在接受到消息时根据tag对应处理各自的业务逻辑。如果消息处理没有异常则返回Action.CommitMessage,如果出现了异常需要重试,可以返回Action.ReconsumeLater,那么消息会在一定时间后重试投递。

以上就是spring boot和阿里云rocketmq的整合开发了,上面只展示了普通消息延时消息的一部分功能,还有很多其它的功能在等待我们去挖掘。