公司对外输出服务 希望对消息队列抽象 在不修改业务代码的情况下 替换MQ 例如 kafka 替换 阿里云 RocketMQ
:
kafka :Apache 自有协议 性能 十万级 消费消息 只支持 pull
RocketMQ :Ali 自有协议 性能 十万级 消费消息 支持 pull/push
rabbitmq 或者 activeMQ:支持AMQP协议 性能 万级 消费消息 支持 pull/push
Redis:Redis的 MQ 实现是使用 lists (队列)数据类型 使用 lpush 入队和 brpop 阻塞出队列的方式 非典型简单的消息队列
所有的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);
}