背景:
公司對外輸出服務 希望對消息隊列抽象 在不修改業務代碼的情況下 替換MQ 例如 kafka 替換 阿里雲 RocketMQ
MQ分類整理
:
kafka :Apache 自有協議 性能 十萬級 消費消息 只支持 pull
RocketMQ :Ali 自有協議 性能 十萬級 消費消息 支持 pull/push
rabbitmq 或者 activeMQ:支持AMQP協議 性能 萬級 消費消息 支持 pull/push
Redis:Redis的 MQ 實現是使用 lists (隊列)數據類型 使用 lpush 入隊和 brpop 阻塞出隊列的方式 非典型簡單的消息隊列
MQ 發送消息抽象:
所有的MQ發送就是調用 可以直接抽象
public class OnsSendServiceImpl implements MQSendService {
private Producer onsProducer;
private String name;
private String groupId;
private String topic;
private String tag;
/**
* 發送mq消息
*
* @param msgId 消息id
* @param message 默認統一使用json
*/
@Override
public void sendMqMessage(String msgId, String message) {
log.info("send ons ,topic:{},tags:{},model:{},key:{}", topic, message, msgId);
try {
Message msg = new Message(topic, tag, msgId, message.getBytes(Charset.forName("utf-8")));
SendResult sendResult = onsProducer.send(msg);
log.info("ons消息發送成功。topic:{},,messageId:{},resultMessageId:{},key:{}", topic, msg.getMsgID(),
sendResult.getMessageId(), msgId);
} catch (Exception e) {
log.error("ons 信息發送失敗,topic:" + topic + ",key:" + msgId + ",e=", e);
}
}
##(重點來了) MQ 消費消息抽象:
參考spring-boot-data-kafka的 @KafkaListener 我們可以抽象一個 類似 @MqConsumer 註解在對應的消費方法上
掃描註解得到方法代理:MqListener 然後封裝組合統一使用pull的方式生成 Consumer
public class MqListener {
private Method method;
private Object bean;
private MqConsumer mqConsumer;
@Override
public String toString() {
return JSON.toJSONString(this);
}
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface MqConsumer {
/**
* 作爲Listener的名字
*
* @return
*/
String name();
String topic();
String tag() default "*";
String servers();
String secretKey() default "";
String accessKey() default "";
}
private synchronized void initMqConsumer(MqKafkaListener mqKafkaListener) {
if (kafkaConsumerMap.get(mqKafkaListener.getKafkaConsumer().name()) != null) {
logger.warn(mqKafkaListener.getKafkaConsumer().name() + "kafkaConsumer 已經註冊過,忽略");
return;
}
String group = this.placeHolderResolver.resolveStringValue(mqKafkaListener.getKafkaConsumer().group());
String topic = this.placeHolderResolver.resolveStringValue(mqKafkaListener.getKafkaConsumer().topic());
String consumerName = mqKafkaListener.getKafkaConsumer().name();
KafkaConsumer<String, String> kafkaConsumer = kafkaConsumerMap.get(consumerName);
if (null == kafkaConsumer) {
Properties props = new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, mqKafkaListener.getKafkaConsumer().servers());
props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 25000);
props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 30);
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization" +
".StringDeserializer");
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization" +
".StringDeserializer");
props.put(ConsumerConfig.GROUP_ID_CONFIG, group);
kafkaConsumer = new KafkaConsumer<>(props);
List<String> subscribedTopics = new ArrayList<>();
subscribedTopics.add(topic);
kafkaConsumer.subscribe(subscribedTopics);
kafkaConsumerMap.put(consumerName, kafkaConsumer);
try {
KafkaConsumer<String, String> finalKafkaConsumer = kafkaConsumer;
threadPool.submit(() -> {
ConsumerRecords<String, String> records = finalKafkaConsumer.poll(1000);
for (ConsumerRecord<String, String> record : records) {
Optional<String> kafkaMessage = Optional.ofNullable(record.value());
if (kafkaMessage.isPresent()) {
String message = kafkaMessage.get();
logger.info("接收到kafka的消息:{}", message);
try {
mqKafkaListener.getMethod().invoke(mqKafkaListener.getBean(), message);
} catch (Exception e) {
logger.error("調用應用系統失敗,看到此異常時,應該系統應該自行處理異常,不應該拋出,請修改!:{}", e);
}
}
}
});
} catch (Exception e) {
logger.error("調用應用系統失敗,看到此異常時,應該系統應該自行處理異常,不應該拋出,請修改!:{}", e);
}
}
}
MQ消費pull/push示例代碼:
RocketMQ Push 方式
Properties properties = new Properties();
// 您在控制檯創建的 Group ID
properties.put(PropertyKeyConst.GROUP_ID, "XXX");
// AccessKeyId 阿里雲身份驗證,在阿里雲服務器管理控制檯創建
properties.put(PropertyKeyConst.AccessKey, "XXX");
// AccesskeySecret 阿里雲身份驗證,在阿里雲服務器管理控制檯創建
properties.put(PropertyKeyConst.SecretKey, "XXX");
// 設置 TCP 接入域名,進入控制檯的實例管理頁面的獲取接入點信息區域查看
properties.put(PropertyKeyConst.NAMESRV_ADDR, "XXX");
// 集羣訂閱方式 (默認)
// properties.put(PropertyKeyConst.MessageModel, PropertyValueConst.CLUSTERING);
// 廣播訂閱方式
// properties.put(PropertyKeyConst.MessageModel, PropertyValueConst.BROADCASTING);
Consumer consumer = ONSFactory.createConsumer(properties);
consumer.subscribe("TopicTestMQ", "TagA||TagB", new MessageListener() { //訂閱多個 Tag
public Action consume(Message message, ConsumeContext context) {
System.out.println("Receive: " + message);
return Action.CommitMessage;
}
});
//訂閱另外一個 Topic,如需取消訂閱該 Topic,請刪除該部分的訂閱代碼,重新啓動消費端即可
consumer.subscribe("TopicTestMQ-Other", "*", new MessageListener() { //訂閱全部 Tag
public Action consume(Message message, ConsumeContext context) {
System.out.println("Receive: " + message);
return Action.CommitMessage;
}
});
consumer.start();
System.out.println("Consumer Started");
RocketMQ Pull 方式
Properties properties = new Properties();
properties.setProperty(PropertyKeyConst.GROUP_ID, "GID-xxxxx");
// AccessKeyId 阿里雲身份驗證,在阿里雲服務器管理控制檯創建
properties.put(PropertyKeyConst.AccessKey, "xxxxxxx");
// AccessKeySecret 阿里雲身份驗證,在阿里雲服務器管理控制檯創建
properties.put(PropertyKeyConst.SecretKey, "xxxxxxx");
// 設置 TCP 接入域名,進入控制檯的實例管理頁面的獲取接入點信息區域查看
properties.put(PropertyKeyConst.NAMESRV_ADDR, "xxxxx");
PullConsumer consumer = ONSFactory.createPullConsumer(properties);
// 啓動 Consumer
consumer.start();
// 獲取 topic-xxx 下的所有分區
Set<TopicPartition> topicPartitions = consumer.topicPartitions("topic-xxx");
// 指定需要拉取消息的分區
consumer.assign(topicPartitions);
while (true) {
// 拉取消息,超時時間爲 3000 ms
List<Message> messages = consumer.poll(3000);
System.out.printf("Received message: %s %n", messages);
}