引言
在阿里雲的官方網站提供了RocketMq的商用版本,但是個人在專案應用上發現和SpirngBoot以及Spring Cloud(Alibaba)等開源的RocketMQ依賴雖然可以正常相容,但是依然出現了註解失效、啟動報錯,商用和開源版本的不相容導致部分程式碼要重複編寫的蛋疼問題。
這樣的相容問題不是簡單加個SDK依賴,切換到商用配置就可以直接使用的(因為個人起初真就是這麼想),為了避免後面再遇到這種奇葩的開發測試用開源RocketMq,生產環境需要使用商用叢集的RocketMq的混合配置的業務場景,個人花了小半天時間熟讀阿里雲的接入文件,加上各種嘗試和測試,總結出一套可以快速使用的相容模板方案。
如果不瞭解阿里雲商用RocketMq,可以看最後一個大節的【阿里雲商用RocketMq介紹】介紹。個人的相容方案靈感來自於官方提供的這個DEMO專案:springboot/java-springboot-demo。
注意本方案是基於SpringBoot2.X和 Spring cloud Alibaba 的兩個專案環境構建專案基礎,在SpringBoot上只做了生產者的配置,而在Spring Cloud Alibaba的Nacos上進行了生產者和消費者的完整相容方案。
最後注意相容整合的版本為商用RocketMq使用4.x版本,最近新出的5.X 的版本並未進行測試,不保證正常使用。
相容關鍵點
- 在沿用SpringBoot的YML基礎配置基礎上實現商用和開源模式的相容。
- 商用RocketMq需要使用官方提供的依賴包,依賴包可以正常相容SpringBoot等依賴。
- 整合之後便於開發和擴充套件,並且易於其他開發人員理解。
- 兩種模式之間互相不會產生干擾。
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
,並且secretKey
和accessKey
以及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,具體的操作如下:
- 構建配置類,這裡仿照了官方提供的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;
}
}
- 構建商用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
的註釋:
這樣是好事還是壞事,大家自行體會。。。。。個人第一次看到的時候著實被震驚了。
- 做完上面兩步之後,我們就可以實現
RocketMqTemplate
呼叫請求,至此完成相容。
SpringBoot專案相容小結
這裡簡單小結一下SpringBoot的相容過程,可以看到整個步驟僅僅是 在商用RocketMq多做了一步bean注入的操作而已,整體使用上十分簡單。但是這裡只介紹了生產者的整合,那麼消費者如何相容?稍安勿躁,我們接著看Spring Cloud版本的整合案例。
Spring Cloud Alibaba專案相容
目前國內使用的比較多的是Spring Cloud Alibaba,注意這些配置都寫入到Nacos當中。
開源版本
開源版本的接入方式和SpringBoot是一樣的,這裡簡單回顧:
- 開源版本需要設定引數,這裡設定了生產者和消費者:
#訊息佇列 - 開源版本
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
- 新增依賴:
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot</artifactId>
<version>2.2.2</version>
</dependency>
- 使用
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
}
}
商用版本
這節是本文稍微複雜一點的部分,我們按照步驟介紹接入過程:
- 在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
- 新增商用RocketMq的TCP接入方式需要的依賴包。
<dependency>
<groupId>com.aliyun.openservices</groupId>
<artifactId>ons-client</artifactId>
<!--以下版本號請替換為Java SDK的最新版本號-->
<version>1.8.8.1.Final</version>
</dependency>
- 新增配置類,和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;
}
}
- 下一步是擴充套件官方訊息訂閱類,為啥要這樣做?主要是接入實驗中發現官方的訊息訂閱類沒有 全量引數的構造器,難以對於訊息訂閱類靜態引數化常量,構造一個訂閱訊息使用預設方式還需要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;
}
}
- 接著我們構建靜態化的訊息訂閱類,這個常量類會封裝系統需要使用到的訊息訂閱物件,在後續註冊監聽器需要使用,這裡先提前定義。
/**
* 佇列常規配置,用於啟動時候初始化配置,
* 所有配置均需要放置到此類中對外擴充套件使用
**/
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);
}
- 做好一系列準備之後,我們開始商用版本生產者相容,商用版本的傳送者可以像是下面這樣封裝官方提供的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整合方式處理。
- 現在我們解決之前遺留的問題,如果是消費者,應該如何更優雅的接收訊息?首先我們需要明白,商用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收發訊息,在使用之前需要注意下面這些前提:
- 步驟二:建立資源
您可以使用IntelliJ IDEA或者Eclipse,本文以IntelliJ IDEA Ultimate為例。
- 安裝1.8或以上版本JDK
- 安裝2.5或以上版本Maven
之後便是在專案中引入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整合真的挺無語的,不過阿里的東西懂的都懂。