RocketMQ接入到Spring Boot

617 阅读4分钟

各版本Java SDK

阿里云Rocket MQ 提供多种 Java SDK 版本接入

商业版TCP协议SDK

商业版SDK即用于接入阿里云RocketMQ商业版
阿里云RocketMQ商业版,是对开源社区版的RocketMQ作增值,其兼容Apache RocketMQ的核心API和功能
通常生产环境使用此版本SDK,RocketMQ 5.0 版本及以上使用2.x.x.x,否则使用1.8.x.x

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

社区版TCP协议SDK

社区版SDK即用于开源社区版的Apache RocketMQ
社区版SDK可用于测试环境,并兼容生产环境接入阿里云RocketMQ商业版时使用

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

商业版HTTP协议SDK

对TCP协议社区版本作增强开发的HTTP协议版本
开源自建 RocketMQ 不支持HTTP协议

<dependency>
    <groupId>com.aliyun.mq</groupId>
    <artifactId>mq-http-sdk</artifactId>
    <!--以下版本号请替换为Java SDK的最新版本号-->
    <version>1.0.3.2</version> 
    <classifier>jar-with-dependencies</classifier>
</dependency>

SDK与RocketMQ版本组合方案

根据各版本Java SDK对不同 RocketMQ 版本的支持,可列出它们之间的组合方案

  • 商业版TCP协议SDK 与 阿里云RocketMQ商业版
  • 商业版TCP协议SDK 与 开源社区版Apache RocketMQ
  • 社区版TCP协议SDK 与 阿里云RocketMQ商业版
  • 社区版TCP协议SDK 与 开源社区版Apache RocketMQ
  • 商业版HTTP协议SDK 与 阿里云RocketMQ商业版

商业版TCP协议接入示例

本次示例基于 Spring Boot 框架引入商业版TCP协议SDK,并接入阿里云RocketMQ商业版

1.引入依赖

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

2.配置类

accessKey,阿里云RocketMQ AccessKey
secretKey,阿里云RocketMQ SecretKey
nameSrvAddr,RocketMQ namesrv 地址
groupId,某一类 Producer 或 Consumer,将这一组指定一个标识
normalTopic,消息主题,这里是作为普通消息类型
delayTopic,消息主体,这里是作为延迟消息类型

/**
 * Rocket MQ 配置类
 */
@Configuration
public class RocketMqConfig {

    @Value("${rocketmq.access-key}")
    private String accessKey;
    @Value("${rocketmq.secret-key}")
    private String secretKey;
    @Value("${rocketmq.namesrv-addr}")
    private String nameSrvAddr;
    @Value("${rocketmq.group-id}")
    private String groupId;
    @Value("${rocketmq.topic.normal}")
    private String normalTopic;
    @Value("${rocketmq.topic.delay}")
    private String delayTopic;

    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);
        return properties;
    }
}

3.生产者客户端

创建完 TransactionProducerBean 后,即 Bean 实例化后进入其中的 initMethod,销毁时进入 shutdown;
发送消息时调用 send 方法

@Component
public class ProducerClient {

    @Autowired
    private RocketMqConfig mqConfig;
    @Autowired
    private MqLocalTransactionChecker localTransactionChecker;

    @Bean(initMethod = "start", destroyMethod = "shutdown")
    public TransactionProducerBean buildTransactionProducer() {
        TransactionProducerBean producer = new TransactionProducerBean();
        //配置属性
        Properties properties = mqConfig.getMqProperties();
        properties.put(PropertyKeyConst.GROUP_ID, mqConfig.getGroupId());
        producer.setProperties(properties);
        producer.setLocalTransactionChecker(localTransactionChecker);
        return producer;
    }
}

4.本地事务检查器

生产者采用事务消息的形式,RocketMQ Broker会定期发起事务回查,确保事务一致性

@Component
public class MqLocalTransactionChecker implements LocalTransactionChecker {
    @Override
    public TransactionStatus check(Message message) {
        System.out.println("开始回查本地事务状态");
        return TransactionStatus.CommitTransaction; //根据本地事务状态检查结果返回不同的TransactionStatus
    }
}

5.消费者客户端

消费客户端用于接收消息,在配置阶段需要订阅消息,指定 topic、tag 过滤表达式、type(SQL92 | TAG)

@Component
public class ConsumerClient {

    @Autowired
    private RocketMqConfig mqConfig;
    @Autowired
    private NormalMessageListener normalMessageListener;
    @Autowired
    private DelayMessageListener delayMessageListener;

    @Bean(initMethod = "start", destroyMethod = "shutdown")
    public ConsumerBean buildConsumer() {
        ConsumerBean consumerBean = new ConsumerBean();
        //配置文件
        Properties properties = mqConfig.getMqProperties();
        properties.put(PropertyKeyConst.GROUP_ID, mqConfig.getGroupId());
        properties.put(PropertyKeyConst.MaxReconsumeTimes, "5"); //消息消费失败的最大重试次数
        consumerBean.setProperties(properties);
        //订阅关系
        Map<Subscription, MessageListener> subscriptionTable = new HashMap<Subscription, MessageListener>();
        //订阅普通消息 Topic
        Subscription subscribeNormal = new Subscription();
        subscribeNormal.setTopic(mqConfig.getNormalTopic());
        subscribeNormal.setExpression("*");
        subscriptionTable.put(subscribeNormal, normalMessageListener);
        //订阅延迟消息 Topic
        Subscription subscribeDelay = new Subscription();
        subscribeDelay.setTopic(mqConfig.getDelayTopic());
        subscribeNormal.setExpression("*");
        subscriptionTable.put(subscribeDelay, delayMessageListener);

        consumerBean.setSubscriptionTable(subscriptionTable);
        return consumerBean;
    }
}

6.消费者监听器

消费客户端只用于订阅,真正的消费逻辑在监听器回调中实现

6.1.延时消息监听器

@Component
public class DelayMessageListener implements MessageListener {
    @Override
    public Action consume(Message message, ConsumeContext consumeContext) {

        System.out.println("Receive: " + message);
        try {
            //do something..
            return Action.CommitMessage;
        } catch (Exception e) {
            //消费失败
            return Action.ReconsumeLater;
        }
    }
}

6.2.普通消息监听器

@Component
public class NormalMessageListener implements MessageListener {
    @Override
    public Action consume(Message message, ConsumeContext consumeContext) {

        System.out.println("Receive: " + message);
        try {
            //do something..
            return Action.CommitMessage;
        } catch (Exception e) {
            //消费失败
            return Action.ReconsumeLater;
        }
    }
}

消费幂等

当消息重复投递,被重复消费时,消费一次与消费多次的结果应是同等的
消息重复的可能:

  • 发送时消息重复
    生产者客户端已向MQ服务端发送消息,但由于服务端未应答(网络原因或宕机),生产者客户端认为消息发送失败,因此重复发送,消费者后续会收到两条内容相同但Message ID不同的消息。
  • 投递时消息重复
    消费者客户端已完成消费,但在向MQ服务端发送应答时失败(网络原因),为保证消息至少被消费一次,服务端重新发送消息,消费者后续会收到两条内容相同并且Message ID也相同的消息。
  • 负载均衡时消息重复(包括但不限于网络抖动、Broker重启以及消费者应用重启)
    当消息队列RocketMQ版的Broker或客户端重启、扩容或缩容时,会触发Rebalance,此时消费者可能会收到少量重复消息。

幂等处理

因为不同的Message ID对应的消息内容可能相同,所以最好的方式是以业务唯一标识作为幂等处理的关键依据,而业务的唯一标识可以通过消息Key设置。
生产者发送消息时设置消息Key:

Message message = new Message();
message.setKey("ORDERID_100");
SendResult sendResult = producer.send(message);           

消费者收到消息时可以根据消息的Key,即订单号来实现消息幂等:

consumer.subscribe("ons_test", "*", new MessageListener() {
    public Action consume(Message message, ConsumeContext context) {
        String key = message.getKey()
        // 根据业务唯一标识的Key做幂等处理。
    }
});