一起养成写作习惯!这是我参与「掘金日新计划 · 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主题,
这里我们可以按照我们的需求来选择使用哪种类型的消息队列,我这里主要是使用了普通队列和延时队列。创建完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的整合开发了,上面只展示了普通消息延时消息的一部分功能,还有很多其它的功能在等待我们去挖掘。