• 消息中间件—SpringBoot下RabbitMQ实战


    消息中间件简介

    MQ全称(Message Queue)`又名消息队列,是一种异步通讯的中间件。可以将它理解成邮局,发送者将消息传递到邮局,然后由邮局帮我们发送给具体的消息接收者(消费者),具体发送过程与时间我们无需关心,它也不会干扰我进行其它事情。常见的MQ有kafkaactivemqrocketMQrabbitmq等等**

    消息中间件的应用场景

    跨系统数据传递、高并发流量削峰、数据异步处理。。。。

     

    消息中间件对比

    综上,各种对比之后,有如下建议:

    一般的业务系统要引入 MQ,最早大家都用 ActiveMQ,但是现在确实大家用的不多了,没经过大规模吞吐量场景的验证,社区也不是很活跃,所以大家还是算了吧,我个人不推荐用这个了;

    后来大家开始用 RabbitMQ,但是确实 erlang 语言阻止了大量的 Java 工程师去深入研究和掌控它,对公司而言,几乎处于不可控的状态,但是确实人家是开源的,比较稳定的支持,活跃度也高;

    不过现在确实越来越多的公司会去用 RocketMQ,确实很不错,毕竟是阿里出品,但社区可能有突然黄掉的风险(目前 RocketMQ 已捐给 Apache,但 GitHub 上的活跃度其实不算高)对自己公司技术实力有绝对自信的,推荐用 RocketMQ,否则回去老老实实用 RabbitMQ 吧,人家有活跃的开源社区,绝对不会黄。

    所以中小型公司,技术实力较为一般,技术挑战不是特别高,用 RabbitMQ 是不错的选择;大型公司,基础架构研发实力较强,用 RocketMQ 是很好的选择。

    如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。

     

    安装使用

    rabbitmq的windows版本安装,可以参考这个地址: https://blog.csdn.net/hzw19920329/article/details/53156015

     

    Rabbitmq基础概念

    Broker:简单来说就是消息队列服务器实体 Exchange:消息交换机,它指定消息按什么规则,路由到哪个队列 Queue:消息队列载体,每个消息都会被投入到一个或多个队列 Binding:绑定,它的作用就是把exchangequeue按照路由规则绑定起来 Routing Key:路由关键字,exchange根据这个关键字进行消息投递 vhost:虚拟主机,一个broker里可以开设多个vhost,用作不同用户的权限分离 producer:消息生产者,就是投递消息的程序 consumer:消息消费者,就是接受消息的程序 channel:消息通道,在客户端的每个连接里,可建立多个channel,每个channel代表一个会话任务

     

    rabbitmq运作流程

    上图所示,在整个消息中间件的使用过程中,我们主要配置的是exchange,queue,rooting key三个,下面的代码主要也是根据着三个进行消息的生产和消费。

    常用的交换机类型有:fanout、direct、topic、headers,他们特性如下:

    fanout:绑定的都发送,忽略路由,不用传routekey

    direct:全文匹配--常用

    topic:模糊匹配--常用

    headers:条件判断,性能很差,不建议使用,我们代码的demo,使用的是direct和topic,后续通过插件的形式,我们还添加了x-delayed-message这个延迟消息队列。

     

    springboot代码实现:

    步骤1:pom文件中引入相应的组件

    <!--rabbitmq -->
          <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-amqp</artifactId>
          </dependency>

    步骤2:部署好mq之后,在application.properties文件中添加rabbitmq相应的配置

    spring.rabbitmq.host=129.204.x.xxx
    spring.rabbitmq.port=5672
    spring.rabbitmq.username=xxx
    spring.rabbitmq.password=xxx
    spring.rabbitmq.virtual-host=xx

    spring.rabbitmq.publisher-confirms=true
    spring.rabbitmq.listener.simple.acknowledge-mode=manual

    步骤3:进行交换机,队列,routingkey的定义和绑定,具体的代码在RabbitConfig文件中。

    @Configuration
    public class RabbitConfig {

      //绑定键
      public final static String man = "kms.topic.man";
      public final static String woman = "kms.topic.woman";

      /**
        * 队列 起名:TestDirectQueue
        * @return
        */
      @Bean
      public Queue DirectQueue() {
          // durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
          // exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
          // autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
          //   return new Queue("TestDirectQueue",true,true,false);
          //一般设置一下队列的持久化就好,其余两个就是默认false
          return new Queue("kms.direct.queue",true);
      }

      /**
        * Direct交换机 起名:TestDirectExchange
        * @return
        */
      @Bean
      DirectExchange DirectExchange() {
          return new DirectExchange("kms.direct.exchange",true,false);
      }

      /**
        * 绑定 将队列和交换机绑定, 并设置用于匹配键:TestDirectRouting
        * @return
        */
      @Bean
      Binding bindingDirect() {
          return BindingBuilder.bind(DirectQueue()).to(DirectExchange()).with("kms.direct.routingKey");
      }

      @Bean
      public Queue firstTopicQueue() {
          return new Queue(RabbitConfig.man);
      }

      @Bean
      public Queue secondTopicQueue() {
          return new Queue(RabbitConfig.woman);
      }

      /**
        * 主题型交换机1
        * @return
        */
      @Bean
      TopicExchange topicExchange() {
          return new TopicExchange("kms.topic.exchange");
      }

      /**
        * 将firstQueue和topicExchange绑定,而且绑定的键值为topic.man
        * 这样只要是消息携带的路由键是topic.man,才会分发到该队列
        * @return
        */
      @Bean
      Binding bindingExchangeMessage() {
          return BindingBuilder.bind(firstTopicQueue()).to(topicExchange()).with(man);
      }

      /**
        * 将secondQueue和topicExchange绑定,而且绑定的键值为用上通配路由键规则topic.#
        * 这样只要是消息携带的路由键是以topic.开头,都会分发到该队列
        * @return
        */
      @Bean
      Binding bindingExchangeMessage2() {
          return BindingBuilder.bind(secondTopicQueue()).to(topicExchange()).with("kms.topic.#");
      }

      /**
        * TODO:RabbitMQ延迟队列
        * @return
        */
      @Bean
      public Queue delayQueue(){
          return QueueBuilder.durable("kms.delay.queue").build();
      }

      @Bean
      public CustomExchange delayExchange(){
          Map<String,Object> map= Maps.newHashMap();
          map.put("x-delayed-type","direct");
          return new CustomExchange("kms.delay.exchange","x-delayed-message",true,false,map);
      }

      @Bean
      public Binding delayBinding(){
          return BindingBuilder.bind(delayQueue()).to(delayExchange()).with("kms.delay.routingKey").noargs();
      }


    }

    步骤4:生产者创建消息的发布,代码在MqProviderService中,其中留意setup()方法,该方法的作用是为了判断消息是否成功发送到中间件, 消息发送完毕后,则回调此方法 ack代表发送是否成功。

    /**
    * 发送数据到mq
    * @Author: yechongbai
    * @Date: 2020/5/11 16:04
    * @Copyright: www.zektech.cn
    * @since 1.0
    */
    @Service
    public class MqProviderService extends GlobalResponseHandler {
      private final Logger logger = LoggerFactory.getLogger(MqProviderService.class);

      @Autowired
      private RabbitTemplate rabbitTemplate;

      @PostConstruct
      public void setup() {
          // 消息发送完毕后,则回调此方法 ack代表发送是否成功
          rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
              @Override
              public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                  System.out.println("进入回调。。。");
                  // ack为true,代表MQ已经准确收到消息
                  if (!ack) {
                      System.out.println("发送消息到MQ失败");
                      System.out.println("ConfirmCallback: "+"原因:"+cause);
                      return;
                  }
                  try {
                      System.out.println(correlationData.getId()+":成功发送消息到MQ");
                      // 修改本地消息表的状态为“已发送”。
                      //TODO:修改本地表
                  } catch (Exception e) {
                      logger.error("警告:修改本地消息表的状态时出现异常", e);
                  }

              }
          });
      }

      /**
        * 发送测试信息--直连交换机
        * @return
        */
      public String sendDirectMessage(){
          String messageId = String.valueOf(UUID.randomUUID());
          UserInfoVO userInfoVO=new UserInfoVO();
          userInfoVO.setPersonNo(messageId);
          userInfoVO.setPersonName("张三");
          userInfoVO.setPhoto("111");
          //将消息携带绑定键值:TestDirectRouting 发送到交换机TestDirectExchange
          System.out.println("推送消息:"+ JSON.toJSONString(userInfoVO));
          //步骤1.添加订单记录
          //前提:消息不能丢失情况需要插入,如果类似微信推送这些不一定需要送到的可以不执行步骤2
          // 步骤2.插入推送消息到数据库 步骤1和步骤2需要事务,才能保证后续该消息是否被消费
          // CorrelationData 当收到消息回执时,会附带上这个参数
          rabbitTemplate.convertAndSend("kms.direct.exchange", "kms.direct.routingKey", userInfoVO,new CorrelationData(messageId));
          return buildSuccessResult();
      }

      /**
        * 发送测试信息--主题交换机1
        * @return
        */
      public String sendTopicMessage(){
          String messageId = String.valueOf(UUID.randomUUID());
          UserInfoVO userInfoVO=new UserInfoVO();
          userInfoVO.setPersonNo(messageId);
          userInfoVO.setPersonName("主题交换机1");
          userInfoVO.setPhoto("111");
          //将消息携带绑定键值:TestDirectRouting 发送到交换机TestDirectExchange
          System.out.println("推送消息:"+ JSON.toJSONString(userInfoVO));
          //步骤1.添加订单记录
          //前提:消息不能丢失情况需要插入,如果类似微信推送这些不一定需要送到的可以不执行步骤2
          // 步骤2.插入推送消息到数据库 步骤1和步骤2需要事务,才能保证后续该消息是否被消费
          // CorrelationData 当收到消息回执时,会附带上这个参数
          rabbitTemplate.convertAndSend("kms.topic.exchange", "kms.topic.man", JSON.toJSONString(userInfoVO),new CorrelationData(messageId));
          return buildSuccessResult();
      }

      /**
        * 发送测试信息--主题交换机2
        * @return
        */
      public String sendTopicMessage2(){
          String messageId = String.valueOf(UUID.randomUUID());
          UserInfoVO userInfoVO=new UserInfoVO();
          userInfoVO.setPersonNo(messageId);
          userInfoVO.setPersonName("主题交换机2");
          userInfoVO.setPhoto("222");
          //将消息携带绑定键值:TestDirectRouting 发送到交换机TestDirectExchange
          System.out.println("推送消息:"+ JSON.toJSONString(userInfoVO));
          //步骤1.添加订单记录
          //前提:消息不能丢失情况需要插入,如果类似微信推送这些不一定需要送到的可以不执行步骤2
          // 步骤2.插入推送消息到数据库 步骤1和步骤2需要事务,才能保证后续该消息是否被消费
          // CorrelationData 当收到消息回执时,会附带上这个参数
          rabbitTemplate.convertAndSend("kms.topic.exchange", "kms.topic.woman", JSON.toJSONString(userInfoVO),new CorrelationData(messageId));
          return buildSuccessResult();
      }

      /**
        * 发送测试信息--延迟队列
        * @param ttl
        * @return
        */
      public String sendDelayMessage(Long ttl){
          String messageId = String.valueOf(UUID.randomUUID());
          UserInfoVO userInfoVO=new UserInfoVO();
          userInfoVO.setPersonNo(messageId);
          userInfoVO.setPersonName("张三");
          userInfoVO.setPhoto("111");
          System.out.println(LocalDateTime.now()+":推送消息:"+ JSON.toJSONString(userInfoVO));
          rabbitTemplate.convertAndSend("kms.delay.exchange", "kms.delay.routingKey", userInfoVO, new MessagePostProcessor() {
              @Override
              public Message postProcessMessage(Message message) throws AmqpException {
                  MessageProperties mp=message.getMessageProperties();
                  mp.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
                  mp.setHeader("x-delay",ttl);
                  System.out.println("延迟队列生产者-发出消息:"+JSON.toJSONString(userInfoVO)+",TTL:"+ttl);
                  return message;
              }
          }, new CorrelationData(messageId));
          return buildSuccessResult();
      }
    }

    步骤5:消息接收者---消费消息,新建MqConsumerService,进行消息的监听并且消费。 默认情况下spring-boot-data-amqp是自动ACK机制,就意味着 MQ 会在消息消费完毕后自动帮我们去ACK,这样依赖就存在这样一个问题:如果报错了,消息不会丢失,会无限循环消费,很容易就吧磁盘空间耗完,虽然可以配置消费的次数但这种做法也有失优雅。目前比较推荐的就是我们手动ACK然后将消费错误的消息转移到其它的消息队列中,做补偿处理。由于我们需要手动控制ACK,因此下面监听完消息后需要调用basicAck通知rabbitmq消息已被正确消费,可以将远程队列中的消息删除 ,所以需要手动执行:channel.basicAck(tag,true);

    @Component
    public class MqConsumerService {

      /**
        * 监听的队列名称 TestDirectQueue
        * @param testMessage
        * @param channel
        * @param tag
        * @throws IOException
        */
      @RabbitListener(queues = "kms.direct.queue")
      public void processDirect(UserInfoVO testMessage,Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
          try{
              System.out.println("DirectReceiver消费者收到消息 : "+ JSON.toJSONString(testMessage));
              //TODO:接收到消息进行业务操作,操作成功,告诉mq该消息已经消费
              //执行业务操作,同一个数据不能处理两次,根据业务情况去重,保证幂等性。 (拓展:redis记录处理情况)
              // 开启了手工确认机制,如果不加这个,项目重新启动,则改消息会被重新消费
              // 异常的话,可以选择让它重新入列,或者丢弃
              channel.basicAck(tag,true);
          }catch (Exception e){
              // 异常情况 :根据需要去: 重发/ 丢弃
              // 重发一定次数后, 丢弃, 日志告警,b1:true表示重新入列,false表示不入列
              channel.basicNack(tag, false, false);
              // 系统 关键数据,永远是有人工干预
          }
      }

      /**
        * 监听的队列名称 TestDirectQueue
        * @param testMessage
        * @param channel
        * @param tag
        * @throws IOException
        */
      @RabbitListener(queues = "kms.topic.man")
      public void processTopic(String testMessage,Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
          try{
              JSONObject orderInfo = JSONObject.parseObject(testMessage);
              System.out.println("TopicReceiver消费者收到消息 : "+ testMessage);
              System.out.println("收到的personNO:"+orderInfo.getString("personNo"));
              //TODO:接收到消息进行业务操作
              channel.basicAck(tag,true);
          }catch (Exception e){
              // 异常情况 :根据需要去: 重发/ 丢弃
              // 重发一定次数后, 丢弃, 日志告警,b1:true表示重新入列,false表示不入列
              channel.basicNack(tag, false, false);
              // 系统 关键数据,永远是有人工干预
          }
      }

      /**
        * 监听的队列名称 TestDirectQueue
        * @param testMessage
        * @param channel
        * @param tag
        * @throws IOException
        */
      @RabbitListener(queues = "kms.topic.woman")
      public void processTopic2(String testMessage,Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
          try{
              JSONObject orderInfo = JSONObject.parseObject(testMessage);
              System.out.println("TopicWomenReceiver消费者收到消息 : "+ testMessage);
              System.out.println("收到的personNO:"+orderInfo.getString("personNo"));
              channel.basicAck(tag,true);
          }catch (Exception e){
              // 异常情况 :根据需要去: 重发/ 丢弃
              // 重发一定次数后, 丢弃, 日志告警,b1:true表示重新入列,false表示不入列
              channel.basicNack(tag, false, false);
              // 系统 关键数据,永远是有人工干预
          }
      }

      /**
        * 监听的队列名称 DelayQueue
        * @param testMessage
        * @param channel
        * @param tag
        * @throws IOException
        */
      @RabbitListener(queues = "kms.delay.queue")
      public void processDelay(UserInfoVO testMessage,Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
          try{
              System.out.println(LocalDateTime.now()+":DelayQueue消费者收到消息 : "+ JSON.toJSONString(testMessage));
              channel.basicAck(tag,true);
          }catch (Exception e){
              // 异常情况 :根据需要去: 重发/ 丢弃
              // 重发一定次数后, 丢弃, 日志告警,b1:true表示重新入列,false表示不入列
              channel.basicNack(tag, false, false);
              // 系统 关键数据,永远是有人工干预
          }
      }

    }

    步骤6:进行测试,新增controller,调用消息生产的方法,进行消息消费的查看。

    @RestController
    @RequestMapping("/remote/mq")
    public class TestMqController {
      @Autowired
      private MqProviderService mqProviderService;

      @PostMapping("sendDirectMessage")
      public String sendDirectMessage(){
          mqProviderService.sendDirectMessage();
          return "ok";
      }

      @PostMapping("sendTopicMessage")
      public String sendTopicMessage(){
          mqProviderService.sendTopicMessage();
          return "ok";
      }

      @PostMapping("sendTopicMessage2")
      public String sendTopicMessage2(){
          mqProviderService.sendTopicMessage2();
          return "ok";
      }

      @PostMapping("sendDelayMessage")
      public String sendDelayMessage(){
          mqProviderService.sendDelayMessage(10*1000L);
          return "ok";
      }
    }

    执行sendDelayMessage输出结果如下:

    2020-05-15T15:16:29.437:推送消息:{"personName":"张三","personNo":"5cd80fa8-42c0-4b70-932f-422ca4167a5a","photo":"111"}
    延迟队列生产者-发出消息:{"personName":"张三","personNo":"5cd80fa8-42c0-4b70-932f-422ca4167a5a","photo":"111"},TTL:10000
    进入回调。。。
    5cd80fa8-42c0-4b70-932f-422ca4167a5a:成功发送消息到MQ
    2020-05-15T15:16:39.933:DelayQueue消费者收到消息 : {"personName":"张三","personNo":"5cd80fa8-42c0-4b70-932f-422ca4167a5a","photo":"111"}

     

  • 相关阅读:
    当你不知道今天星期几,不妨在编辑器写下这段代码
    自定义注解!绝对是程序员装逼的利器!!
    什么是可串行化MVCC
    Jetpack新成员,一篇文章带你玩转Hilt和依赖注入
    连接真机开发安卓(Android)移动app MUI框架 添加购物车等——混合式开发(四)
    从前世今生聊一聊,大厂为啥亲睐时序数据库
    工作五年,面试官说我只会CRUD!竟然只给我10K!
    bootstrap知识总结
    数据处理的两个基本问题05 零基础入门学习汇编语言42
    转移指令的原理02 零基础入门学习汇编语言44
  • 原文地址:https://www.cnblogs.com/shuideqing/p/12896308.html
Copyright © 2020-2023  润新知