概述

本次验证金蝶MQ使用,验证结果将同步该文档
验证点:
1.金蝶MQ是否适配JDK8和JDK17
2.金蝶MQ是否适配现有MQ场景(目前内部主要是采用了rabbitMQ的主题模式)
3.是否支持队列模式、订阅模式、广播模式、延时队列、事物消息、以及广播模式下消息消费是否支持幂等
4.死信队列/延时队列验证
5.业务服务接入验证是否通过

验证说明

以下验证代码在rabbitmq下都可以正常运行,切换金蝶mq验证

验证结论

  • 优势:目前预研的几款国产化mq(宝兰德mq/金蝶mq/东方通)中代码适配度最高的一套方案,不需要修改rabbitmq的代码,只需要修改服务地址
  • 缺陷:
    (1)不支持rabbitmq的插件,比如通过插件实现的rabbitmq延时队列(有替代方案:可以通过死信队列实现)
    (2)不支持rabbitmq的事务消息,比如确认模式和回退模式的回调
  • 未确认点:mq吞吐和并发量

连接配置

#金蝶mq连接配置
spring.rabbitmq.host = 192.168.xx.xx
spring.rabbitmq.port = 5672
spring.rabbitmq.username = xxxx
spring.rabbitmq.password = xxxx
spring.rabbitmq.virtual-host = xxx
#死信队列配置
# 允许消息消费失败的重试
spring.rabbitmq.listener.simple.retry.enabled = true
# 消息最多消费次数3次
spring.rabbitmq.listener.simple.retry.max-attempts = 3
# 消息多次消费的间隔1秒
spring.rabbitmq.listener.simple.retry.initial-interval = 1000
#  设置为false,会丢弃消息或者重新发布到死信队列
spring.rabbitmq.listener.simple.default-requeue-rejected = false
# 开启回退模式
spring.rabbitmq.publisher-returns=true
#开启ack手动签收,message.getMessageProperties().getDeliveryTag()递增才能生效
spring.rabbitmq.listener.simple.acknowledge-mode=manual

是否适配JDK8和JDK17-可用

  • 验证结论:可用

适配现有MQ场景验证-可用

  • 验证结论:可用
  • 声明绑定交互机、队列、路由
public final static String BASIC_REGULATOR_INSERT_QUEUE = "checkInsertQueue";
public final static String BASIC_REGULATOR_INSERT_TOPIC_EXCHANGE = "checkTopicExchange";
public final static String BASIC_REGULATOR_INSERT_KEY = "checkOne";
@Bean(name = BASIC_REGULATOR_INSERT_QUEUE)
public Queue getgpaBasicPomlCheckUserInsertQueue1() {
return new Queue(BASIC_REGULATOR_INSERT_QUEUE);
}
@Bean(name = BASIC_REGULATOR_INSERT_TOPIC_EXCHANGE)
TopicExchange getGpaPomlTopicExchange1() {
return new TopicExchange(BASIC_REGULATOR_INSERT_TOPIC_EXCHANGE);
}
@Bean
Binding bindingPomlUserInsertToTopicExchange1(
@Qualifier(BASIC_REGULATOR_INSERT_QUEUE) Queue queue,
@Qualifier(BASIC_REGULATOR_INSERT_TOPIC_EXCHANGE) TopicExchange topicExchange) {
return BindingBuilder.bind(queue)
.to(topicExchange)
.with(BASIC_REGULATOR_INSERT_KEY);
}
  • 消息发送
@ApiOperation("exchangeTopicCheck")
@GetMapping("/exchangeTopicCheck")
public Result<Boolean> exchangeTopicCheck() {
  String msg = "Exchange.Topic msg";
  System.out.println("Exchange.Topic验证发送:" + msg);
  amqpTemplate.convertAndSend(ExchangeTopicMqConfig.BASIC_REGULATOR_INSERT_TOPIC_EXCHANGE,
  ExchangeTopicMqConfig.BASIC_REGULATOR_INSERT_KEY,
  msg);
  return Result.ok(true);
  }
  • 消息消费
@RabbitListener(queues = ExchangeTopicMqConfig.BASIC_REGULATOR_INSERT_QUEUE)
public void rabbitRegulatorInsertConsumer(String string) {
log.info("接收到Exchange.Topic消息:" + string);
}
  • 运行效果
    在这里插入图片描述

直接通过队列收发消息验证-不可用

  • 验证结论:不可用,金蝶mq不能直接通过队列收发消息
  • 声明队列
@Bean
public Queue simpleQueue() {
return new Queue("simple.queue");
}
  • 消息发送
@ApiOperation("sendByQueueCheck")
@GetMapping("/sendByQueueCheck")
public Result<Boolean> sendByQueueCheck() {
  String msg = "sendByQueueCheck";
  System.out.println("队列模式验证发送:" + msg);
  amqpTemplate.convertAndSend("simple.queue", msg);
  return Result.ok(true);
  }
  • 消息消费
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueueMessage(String msg) {
System.out.println("spring 消费者接收到消息:【" + msg + "】");
}
  • 运行效果
    在这里插入图片描述

广播模式fanout交换机验证-可用

  • 验证结论:可用
  • 声明绑定交互机、队列、路由
@Bean
public FanoutExchange fanoutExchange() {
return new FanoutExchange("itcast.fanout");
}
@Bean
public Queue fanoutQueue1() {
return new Queue("fanout.queue1");
}
@Bean
public Binding bindingQueue1(Queue fanoutQueue1, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
}
@Bean
public Queue fanoutQueue2() {
return new Queue("fanout.queue2");
}
@Bean
public Binding bindingQueue2(Queue fanoutQueue2, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
}
  • 消息发送
@ApiOperation("sendByFanoutCheck")
@GetMapping("/sendByFanoutCheck")
public Result<Boolean> sendByFanoutCheck() {
  String msg = "sendByFanoutCheck";
  System.out.println("广播模式fanout验证发送:" + msg);
  amqpTemplate.convertAndSend("itcast.fanout", null, msg);
  return Result.ok(true);
  }
  • 消息消费
@RabbitListener(queues = "fanout.queue1")
public void listenFanoutQueue1(String msg) {
System.out.println("消费者1接收到Fanout消息:【" + msg + "】");
}
@RabbitListener(queues = "fanout.queue2")
public void listenFanoutQueue2(String msg) {
System.out.println("消费者2接收到Fanout消息:【" + msg + "】");
}
  • 运行效果
    在这里插入图片描述

路由模式direct交换机验证-可用

  • 验证结论:可用
  • 消息发送
@ApiOperation("sendByDirectCheck")
@GetMapping("/sendByDirectCheck")
public Result<Boolean> sendByDirectCheck() {
  String msg = "sendByFanoutCheck";
  System.out.println("路由模式direct验证发送:" + msg);
  amqpTemplate.convertAndSend("itcast.direct", "red", msg);
  return Result.ok(true);
  }
  • 声明绑定交互机、队列、路由和消息消费
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue1"),
exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),
key = {"red", "blue"}
))
public void listenDirectQueue1(String msg) {
System.out.println("消费者接收到direct.queue1的消息:【" + msg + "】");
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue2"),
exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),
key = {"red", "yellow"}
))
public void listenDirectQueue2(String msg) {
System.out.println("消费者接收到direct.queue2的消息:【" + msg + "】");
}
  • 运行效果
    在这里插入图片描述

主题模式topic交换机验证-可用

  • 验证结论:可用
  • 消息发送
@ApiOperation("sendByTopicCheck")
@GetMapping("/sendByTopicCheck")
public Result<Boolean> sendByTopicCheck() {
  String msg = "sendByTopicCheck";
  System.out.println("主题模式topic验证发送:" + msg);
  amqpTemplate.convertAndSend("itcast.topic", "china.news", msg);
  return Result.ok(true);
  }
  • 声明绑定交互机、队列、路由和消息消费
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue1"),
exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),
key = "china.#"
))
public void listenTopicQueue1(String msg) {
System.out.println("消费者接收到topic.queue1的消息:【" + msg + "】");
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue2"),
exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),
key = "#.news"
))
public void listenTopicQueue2(String msg) {
System.out.println("消费者接收到topic.queue2的消息:【" + msg + "】");
}
  • 运行效果
    在这里插入图片描述

死信队列验证-可用

  • 验证结论:可用
  • 配置
#死信队列配置
# 允许消息消费失败的重试
spring.rabbitmq.listener.simple.retry.enabled=true
# 消息最多消费次数3次
spring.rabbitmq.listener.simple.retry.max-attempts=3
# 消息多次消费的间隔1秒
spring.rabbitmq.listener.simple.retry.initial-interval=1000
#  设置为false,会丢弃消息或者重新发布到死信队列
spring.rabbitmq.listener.simple.default-requeue-rejected=false
  • 声明绑定交互机、队列、路由
//--------------重定向交换机-begin--------------
public static final String REDIRECT_EXCHANGE = "TREDIRECT_EXCHANGE";//重定向交换机
public static final String REDIRECT_ROUTING_KEY = "TTREDIRECT_KEY_R"; //重定向队列routing key
public static final String REDIRECT_QUEUE = "TREDIRECT_QUEUE"; //重定向消息存储queue
/**
* 定义重定向交换机
*/
@Bean("redirectExchange")
public Exchange redirectExchange() {
return ExchangeBuilder.directExchange(REDIRECT_EXCHANGE).durable(true).build();
}
/**
* 定义重定向消息存储queue
*/
@Bean("redirectQueue")
public Queue redirectQueue() {
return QueueBuilder.durable(REDIRECT_QUEUE).build();
}
/**
* 将重定向队列通过重定向routingKey(TKEY_R)绑定到消息交换机上
*
* @return the binding
*/
@Bean
public Binding redirectBinding() {
return new Binding(REDIRECT_QUEUE, Binding.DestinationType.QUEUE, REDIRECT_EXCHANGE
, REDIRECT_ROUTING_KEY, null);
}
//--------------重定向交换机-end--------------
//--------------死信队列交换机-begin--------------
public static final String MESSAGE_EXCHANGE = "TDL_EXCHANGE";//消息接收的交换机
public static final String MESSAGE_ROUTING_KEY = "TDL_KEY";//消息routing key
public static final String MESSAGE_QUEUE = "TDL_QUEUE"; //消息存储queue
/**
* 定义消息接收交换机
* 交换机类型没有关系 不一定为directExchange  不影响该类型交换机的特性.
*/
@Bean("messageExchange")
public Exchange messageExchange() {
return ExchangeBuilder.directExchange(MESSAGE_EXCHANGE).durable(true).build();
}
/**
* 定义消息存储queue,设置死信交换机和死信路由key,本例子当异常时将消息
* 发送到x-dead-letter-exchange,然后x-dead-letter-exchange根据x-dead-letter-routing-key将消息路由到REDIRECT_QUEUE
*/
@Bean("messageQueue")
public Queue messageQueue() {
Map<String, Object> args = new HashMap<>(2);
  //       x-dead-letter-exchange    声明  死信队列Exchange
  args.put("x-dead-letter-exchange", REDIRECT_EXCHANGE);
  //       x-dead-letter-routing-key    声明 队列抛出异常并重试无效后进入重定向队列的routingKey(TKEY_R)
  args.put("x-dead-letter-routing-key", REDIRECT_ROUTING_KEY);
  return QueueBuilder.durable(MESSAGE_QUEUE).withArguments(args).build();
  }
  /**
  * 消息队列绑定到消息交换器上.,第二个参数需要指定类型为队列来区分,因为交换机可以绑定交换机
  *
  * @return the binding
  */
  @Bean
  public Binding messageBinding() {
  return new Binding(MESSAGE_QUEUE, Binding.DestinationType.QUEUE, MESSAGE_EXCHANGE
  , MESSAGE_ROUTING_KEY, null);
  }
  //--------------死信队列交换机-end--------------
  • 消息发送
@ApiOperation("dLXCheck")
@GetMapping("/dLXCheck")
public Result<Boolean> dLXCheck(@RequestParam(value = "number") Integer number) {
  System.out.println("死信队列验证发送 : " + number);
  // 发送消息并制定routing key
  amqpTemplate.convertAndSend(
  DeadLetterMqConfig.MESSAGE_EXCHANGE,
  DeadLetterMqConfig.MESSAGE_ROUTING_KEY,
  number);
  return Result.ok(true);
  }
  • 消息消费
@RabbitListener(queues = DeadLetterMqConfig.MESSAGE_QUEUE)
public void meetingRoomCancelPushQueue1(Integer number) {
log.info("Listener.死信队列消息接受:" + number + "/0 ");
int i = number / 0;
}
@RabbitListener(queues = DeadLetterMqConfig.REDIRECT_QUEUE)
public void meetingRoomCancelPushQueue2(Integer number) {
log.info("Listener.死信队列重定向消息接受:" + number);
}
  • 运行效果
    在这里插入图片描述
    在这里插入图片描述

延时队列实现验证(基于死信队列实现)-可用

  • 验证结论:可用
  • 声明绑定交互机、队列、路由
public static final String X_CHANGE = "X";
private static final String QUEUE_A = "QA";
private static final String QUEUE_B = "QB";
private static final String Y_DEAD_LETTER_EXCHANGE = "Y";
private static final String DEAD_LETTER_QUEUE = "QD";
@Bean
public DirectExchange xExchange() {
return new DirectExchange(X_CHANGE);
}
@Bean
public DirectExchange yExchange() {
return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);
}
/**
* 声明队列A 的消息ttl为10s,并绑定到对应的死信交换机
*/
@Bean("queueA")
public Queue queueA() {
HashMap<String, Object> args = new HashMap<>();
  args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
  args.put("x-dead-letter-routing-key", "YD");
  //延时10秒
  args.put("x-message-ttl", 10000);
  return QueueBuilder.durable(QUEUE_A).withArguments(args).build();
  }
  /**
  * 声明队列 A 绑定 X 交换机
  */
  @Bean
  public Binding queueaBindingX(@Qualifier("queueA") Queue queueA,
  @Qualifier("xExchange") DirectExchange xExchange) {
  return BindingBuilder.bind(queueA).to(xExchange).with("XA");
  }
  /**
  * 声明队列 B ttl 为 40s 并绑定到对应的死信交换机
  */
  @Bean("queueB")
  public Queue queueB() {
  Map<String, Object> args = new HashMap<>(3);
    //声明当前队列绑定的死信交换机
    args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
    //声明当前队列的死信路由 key
    args.put("x-dead-letter-routing-key", "YD");
    //声明队列的 TTL,延时40秒
    args.put("x-message-ttl", 40000);
    return QueueBuilder.durable(QUEUE_B).withArguments(args).build();
    }
    /**
    * 声明队列 B 绑定 X 交换机
    */
    @Bean
    public Binding queuebBindingX(@Qualifier("queueB") Queue queue1B,
    @Qualifier("xExchange") DirectExchange xExchange) {
    return BindingBuilder.bind(queue1B).to(xExchange).with("XB");
    }
    /**
    * 声明死信队列 QD
    */
    @Bean("queueD")
    public Queue queueD() {
    return new Queue(DEAD_LETTER_QUEUE);
    }
    /**
    * 声明死信队列 QD 绑定关系
    */
    @Bean
    public Binding deadLetterBindingQAD(@Qualifier("queueD") Queue queueD,
    @Qualifier("yExchange") DirectExchange yExchange) {
    return BindingBuilder.bind(queueD).to(yExchange).with("YD");
    }
  • 消息发送
@ApiOperation("tTLCheck")
@GetMapping("/tTLCheck")
public Result<Boolean> tTLCheck() {
  String msg = "tTLCheck msg,time" + new Date();
  System.out.println("延时队列验证发送 : " + msg);
  // 发送消息并制定routing key
  amqpTemplate.convertAndSend(TtlQueueConfig.X_CHANGE, "XA", msg);
  amqpTemplate.convertAndSend(TtlQueueConfig.X_CHANGE, "XB", msg);
  return Result.ok(true);
  }
  • 消息消费
@RabbitListener(queues = "QD")
public void receiveD(String string) throws IOException {
log.info("当前时间:{},收到死信队列信息{}", new Date(), string);
}
  • 运行效果
    在这里插入图片描述

消息确认机制:确认模式-不可用

  • 验证结论:不可用
  • 配置
spring.rabbitmq.publisher-confirm-type=correlated
  • 监听器
  • 消息发送
@ApiOperation("correlatedCheck")
@GetMapping("/correlatedCheck")
public Result<Boolean> correlatedCheck() {
  String msg = "correlatedCheck";
  System.out.println("消息可靠性投递-确认模式验证发送:" + msg);
  amqpTemplate.convertAndSend("itcast.top", "china.news", msg);
  return Result.ok(true);
  }
  • 声明绑定交互机、队列、路由
    未定义交互机、队列、路由,rabbitmq发送交换机失败会触发ConfirmCallback回调事件
  • 运行效果
    未触发ConfirmCallback回调事件
    ##消息确认机制:回退模式-不可用
  • 验证结论:不可用
  • 配置
# 开启回退模式
spring.rabbitmq.publisher-returns=true
  • 监听器
    在这里插入图片描述

  • 消息发送

@ApiOperation("publisherReturnsCheck")
@GetMapping("/publisherReturnsCheck")
public Result<Boolean> publisherReturnsCheck() {
  String msg = "publisherReturnsCheck";
  System.out.println("消息可靠性投递-回退模式验证发送:" + msg);
  amqpTemplate.convertAndSend("itcast.topic", null, msg);
  return Result.ok(true);
  }
  • 声明绑定交互机、队列、路由
    声明itcast.topic的TOPIC类型交换机未绑定对应队列,rabbitmq消息未从Exchange路由到Queue会触发ReturnCallback回调事件
  • 运行效果
    未触发ReturnCallback回调事件
    ##消息确认机制:ack验证-可用
  • 验证结论:可用
  • 配置
#开启ack手动签收,message.getMessageProperties().getDeliveryTag()递增才能生效
spring.rabbitmq.listener.simple.acknowledge-mode=manual
  • 消息发送
@ApiOperation("ackCheck")
@GetMapping("/ackCheck")
public Result<Boolean> ackCheck() {
  String msg = "ackCheck";
  System.out.println("ack验证发送:" + msg);
  amqpTemplate.convertAndSend("itcast.ack", "ack", msg);
  return Result.ok(true);
  }
  • 声明绑定交互机、队列、路由和消息消费
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queuef"),
exchange = @Exchange(name = "itcast.ack", type = ExchangeTypes.DIRECT),
key = {"ack"}
))
public void listenAckQueue(Message message, Channel channel) throws InterruptedException, IOException {
// 消息投递序号,消息每次投递该值都会+1,spring.rabbitmq.listener.simple.acknowledge-mode=manual 才能生效
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
if (deliveryTag < 5) {
//模拟处理消息出现bug
int i = 1 / 0;
}
System.out.println("成功接受到消息:" + message);
// 签收消息
/**
* 参数1:消息投递序号
* 参数2:是否一次可以签收多条消息
*/
channel.basicAck(deliveryTag, true);
} catch (Exception e) {
System.out.println("消息消费失败!");
Thread.sleep(2000);
// 拒签消息
/**
* 参数1:消息投递序号
* 参数2:是否一次可以拒签多条消息
* 参数3:拒签后消息是否重回队列
*/
channel.basicNack(deliveryTag, true, true);
}
}
  • 运行效果
    未ack响应mq会重试发送
    在这里插入图片描述