【RocketMq】商用RocketMq和開源RocketMq的相容問題解決方案

Xander發表於2022-12-30

引言

在阿里雲的官方網站提供了RocketMq的商用版本,但是個人在專案應用上發現和SpirngBoot以及Spring Cloud(Alibaba)等開源的RocketMQ依賴雖然可以正常相容,但是依然出現了註解失效、啟動報錯,商用和開源版本的不相容導致部分程式碼要重複編寫的蛋疼問題。

這樣的相容問題不是簡單加個SDK依賴,切換到商用配置就可以直接使用的(因為個人起初真就是這麼想),為了避免後面再遇到這種奇葩的開發測試用開源RocketMq,生產環境需要使用商用叢集的RocketMq的混合配置的業務場景,個人花了小半天時間熟讀阿里雲的接入文件,加上各種嘗試和測試,總結出一套可以快速使用的相容模板方案。

如果不瞭解阿里雲商用RocketMq,可以看最後一個大節的【阿里雲商用RocketMq介紹】介紹。個人的相容方案靈感來自於官方提供的這個DEMO專案:springboot/java-springboot-demo

注意本方案是基於SpringBoot2.XSpring cloud Alibaba 的兩個專案環境構建專案基礎,在SpringBoot上只做了生產者的配置,而在Spring Cloud Alibaba的Nacos上進行了生產者和消費者的完整相容方案。

最後注意相容整合的版本為商用RocketMq使用4.x版本,最近新出的5.X 的版本並未進行測試,不保證正常使用。

相容關鍵點

  1. 在沿用SpringBoot的YML基礎配置基礎上實現商用和開源模式的相容。
  2. 商用RocketMq需要使用官方提供的依賴包,依賴包可以正常相容SpringBoot等依賴。
  3. 整合之後便於開發和擴充套件,並且易於其他開發人員理解。
  4. 兩種模式之間互相不會產生干擾。

SpringBoot2.X 相容

下面先介紹SpringBoot專案的相容。

SpringBoot專案相容

開源版本

首先我們觀察YAML檔案,對於開源版本的RocketMq設定,單機版本可以直接配置一個ip和埠即可,如果是叢集則用分號隔開多個NameServer的連線IP地址(NameServ獨立部署,內部進行自動同步 )。

#訊息佇列
rocketmq:
  # 自定義屬性,作用下文將會進行解釋
  use-aliyun-rocketSever: false
  name-server: 192.168.58.128:9876 # 192.168.244.128:9876;192.168.244.129:9876;
  producer:
    group: testGroup
    # 本地開發不使用商業版,可以不配置
    secret-key: NONE
    # 本地開發不使用商業版,可以不配置
    access-key: NONE
    # 商用版本請求超時時間,開源版本不使用此引數
    timeoutMillis: NONE

SpringBoot中整合開源的RocketMq非常簡單,只需要一個依賴就可以自動完成相關準備:

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot</artifactId>
    <version>2.2.2</version>
    </dependency>

具體的使用通常為封裝或者直接使用RocketMqTemplate


@Autowired
private RocketMQTemplate mqTemplate;

public void sendRocketMqUniqueTextMessage(String topic, String tag, String queueMsg) {
        if (StringUtils.isNotEmpty(topic) && StringUtils.isNotEmpty(tag) && StringUtils.isNotEmpty(queueMsg)) {
            String queueName = topic + ":" + tag;
            //封裝訊息,分配唯一id
            MessageData messageData = new MessageData();
            messageData.setMsgId(IdUtil.randomUUID());
            messageData.setMsgContent(queueMsg);
            queueMsg = JSON.toJSONString(messageData);
            log.info("執行緒:{},向佇列:{},傳送訊息:{}", Thread.currentThread()
                    .getName(), queueName, queueMsg);
            try {
                mqTemplate.syncSend(queueName, queueMsg);
            } catch (Exception e) {
                log.info("向佇列:{},傳送訊息出現異常:{}", queueName, queueMsg);
                //出現異常,儲存異常資訊到資料庫
                SaMqMessageFail saMqMessageFail = new SaMqMessageFail();
                // 封裝失敗訊息,呼叫失效處理Service將失敗傳送請求入庫,或許透過其他方法重試
                saMqMessageFailService.insert(saMqMessageFail);
            }
        }
    }

開源版本的SpringBoot整合RocketMq就是如此簡單。

商用版本

商用版本RocketMq我們使用商業版TCP協議SDK(推薦),注意這裡用的是4.X版本。商用版本的YAML配置和開源版本顯式配置是一樣的,但是需要注意引數use-aliyun-rocketSever=true,並且secretKeyaccessKey以及name-server都需要配置為阿里雲提供的配置,最後設定訊息傳送超時時間timeoutMillis設定合理時間(單位為毫秒),便於排查問題和防止執行緒長期佔用:

#訊息佇列
rocketmq:
  use-aliyun-rocketSever: true
  # 使用阿里雲提供的endpoint
  name-server: http://xxxxx.aliyuncs.com:8080
  producer:
    group: testGroup
    secret-key: xxx
    access-key: xxxx
    # 商用版本請求超時時間
    timeoutMillis: 15000
  # 如果需要設定消費者,可以按照同樣的方式整合
  consumer:
      group: testGroup
    secret-key: xxx
    access-key: xxxx
    # 商用版本請求超時時間 15秒
    timeoutMillis: 15000
注意這裡僅僅配置了生產者,讀者可以按需設定為消費者,設定方式和生產者同理。

設定YAML之後,我們需要在Maven中引入下面的配置:

<dependency>
    <groupId>com.aliyun.openservices</groupId>
    <artifactId>ons-client</artifactId>
    <!--以下版本號請替換為Java SDK的最新版本號-->
    <version>1.8.8.1.Final</version>
</dependency>                            

最後應該如何使用呢?這裡就是商用RocketMq比較蛋疼的點了,使用RocketMQTemplate是這種情況下是無法使用商用RocketMq的,我們需要 手動注入商用的SDK依賴ProducerBean,具體的操作如下:

  1. 構建配置類,這裡仿照了官方提供的demo增減配置:
/**
阿里雲服務配置封裝,注意和本地部署的rocketmq配置區分
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "rocketmq")
public class AliyunRocketMqConfig {

    /**
     *鑑權需要的AccessKey ID
     */
    @Value("${rocketmq.use-aliyun-rocketSever:null}")
    private String useAliyunRocketMqServerEnable;
    /**
     *鑑權需要的AccessKey ID
     */
    @Value("${rocketmq.producer.access-key:null}")
    private String accessKey;

    /**
     *鑑權需要的AccessKey Secret
     */
    @Value("${rocketmq.producer.secret-key:null}")
    private String secretKey;

    /**
     * 例項TCP 協議公網接入地址(實際專案,填寫自己阿里雲MQ的公網地址)
     */
    @Value("${rocketmq.name-server:null}")
    private String nameSrvAddr;

    /**
     * 延時佇列group
     */
    @Value("${rocketmq.producer.group:null}")
    private String groupId;

    /**
     * 訊息傳送超時時間,如果服務端在配置的對應時間內未ACK,則傳送客戶端認為該訊息傳送失敗。
     */
    @Value("${rocketmq.producer.timeoutMillis:null}")
    private String timeoutMillis;


    //獲取Properties
    public Properties getRocketMqProperty() {
        Properties properties = new Properties();
        properties.setProperty(PropertyKeyConst.GROUP_ID,this.getGroupId());
        properties.setProperty(PropertyKeyConst.AccessKey, this.accessKey);
        properties.setProperty(PropertyKeyConst.SecretKey, this.secretKey);
        properties.setProperty(PropertyKeyConst.NAMESRV_ADDR, this.nameSrvAddr);
        properties.setProperty(PropertyKeyConst.SendMsgTimeoutMillis, this.timeoutMillis);
        return properties;
    }

}
  1. 構建商用RocketMq初始化類。這裡會遇到比較蛋疼的事情,因為我們的依賴是商用RocketMq與開源的SpringBoot依賴共存的,雖然我們可以商用的RocketMq,但是啟動的時候會執行到此類進行初始化,返回NULL會導致SpringBoot專案無法正常啟動,這裡無奈只能使用一個warn日誌進行提示開源版本有可能出現Bean被覆蓋問題,實際上使用下來沒有特別大的影響。
寫的比較醜陋,讀者有更優雅的處理方式歡迎指導。筆者目前只想到了使用這種“不管”的方式保證專案不改任何程式碼的情況正常執行。
/**
 * 阿里雲rocketMq初始化
 **/
@Configuration
@Slf4j
public class AliyunProducerInit {

    @Autowired
    private AliyunRocketMqConfig aliyunRocketMqConfig;

    @Bean(initMethod = "start", destroyMethod = "shutdown")
    public ProducerBean buildProducer() {
        if(!Boolean.valueOf(aliyunRocketMqConfig.getUseAliyunRocketMqServerEnable())){
            log.warn("非商用版本為了相容依然需要注入此Bean,但是隻讀取有關nameServ和group資訊");
        }
        ProducerBean producer = new ProducerBean();
        // ProducerBean中的properties只有被覆蓋的配置會使用自定義配置,其他配置會使用SDK的預設配置。
        producer.setProperties(aliyunRocketMqConfig.getRocketMqProperty());
        return producer;
    }
}

此外這裡必須要吐槽一下商用RocketMq的註釋居然是全中文的!比如com.aliyun.openservices.ons.api.bean.ProducerBean的註釋:

producerBean

這樣是好事還是壞事,大家自行體會。。。。。個人第一次看到的時候著實被震驚了。
  1. 做完上面兩步之後,我們就可以實現RocketMqTemplate呼叫請求,至此完成相容。

SpringBoot專案相容小結

這裡簡單小結一下SpringBoot的相容過程,可以看到整個步驟僅僅是 在商用RocketMq多做了一步bean注入的操作而已,整體使用上十分簡單。但是這裡只介紹了生產者的整合,那麼消費者如何相容?稍安勿躁,我們接著看Spring Cloud版本的整合案例。

Spring Cloud Alibaba專案相容

目前國內使用的比較多的是Spring Cloud Alibaba,注意這些配置都寫入到Nacos當中。

開源版本

開源版本的接入方式和SpringBoot是一樣的,這裡簡單回顧:

  1. 開源版本需要設定引數,這裡設定了生產者和消費者:
#訊息佇列 - 開源版本
rocketmq:
  use-aliyun-rocketSever: false
  name-server: 192.168.0.92:9876
  producer:
    group: testGroup
    secret-key: NONE
    access-key: NONE
    timeoutMillis: 15000
    consumeThreadNums: 20
  consumer:
    group: testGroup
    secret-key: NONE
    access-key: NONE
    timeoutMillis: 15000
    consumeThreadNums: 20
  1. 新增依賴:
<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot</artifactId>
    <version>2.2.2</version>
</dependency>
  1. 使用RocketMqTemplate可以進行訊息傳送,而消費者則需要使用監聽器+註解的方式,快速注入一個消費者。大體模板如下:
@Slf4j
@Component
@RocketMQMessageListener(topic = "test_topic", consumerGroup = "testGroup", selectorExpression = "test_tag")
public class QueueRemoteRecoListener implements RocketMQListener<MessageExt> {

    @Override
    public void onMessage(MessageExt message) {
        // dosomething
    }

}

商用版本

這節是本文稍微複雜一點的部分,我們按照步驟介紹接入過程:

  1. 在Nacos的配置中加入RocketMq商用所需的配置內容,和開源版本的設定類似:
#訊息佇列 - 商用版本
rocketmq:
 # 開源RocketMq和商業版RocketMq切換開關
use-aliyun-rocketSever: true
name-server: http://xxxx.mq.aliyuncs.com:80
producer:
# 目前uat藉助開發測試使用
  group: testGroup
  secret-key: xxx
  access-key: xxx
  timeoutMillis: 15000
  consumeThreadNums: 20
consumer:
  group: testGroup
  secret-key: xxx
  access-key: xxx
  timeoutMillis: 15000
  consumeThreadNums: 20
  1. 新增商用RocketMq的TCP接入方式需要的依賴包。
<dependency>
    <groupId>com.aliyun.openservices</groupId>
    <artifactId>ons-client</artifactId>
    <!--以下版本號請替換為Java SDK的最新版本號-->
    <version>1.8.8.1.Final</version>
</dependency>                            
  1. 新增配置類,和SpringBoot的商用版本方式也是類似的:
/**
 * 
 * rocketmq 阿里雲服務配置封裝,注意和本地部署的rocketmq配置區分
 **/
@Data
@Configuration
@ConfigurationProperties(prefix = "rocketmq")
public class AliyunCommercialRocketMqConfig {

    /**
     *鑑權需要的AccessKey ID
     */
    @Value("${rocketmq.use-aliyun-rocketSever:null}")
    private String useAliyunRocketMqServerEnable;
    /**
     *鑑權需要的AccessKey ID
     */
    @Value("${rocketmq.consumer.access-key:null}")
    private String accessKey;

    /**
     *
     */
    @Value("${rocketmq.use-aliyun-rocketsever:null}")
    private String useAliyunRocketServer;

    /**
     *鑑權需要的AccessKey Secret
     */
    @Value("${rocketmq.consumer.secret-key:null}")
    private String secretKey;

    /**
     * 例項TCP 協議公網接入地址(實際專案,填寫自己阿里雲MQ的公網地址)
     */
    @Value("${rocketmq.name-server:null}")
    private String nameSrvAddr;

    /**
     * 延時佇列group
     */
    @Value("${rocketmq.consumer.group:null}")
    private String groupId;

    /**
     * 訊息傳送超時時間,如果服務端在配置的對應時間內未ACK,則傳送客戶端認為該訊息傳送失敗。
     */
    @Value("${rocketmq.consumer.timeoutMillis:null}")
    private String timeoutMillis;

    /**
     * 將消費者執行緒數固定為20個 20為預設值
     */
    @Value("${rocketmq.consumer.consumeThreadNums:null}")
    private String consumeThreadNums;



    public Properties getRocketMqProperty() {
        Properties properties = new Properties();
        properties.setProperty(PropertyKeyConst.GROUP_ID,this.getGroupId());
        properties.setProperty(PropertyKeyConst.AccessKey, this.accessKey);
        properties.setProperty(PropertyKeyConst.SecretKey, this.secretKey);
        properties.setProperty(PropertyKeyConst.NAMESRV_ADDR, this.nameSrvAddr);
        properties.setProperty(PropertyKeyConst.SendMsgTimeoutMillis, this.timeoutMillis);
        return properties;
    }

}
  1. 下一步是擴充套件官方訊息訂閱類,為啥要這樣做?主要是接入實驗中發現官方的訊息訂閱類沒有 全量引數的構造器,難以對於訊息訂閱類靜態引數化常量,構造一個訂閱訊息使用預設方式還需要static程式碼塊設定。所以這裡對於官方的訊息訂閱類進行擴充套件,子類程式碼是對於父類複製了一遍,沒有做其他改動:
/**
對於指定類進行擴充套件,更容易的初始化
@see com.aliyun.openservices.ons.api.bean.Subscription 被擴充套件類
 **/
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class AliyunCommercialRocketMqSubscriptionExt extends Subscription {
    /**
     * 主題
     */
    private String topic;
    /**
     * 條件表示式,具體參考rocketmq
     */
    private String expression;

    /**
     * TAG or SQL92
     * <br>if null, equals to TAG
     *
     * @see com.aliyun.openservices.ons.api.ExpressionType#TAG
     * @see com.aliyun.openservices.ons.api.ExpressionType#SQL92
     */
    private String type;


    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((topic == null) ? 0 : topic.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        AliyunCommercialRocketMqSubscriptionExt other = (AliyunCommercialRocketMqSubscriptionExt) obj;
        if (topic == null) {
            if (other.topic != null) {
                return false;
            }
        } else if (!topic.equals(other.topic)) {
            return false;
        }
        return true;
    }

}
  1. 接著我們構建靜態化的訊息訂閱類,這個常量類會封裝系統需要使用到的訊息訂閱物件,在後續註冊監聽器需要使用,這裡先提前定義。
/**
 * 佇列常規配置,用於啟動時候初始化配置,
 * 所有配置均需要放置到此類中對外擴充套件使用
 **/
public final class AliyunCommercialRocketMqConstants {
    
    public static final String TEST_TOPIC = "test_topic";
    public static final String TEST_TAG = "test_tag";
    
    /**
     * 參考案例,僅僅作為開發驗證使用
     */
    public static final AliyunCommercialRocketMqSubscriptionExt QUEUE_TEST = new AliyunCommercialRocketMqSubscriptionExt(RocketMqKey.TEST_TOPIC, RocketMqKey.TEST_TAG, null);
}

為了方便管理,我們可以把這些關鍵Key在單獨放到一個類:

public class RocketMqKey {
    public static final String TEST_TOPIC = "test_topic";
    public static final String TEST_TAG = "test_tag";   
}

/**
 * 佇列常規配置,用於啟動時候初始化配置,
 * 所有配置均需要放置到此類中對外擴充套件使用
 **/
public final class AliyunCommercialRocketMqConstants {
    /**
     * 參考案例,僅僅作為開發驗證使用
     */
    public static final AliyunCommercialRocketMqSubscriptionExt QUEUE_TEST = new AliyunCommercialRocketMqSubscriptionExt(RocketMqKey.TEST_TOPIC, RocketMqKey.TEST_TAG, null);
}
  1. 做好一系列準備之後,我們開始商用版本生產者相容,商用版本的傳送者可以像是下面這樣封裝官方提供的demo,也可以使用注入ProducerBean的方式整合:
/**
 * 商用aliyun Rocketmq 工具類封裝
 **/
@Component
@Slf4j
public class AliyunCommercialRocketMqSendUtils {

    private static final long KEEP_ALIVE_TIME = 60L;

    private final ThreadPoolExecutor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(0, Integer.MAX_VALUE,
            KEEP_ALIVE_TIME, TimeUnit.SECONDS,
            new SynchronousQueue<>());

    @Autowired
    private Producer producer;

    @Autowired
    private SaMqMessageFailService saMqMessageFailService;

    @Autowired
    private AliyunRocketMqConfig aliyunRocketMqConfig;

    /**
     * 非同步推送,需要呼叫方手動指定callback
     
     */
    public void singleAsyncSend(String topic, String tag, String msgBody, SendCallback sendCallback) {
        singleAsyncSend(topic, tag, msgBody, null, sendCallback);
    }

    /**
     * 非同步推送,需要呼叫方手動指定callback
     * 如果需要使用callback通知,可以使用下面的程式碼進行處理
     <pre>
         producer.sendAsync(msg, new SendCallback() {
        @Override
        public void onSuccess(final SendResult sendResult) {
            assert sendResult != null;
            System.out.println(sendResult);
        }

        @Override
        public void onException(final OnExceptionContext context) {
            ONSClientException exception = context.getException();
            //                    //出現異常意味著傳送失敗,為了避免訊息丟失,建議快取該訊息然後進行重試。
            log.error("【RocketMq-Commercial】傳送失敗,訊息儲存到失敗記錄表,傳送 topic => {}, 傳送 tag => {}, msgBody => {}, key => {}(如果設定key,可以使用key查詢訊息),商用版需要在RocketMq雲服務重試,失敗原因為: {}"
            , topic, tag, msgBody, key, exception.getMessage());
            buildMqErrorInfoAndInsertDb(msgBody, exception);
        }
    });
     </pre>
     * @param topic 主題
     * @return void
     * @description
     * @param: tag  標籤
     * @param: msgBody 推送訊息
     * @param: key 方便訊息查詢的key
     * @param: sendCallback 手動回撥
     */
    public void singleAsyncSend(String topic, String tag, String msgBody, String key, SendCallback sendCallback) {
        if(Boolean.FALSE.equals(Boolean.valueOf(aliyunRocketMqConfig.getUseAliyunRocketMqServerEnable()))){
            throw new UnsupportedOperationException("請設定 UseAliyunRocketServer=true 切換到商用rocketMq模式");
        }
        if (StringUtils.isAnyBlank(topic, tag, msgBody)) {
            throw new IllegalArgumentException("topic, tag and msgBody must be not blank");
        }
        //對於使用非同步介面,建議設定單獨的回撥處理執行緒池,擁有更靈活的配置和監控能力。
        //如下構造執行緒的方式請求佇列為無界僅用作示例,有OOM的風險。
        //更合理的構造方式請參考阿里巴巴Java開發手冊: https://github.com/alibaba/p3c
        producer.setCallbackExecutor(THREAD_POOL_EXECUTOR);

        //迴圈傳送訊息
        Message msg = new Message( //
                // Message所屬的Topic
                topic,
                // Message Tag 可理解為Gmail中的標籤,對訊息進行再歸類,方便Consumer指定過濾條件在MQ伺服器過濾
                tag,
                // Message Body 可以是任何二進位制形式的資料, MQ不做任何干預
                // 需要Producer與Consumer協商好一致的序列化和反序列化方式
                msgBody.getBytes());
        // 設定代表訊息的業務關鍵屬性,請儘可能全域性唯一
        // 以方便您在無法正常收到訊息情況下,可透過MQ 控制檯查詢訊息並補發
        // 注意:不設定也不會影響訊息正常收發
        if (CharSequenceUtil.isNotBlank(key)) {
            msg.setKey(key);
        }
        // 傳送訊息,只要不拋異常就是成功
        try {
            producer.sendAsync(msg, sendCallback);
        } catch (ONSClientException exception) {
            //出現異常意味著傳送失敗,為了避免訊息丟失,建議快取該訊息然後進行重試。
            log.error("【RocketMq-Commercial】傳送失敗,訊息儲存到失敗記錄表,傳送 topic => {}, 傳送 tag => {}, msgBody => {}, key => {}(如果設定key,可以使用key查詢訊息),商用版需要在RocketMq雲服務重試,失敗原因為: {}"
                    , topic, tag, msgBody, key, exception.getMessage());
            buildMqErrorInfoAndInsertDb(msgBody, exception);
        }


    }

    /**
     * 阻塞單獨推送佇列,使用系統預設的Key生成規則
     *
     * @param topic 主題
     * @return void
     * @description 阻塞單獨推送佇列
     * @param: tag 標籤
     * @param: msgBody msgBody內容
     */
    public void singleSyncSend(String topic, String tag, String msgBody) {
        singleSyncSend(topic, tag, msgBody, null);
    }

    /**
     * 阻塞單獨推送佇列,使用系統預設的Key生成規則
     *
     * @param topic 主題
     * @return void
     * @description 阻塞單獨推送佇列
     * @param: tag 標籤
     * @param: msgBody msgBody內容
     */
    public void singleSyncSend(String topic, String tag, String msgBody, String key) {
        if(!Boolean.valueOf(aliyunRocketMqConfig.getUseAliyunRocketMqServerEnable())){
            throw new UnsupportedOperationException("請設定 UseAliyunRocketServer=true 切換到商用rocketMq模式");
        }
        if (StringUtils.isAnyBlank(topic, tag, msgBody)) {
            throw new IllegalArgumentException("topic, tag and msgBody must be not blank");
        }
        Message msg = new Message( //
                // Message所屬的Topic
                topic,
                // Message Tag 可理解為Gmail中的標籤,對訊息進行再歸類,方便Consumer指定過濾條件在MQ伺服器過濾
                tag,
                // Message Body 可以是任何二進位制形式的資料, MQ不做任何干預
                // 需要Producer與Consumer協商好一致的序列化和反序列化方式
                msgBody.getBytes());
        // 設定代表訊息的業務關鍵屬性,請儘可能全域性唯一
        // 以方便您在無法正常收到訊息情況下,可透過MQ 控制檯查詢訊息並補發
        // 注意:不設定也不會影響訊息正常收發
        if (CharSequenceUtil.isNotBlank(key)) {
            msg.setKey(key);
        }
        // 傳送訊息,只要不拋異常就是成功
        try {
            SendResult sendResult = producer.send(msg);
            log.info("【RocketMq-Commercial】推送成功,singleSyncSend => {}", JSON.toJSONString(sendResult));
        } catch (ONSClientException e) {
            log.error("【RocketMq-Commercial】傳送失敗,訊息儲存到失敗記錄表,傳送 topic => {}, 傳送 tag => {}, msgBody => {}, key => {}(如果設定key,可以使用key查詢訊息),商用版需要在RocketMq雲服務重試,失敗原因為: {}"
                    , topic, tag, msgBody, key, e.getMessage());
            buildMqErrorInfoAndInsertDb(msgBody, e);
        }

    }

    /**
     * 構建mq錯誤資訊,並且插入到異常推送表
     *
     * @param msgBody
     * @return void
     * @description 構建mq錯誤資訊,並且插入到異常推送表
     * @param: e
     */
    private void buildMqErrorInfoAndInsertDb(String msgBody, ONSClientException e) {
        //出現異常意味著傳送失敗,為了避免訊息丟失,建議快取該訊息然後進行重試。
        //出現異常,儲存異常資訊到資料庫
        SaMqMessageFail saMqMessageFail = new SaMqMessageFail();
        saMqMessageFail.setMqId(IdUtil.randomUUID());
        saMqMessageFail.setQueueName("RocketMq-Commercial");
        saMqMessageFail.setQueueMessage(msgBody);
        // 2 代表失敗
        saMqMessageFail.setStatus(2);
        // 1 代表重試次數
        saMqMessageFail.setProcCount(1);
        saMqMessageFail.setFailReason(e.toString());
        saMqMessageFail.setCreateDate(new Date());
        saMqMessageFailService.insert(saMqMessageFail);
    }

}
注入ProductBean的方式可以從上面提到的SpringBoot整合方式處理。
  1. 現在我們解決之前遺留的問題,如果是消費者,應該如何更優雅的接收訊息?首先我們需要明白,商用RocketMq注入是需要依靠手動構建監聽器,但是我們上面提到SpringBoot提供了註解+ @Component的方式實現佇列監聽的消費者。此外為了避免和開源版本衝突,我們使用之前引數配置自定義的開關,在遇到開源版本的時候我們返回null(這裡返回null Spring會掃描獲取SpringBoot的RocketMq依賴,不會出現報錯和無法啟動的問題,和前面的情況略有不同)防止自動注入商用RocketMq的監聽器。

下面是商用版本注入ConsumerBean的程式碼,訂閱商用RocketMq的消費監聽器:

/**
 *  阿里雲商用Rocketmq 佇列接收
 *  商用rocketmqConsumer的所有請求會進入一個入口,需要分發到不同的具體業務處理。
 **/
@Component
@Slf4j
public class AliyunCommercialRocketMqQueueConsumer {

    @Autowired
    private AliyunCommercialRocketMqConfig aliyunCommercialRocketMqConfig;

    // 自定義的監聽器
    @Autowired
    private AliyunCommerciaRocketMqTestListener aliyunCommerciaRocketMqTestListener;

    @Bean(initMethod = "start", destroyMethod = "shutdown")
    public ConsumerBean buildConsumer() {
        // 如果開關為false, 則不能注入此物件,否則商用的API會頂替掉 框架諸如的Bean出現異常
        if(!Boolean.valueOf(aliyunCommercialRocketMqConfig.getUseAliyunRocketServer())){
            log.warn("非商用版本不注入商用版本監聽器");
            return null;
        }
        ConsumerBean consumerBean = new ConsumerBean();
        //配置檔案
        Properties properties = aliyunCommercialRocketMqConfig.getRocketMqProperty();
        properties.setProperty(PropertyKeyConst.GROUP_ID, aliyunCommercialRocketMqConfig.getGroupId());
        //將消費者執行緒數固定為20個 20為預設值
        properties.setProperty(PropertyKeyConst.ConsumeThreadNums, aliyunCommercialRocketMqConfig.getConsumeThreadNums());
        consumerBean.setProperties(properties);
        //訂閱關係
        Map<Subscription, MessageListener> subscriptionTable = new HashMap<>(2);
        // 使用了之前定義的測試用的監聽器
        subscriptionTable.put(AliyunCommercialRocketMqConstants.QUEUE_TEST, aliyunCommerciaRocketMqTestListener);
        //訂閱多個topic如上面設定

        consumerBean.setSubscriptionTable(subscriptionTable);
        return consumerBean;
    }


}

AliyunCommerciaRocketMqTestListener 自定義監聽器為了相容商用和開源版本做了下面的改變,為了方便理解這裡加入了相關注解:

/**
 * 監聽器開發模板
 **/
@Slf4j
@Component
// 開源版本可以相容此註解
@RocketMQMessageListener(topic = RocketMqKey.TEST_TOPIC, consumerGroup = RocketMqKey.TEST_GROUP, selectorExpression = RocketMqKey.TEST_TAG)
// RocketMQListener<MessageExt> 屬於SpringBoot RocketMq的依賴
// MessageListener 屬於商用RocketMq的SDK
public class QueueRemoteReconListener implements RocketMQListener<MessageExt>, MessageListener {


    // 開源版本的使用
    @Override
    public void onMessage(MessageExt message) {
        // 開源版本的SpringBoot方式,和上文介紹相同
    }

    /** 
     * @description  
     * @param message 訊息類. 一條訊息由主題, 訊息體以及可選的訊息標籤, 自定義附屬鍵值對構成.
     * 注意: 我們對每條訊息的自定義鍵值對的長度沒有限制, 但所有的自定義鍵值對, 系統鍵值對序列化後, 所佔空間不能超過32767位元組.
     * @param: context 每次消費訊息的上下文,供將來擴充套件使用
     * @return com.aliyun.openservices.ons.api.Action 
     */ 
    @Override
    public Action consume(Message message, ConsumeContext context) {
        // 商用版本要求實現這個方法,Action如下,預設失敗重試16次,返回ReconsumeLater會觸發重試機制,所以會存在重複消費的問題
        //public enum Action {
            /**
             * 消費成功,繼續消費下一條訊息
             */
            //CommitMessage,
            /**
             * 消費失敗,告知伺服器稍後再投遞這條訊息,繼續消費其他訊息
             */
            //ReconsumeLater,
        //}

    }
}

以上就是消費者的相容處理,既可以滿足開源版本的註解開發要求,也可以不膨脹類的情況下沿用擴充套件。

Spring Cloud Alibaba專案相容小結

從個人的角度來看,在生產者的相容上比較容易實現,按照官方的demo構建帶商用RocketMq的ProducerBean即可,而消費者的整合則要複雜一些,這裡為了考慮不重複的設定或者寫重複程式碼,把關鍵的配置靜態化,同時為了官方寫的粗糙的訊息訂閱類做了繼承覆蓋的操作相容。此外為了防止開源版本的消費者Bean被商用的監聽器覆蓋導致失效,使用了簡單的開關來進行Bean的注入控制,寫的比較粗糙,讀者有更好的寫法歡迎討論。

總體來看來說商用版本的RocketMq整合起來略微麻煩,但是還是可以接受。此外也可以看到SDK的相容性實際上是比較簡陋的,很多地方的很像但是又完全是自己重新設計的,感覺就是一個新的團隊給老團隊做出來的東西做相容的感覺,不過不就是橋接嘛,我也會,所以最後個人對付消費者相容就用來多介面實現的相容寫法了。

最終的整合效果是開源版本關閉注入bean的開關,配置為了照顧Properties對於不需要的配置進行佔位處理,而商用版本則開啟開關,透過自定義注入監聽器的方式頂替掉SpringBoot的依賴。

最後的效果是隻需要修改RocketMq的配置,啟動之後自動連線到相關的RocketMq。

開源版本:

#訊息佇列 - 開源版本
rocketmq:
  use-aliyun-rocketSever: false
  name-server: 192.168.0.92:9876
  producer:
    group: testGroup
    secret-key: NONE
    access-key: NONE
    timeoutMillis: 15000
    consumeThreadNums: 20
  consumer:
    group: testGroup
    secret-key: NONE
    access-key: NONE
    timeoutMillis: 15000
    consumeThreadNums: 20

商用版本

#訊息佇列 - 商用版本
rocketmq:
 # 開源RocketMq和商業版RocketMq切換開關
  use-aliyun-rocketSever: true
  name-server: http://xxxx.mq.aliyuncs.com:80
  producer:
# 目前uat藉助開發測試使用
    group: testGroup
      secret-key: xxx
      access-key: xxx
      timeoutMillis: 15000
      consumeThreadNums: 20
    consumer:
      group: testGroup
      secret-key: xxx
      access-key: xxx
      timeoutMillis: 15000
      consumeThreadNums: 20

阿里雲商用RocketMq簡單介紹(4.X版本)

官方介紹:https://help.aliyun.com/product/29530.html

有關RocketMq本身的介紹部分可以閱讀:[[【RocketMq】RocketMq 掃盲]],這裡挑了商用版本個人認為需要關注的幾個點進行介紹。注意這裡使用的商用RocketMq版本為4.X的版本。

計費模式

商用RocketMq主要分為下面幾個部分:

  • 主系列:標準版、專業版、鉑金版
  • 子系列:單節點版、叢集版

個人最後是白嫖了公司的商用叢集版RocketMq進行實驗,計費的方式分為包年包月按量付費,前者適用於流量比較大並且固定的情況,使用套餐比較划算,而後者按需付費則適用於RocketMq使用較少(或者不穩定)或者呼叫量較少的情況,個人學習也比較推薦按量付費模式,比包年包月划算很多。

如果出現欠費,在例項停服7天后兩種付費模式均會清除所有的RocketMq資料,而日常欠費則會被自動停用,嘛,和電話卡欠費差不多的道理,這裡就不過多擴充了。如果要退訂商用RocketMq的服務。按量付費可以隨時退訂,包年包月也會根據天數進行退訂。

PS:看完這一整套計費模式下來,發現還是挺良心的。讓我沒想到的是包年包月居然可以計算未使用的天數返還,這一點挺不錯的,用起來不用擔心被套進去。

接入方式

商用RocketMq的接入步驟如下:

官方在快速入門中提供了建立資源的解釋影片:https://help.aliyun.com/document_detail/441914.html。有兩個點需要注意,第一個點是RAM使用者必須要進行賬戶授權才能正常使用,第二個點是對外訪問方式設定,訊息佇列RocketMQ版支援VPC訪問和公網訪問。資源建立完成之後就可以使用官方提供的SDK收發訊息,在使用之前需要注意下面這些前提:

之後便是在專案中引入JAVA依賴和複製貼上模板程式碼驗證。

訊息佇列模型

商用RocketMq的基本使用邏輯如下:

按照訊息型別,商用RocketMq提供了下面的訊息型別:

SDK接入參考

SDK接入的主頁連結如下:

https://help.aliyun.com/document_detail/69111.html

主要介紹了下面幾個點,實際參考第二個推薦的接入方式即可。

對於JAVA應用程式建議使用TCP協議SDK,整合的方式也比較簡單,為了方便理解,也可以透過以下連結獲取相關Demo,裡面都有詳細的註釋解釋:

如果是HTTP協議的SDK,則使用下面的依賴包:

<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>

如果是TCP協議的接入方式,則使用下面的依賴包:

<dependency>
    <groupId>com.aliyun.openservices</groupId>
    <artifactId>ons-client</artifactId>
    <!--以下版本號請替換為Java SDK的最新版本號-->
    <version>1.8.8.1.Final</version>
</dependency>                            

接入細節說明

個人在實驗的時候發現HTTP協議的版本整合SDK更像是給了一套API工具包,好處是可以不需要依賴Spring框架等單獨使用,但是最大的缺點也是這裡,很難用於框架當中,如果要實現自動接收訊息並且處理,需要依靠手動實現定時任務拉取訊息實現自動消費,這一點也比較蛋疼。

這裡列舉 HTTP的SDK的傳送和接受程式碼,首先是生產者的模板程式碼,這些程式碼和開源的RocketMq使用類似:

// 省略大量程式碼。。。
// 迴圈傳送4條訊息。
            for (int i = 0; i < 4; i++) {
                TopicMessage pubMsg;        // 普通訊息。
                pubMsg = new TopicMessage(
                // 訊息內容。
                "hello mq!".getBytes(),
                // 訊息標籤。
                "A"
                );
                // 設定訊息的自定義屬性。
                pubMsg.getProperties().put("a", String.valueOf(i));
                // 設定訊息的Key。
                pubMsg.setMessageKey("MessageKey");
               
            // 同步傳送訊息,只要不拋異常就是成功。
            TopicMessage pubResultMsg = producer.publishMessage(pubMsg);

            // 同步傳送訊息,只要不拋異常就是成功。
            System.out.println(new Date() + " Send mq message success. Topic is:" + topic + ", msgId is: " + pubResultMsg.getMessageId()
                        + ", bodyMD5 is: " + pubResultMsg.getMessageBodyMD5());
            }
        } catch (Throwable e) {
            // 訊息傳送失敗,需要進行重試處理,可重新傳送這條訊息或持久化這條資料進行補償處理。
            System.out.println(new Date() + " Send mq message failed. Topic is:" + topic);
            e.printStackTrace();
        }
// 省略大量程式碼。。。

而在接收方稍微麻煩一些,官方的案例是使用 死迴圈不斷檢查Broker是否有積壓訊息,如果有則透過主動拉取訊息的模式去拉取訊息。

實現多執行緒等待的效果可以寫入到Thread繼承類的Run方法中。
 // 在當前執行緒迴圈消費訊息,建議多開個幾個執行緒併發消費訊息。
        do {
            
            // 省略大量程式碼

// 處理業務邏輯。
            for (Message message : messages) {
                System.out.println("Receive message: " + message);
            }

            // 訊息重試時間到達前若不確認訊息消費成功,則訊息會被重複消費。
            // 訊息控制程式碼有時間戳,同一條訊息每次消費的時間戳都不一樣。
            {
                List<String> handles = new ArrayList<String>();
                for (Message message : messages) {
                    handles.add(message.getReceiptHandle());
                }

                try {
                    consumer.ackMessage(handles);
                } catch (Throwable e) {
                    // 某些訊息的控制程式碼可能超時,會導致訊息消費狀態確認不成功。
                    if (e instanceof AckMessageException) {
                        AckMessageException errors = (AckMessageException) e;
                        System.out.println("Ack message fail, requestId is:" + errors.getRequestId() + ", fail handles:");
                        if (errors.getErrorMessages() != null) {
                            for (String errorHandle :errors.getErrorMessages().keySet()) {
                                System.out.println("Handle:" + errorHandle + ", ErrorCode:" + errors.getErrorMessages().get(errorHandle).getErrorCode()
                                        + ", ErrorMsg:" + errors.getErrorMessages().get(errorHandle).getErrorMessage());
                            }
                        }
                        continue;
                    }
                    e.printStackTrace();
                }
            }
       } while (true);
     // 省略大量程式碼
            

寫在最後

算是一次個人相容的筆記。還是要吐槽商用的RocketMq整合真的挺無語的,不過阿里的東西懂的都懂。

相關文章