消息队列

消息队列概念

MQ全称为Message Queue,消息队列是一种应用程序对应用程序的通信方法。应用程序通过读写出入队列的消息(针对应用程序的数据)来通信,而无需专用连接来链接它们。

消息传递指的是程序之间通过在消息中发送数据进行通信,而不是通过直接调用彼此来通信,直接调用通常是用于诸如远程过程调用的技术。排队指的是应用程序通过队列来通信。队列的使用除去了接收和发送应用程序同时执行的要求。

服务与服务之间的通信方式与两种:同步调用和异步消息调用

  1. 同步调用:远程过程调用,REST 和 RPC

  2. 异步消息调用:消息队列

常见消息队列中间件

RabbitMQ、ActiveMQ、RocketMQ、Kafka

  1. RabbitMQ 稳定可靠,数据一致,支持多协议,有消息确认,基于erlang语言

  2. Kafka 高吞吐,高性能,快速持久化,无消息确认,无消息遗漏,可能会有有重复消息,依赖于zookeeper,成本高.

  3. ActiveMQ 不够灵活轻巧,对队列较多情况支持不好.

  4. RocketMQ 性能好,高吞吐,高可用性,支持大规模分布式,协议支持单一

消息队列作用

  1. 解耦

    场景:用户下单,订单系统需要通知库存系统

    1. 传统做法

      1. 例:订单系统调用库存系统的接口

      2. 传统模式缺点:假如库存系统无法访问,则订单减库存将失败,从而导致订单失败,订单系统与库存系统耦合

    2. 使用消息队列

      1. 订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功

      2. 库存系统:订阅下单的消息,采用拉/推的方式,获取下单信息,库存系统根据下单信息,进行库存操作

      3. 在下单时库存系统不能正常使用。也不影响正常下单,因为下单后,订单系统写入消息队列就不再关心其他的后续操作了。实现订单系统与库存系统的应用解耦

  2. 异步

    场景:用户注册后,需要发注册邮件和注册短信

    1. 传统做法

      1. a.串行:将注册信息写入数据库成功后,发送注册邮件,再发送注册短信

      2. b.并行:将注册信息写入数据库成功后,发送注册邮件的同时,发送注册短信

    2. 使用消息队列

      1. 将不是必须的业务逻辑,异步处理。

      2. 发送注册邮件和发送注册邮件异步读取被写入的消息队列,减少响应时间

  3. 流量削峰

    场景:商品秒杀业务,一般会因为流量过大,导致流量暴增,应用挂掉

    1. 传统做法

      限制用户数量

    2. 使用消息队列

      1. 用户的请求,服务器接收后,首先写入消息队列,秒杀业务根据消息队列中的请求信息,再做后续处理

      2. 假如消息队列长度超过最大数量,则直接抛弃用户请求或跳转到错误页面

  4. 消息通讯

    消息通讯:消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等

    1. 使用消息队列实现点对点通信

      客户端A和客户端B使用同一队列,进行消息通讯

    2. 使用消息队列实现聊天室通信

      客户端A,客户端B,客户端N订阅同一主题,进行消息发布和接收。实现类似聊天室效果

  5. 日志处理

    日志处理:指将消息队列用在日志处理中,比如Kafka的应用,解决大量日志传输的问题

    使用消息队列完成日志处理

    1. 日志采集客户端,负责日志数据采集,定时写受写入Kafka队列

    2. Kafka消息队列,负责日志数据的接收,存储和转发

    3. 日志处理应用:订阅并消费kafka队列中的日志数据

RabbitMQ介绍

  1. RabbitMQ是一个在AMQP基础上完成的,是一个可复用的企业消息系统。遵循Mozilla Public License开源协议。

  2. AMQP,即Advanced Message Queuing Protocol, 一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。Erlang中的实现有 RabbitMQ等。

  3. 特性:

    1. 保证可靠性:使用一些机制来保证可靠性,如持久化、传输确认、发布确认

    2. 灵活的路由功能

    3. 可伸缩性:支持消息集群,多台RabbitMQ服务器可以组成一个集群

    4. 高可用性:RabbitMQ集群中的某个节点出现问题时队列任然可用

    5. 支持多种协议

    6. 支持多语言客户端

    7. 提供良好的管理界面

    8. 提供跟踪机制:如果消息出现异常,可以通过跟踪机制分析异常原因

    9. 提供插件机制:可通过插件进行多方面扩展

RabbitMQ 安装及配置

使用 VM 基于 Centos 安装

  1. 安装前准备

    1. 安装 C++ 编译环境

      # yum -y install make gcc gcc-c++
      yum -y install make gcc gcc-c++ kernel-devel m4 ncurses-devel openssl-devel unixODBC unixODBC-devel httpd python-simplejson
    2. 下载 erlang 和 RabbitMQ

      # 下载erlang
      wget http://www.erlang.org/download/otp_src_20.1.tar.gz

      # 下载rabbitMQ
      wget https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.7.0/rabbitmq-server-generic-unix-3.7.0.tar.xz
  2. 安装 erlang

    1. 解压 erlang 安装包

      tar -xvf otp_src_20.1.tar.gz
    2. 进入解压文件夹

      cd otp_src_20.1
    3. 指定安装目录及安装配置(需要先安装并配置JDK)


      # erlang指定安装在/usr/local/erlang目录
           
      ./configure --prefix=/usr/local/erlang --enable-smp-support --enable-threads --enable-sctp --enable-kernel-poll --enable-hipe --with-ssl --without-javac
    4. 编译与安装

      make && make install
      1. 配置erlang环境变量

        vi /etc/profile

        将 export PATH=$PATH:/usr/local/erlang/bin 添加到文件末尾
      2. 重新加载profile文件

        source /etc/profile
  3. 安装 RabbitMQ

    1. 解压 RabbitMQ 安装包

      由于下载的安装包为xz文件,先将xz解压为tar\
      
      xz -d rabbitmq-server-generic-unix-3.7.0.tar.xz
      
      再解压缩tar文件
      
      tar -xvf rabbitmq-server-generic-unix-3.7.0.tar
    2. 启动 RabbitMQ

      1. 进入到解压的 RabbitMQ 的sbin 目录

        cd rabbitmq_server-3.7.0/sbin
      2. 启动

        ./rabbitmq-server -detached
      3. 查看进程

        lsof -i:5672
        
        ps aux|grep rabbit
        #ps a 显示现行终端机下的所有程序,包括其他用户的程序。
        #ps u   以用户为主的格式来显示程序状况。
        #ps x   显示所有程序,不以终端机来区分。
  4. 启动管理界面

    1. 启动 RabbitMQ 的管理系统插件(需进入 sbin 目录)

      ./rabbitmq-plugins enable rabbitmq_management
    2. 访问管理系统

  5. 放行端口

    如果没有网络指令需要先安装:yum install net-tools

    1. 查看并放行端口

      netstat -tlnp
      firewall-cmd --add-port=15672/tcp --permanent
      firewall-cmd --add-port=5672/tcp --permanent
    2. 也可以直接关闭防火墙

      CentOS7

      #关闭防火墙 
      systemctl stop firewalld
      #开机禁用 
      systemctl disable firewalld
      #查看状态
      systemctl status firewalld
    3. 云服务器需要在控制台添加“安全组设置”

RabbitMQ 管理

用户管理

  1. 用户级别

    1. 超级管理员administrator,可以登录控制台,查看所有信息,可以对用户和策略进行操作

    2. 监控者monitoring,可以登录控制台,可以查看节点的相关信息,比如进程数,内存磁盘使用情况

    3. 策略制定者policymaker ,可以登录控制台,制定策略,但是无法查看节点信息

    4. 普通管理员 management 仅能登录控制台

    5. 其他, 无法登录控制台,一般指的是提供者和消费者

  2. 添加用户

    1. 命令模式

      1. 添加/配置用户

        # 插件目录
        
        ./rabbitmqctl add_user ytao admin123
      2. 设置用户权限

        #设置admin为administrator级别
        
        ./rabbitmqctl set_user_tags ytao administrator
    2. web方式

      1. 浏览器访问 RabbitMQ web界面 http:// IP地址:15672/(使用guest guest 登录, guest 具有最高权限)

      2. 添加用户

        Admin --> Add a user 填写用户信息 --> Add user 提交

      3. 为用户分配可以访问的虚拟主机

        1. 默认情况下没有任何可以访问的,我们可以添加一个主机(相当于添加一个数据库),然后分配权限

        2. 创建虚拟主机

          点击菜单(Virtual Hosts) --> Add a new Virtual host(添加新的虚拟主机) --> Name(主机名) --> 点击创建(Add virtual host)

        3. 给指定用户分配虚拟主机

          点击Users --> 点击用户名 --> 选择虚拟主机 --> 点击设置权限(Set permission)

        4. 设置完成后,回到用户界面确认

消息队列的模式

  1. 简单模式

    简单模式就是我们的生产者将消息发到队列,消费者从队列中取消息,一条消息对应一个消费者

  2. 工作模式

    Work模式就是一条消息可以被多个消费者尝试接收,但是最终只能有一个消费者能获取

  3. 订阅模式 一条消息可以被多个消费者同时获取,生产者将消息发送到交换机,消费者将自己对应的队列注册到交换机,当发送消息后所有注册的队列的消费者都可以收到消息

  4. 路由模式

    生产者将消息发送到了type为direct模式的交换机,消费者的队列在将自己绑定到路由的时候会给自己绑定一个key,只有消费者发送对应key格式的消息时候队列才会收到消息

  5. Topic 模式

  6. RPC 模式

Maven应用使用RabbitMQ

  1. 创建 Maven 项目

  2. 添加依赖

    <dependency>
           <groupId>com.rabbitmq</groupId>
           <artifactId>amqp-client</artifactId>
           <version>4.5.0</version>
       </dependency>
       <dependency>
           <groupId>org.slf4j</groupId>
           <artifactId>slf4j-log4j12</artifactId>
           <version>1.7.25</version>
    </dependency>
  3. 创建日志配置文件 log4j.properties

    log4j.rootLogger=DEBUG,A1 log4j.logger.com.taotao = DEBUG 
    log4j.logger.org.mybatis = DEBUG
    log4j.appender.A1=org.apache.log4j.ConsoleAppender
    log4j.appender.A1.layout=org.apache.log4j.PatternLayout
    log4j.appender.A1.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss,SSS} [%t] [%c]-[%p] %m%n
  4. 创建帮助类

    package com.qfedu.utils;
    
    import com.rabbitmq.client.Connection;
    import com.rabbitmq.client.ConnectionFactory;
    
    public class ConnectionUtil {
         public static Connection getConnection() throws Exception {
              //定义连接工厂
              ConnectionFactory factory = new ConnectionFactory();
              // 设置服务地址
              factory.setHost("服务器地址");
              //端口
              factory.setPort(5672);
              // 设置账号信息,用户名、密码、vhost
              factory.setVirtualHost("账号信息");
              factory.setUsername("用户名");
              factory.setPassword("密码");
              // 通过工程获取连接
              Connection connection = factory.newConnection();
              return connection;
         }
    }
  5. 发送消息

    import com.qfedu.utils.ConnectionUtil;
    import com.rabbitmq.client.Channel;
    import com.rabbitmq.client.Connection;
    
    public class Send {
    
        public static void main(String[] argv) throws Exception {
            // 获取到连接以及mq通道
            Connection connection = ConnectionUtil.getConnection();
            // 相当于数据库中的创建连接
            // 从连接中创建通道
            Channel channel = connection.createChannel();
            // 相当于数据库中的 statement
            // 声明(创建)队列,如果存在就不创建,不存在就创建
            // 参数1 队列名,
            // 参数2 durable: 是否持久化, 队列的声明默认是存放到内存中的,如果rabbitmq重启会丢失,如果想重启之后还存在就要使队列持久化,保存到Erlang自带的Mnesia数据库中,当rabbitmq重启之后会读取该数据库
            // 参数3 exclusive:是否排外的,有两个作用,一:当连接关闭时connection.close()该队列是否会自动删除; 二:该队列是否是私有的private,如果不是排外的,可以使用两个消费者都访问同一个队列,没有任何问题,如果是排外 的,会对当前队列加锁,其他通道channel是不能访问的,如果强制访问会报异常: com.rabbitmq.client.ShutdownSignalException: channel error; protocol method: #method<channel.close>(reply-code=405, reply-text=RESOURCE_LOCKED - cannot obtain exclusive access to locked queue 'queue_name' in vhost '/', class-id=50, method-id=20)一般等于true的话 用于一个队列只能有一个消费者来消费的场景
            // 参数4 autoDelete:是否自动删除,当最后一个消费者断开连接之后队列是否自动被删除,可以通过RabbitMQ Management,查看某个队列的消费者数量,当consumers = 0时队列就会自动删除
            // 参数5 arguments:  参数
            //channel.queueDeclare("queue1", false, false, true, null);
            // 消息内容
             String message = "Hello World!";
            // 参数1 交换机,此处无
            // 参数2 发送到哪个队列
            // 参数3 属性
            // 参数4 内容
             channel.basicPublish("", "queue1", null, message.getBytes());
            // 将消息发动到数据库
             System.out.println(" 发送数据 '" + message + "'");
            //关闭通道和连接
             channel.close();
             connection.close();
        }
    }
  6. 消费消息

    import com.qfedu.utils.ConnectionUtil;
    import com.rabbitmq.client.*;
    
    import java.io.IOException;
    
    public class Send {
    
        public static void main(String[] argv) throws Exception {
            // 获取到连接以及mq通道
            Connection connection = ConnectionUtil.getConnection();
            // 相当于数据库中的创建连接
            // 从连接中创建通道
            Channel channel = connection.createChannel();
            // 相当于数据库中的 statement
            // 声明(创建)队列,如果存在就不创建,不存在就创建
            // 参数1 队列名,
            // 参数2 durable: 是否持久化, 队列的声明默认是存放到内存中的,如果rabbitmq重启会丢失,如果想重启之后还存在就要使队列持久化,保存到Erlang自带的Mnesia数据库中,当rabbitmq重启之后会读取该数据库
            // 参数3 exclusive:是否排外的,有两个作用,一:当连接关闭时connection.close()该队列是否会自动删除; 二:该队列是否是私有的private,如果不是排外的,可以使用两个消费者都访问同一个队列,没有任何问题,如果是排外 的,会对当前队列加锁,其他通道channel是不能访问的,如果强制访问会报异常: com.rabbitmq.client.ShutdownSignalException: channel error; protocol method: #method<channel.close>(reply-code=405, reply-text=RESOURCE_LOCKED - cannot obtain exclusive access to locked queue 'queue_name' in vhost '/', class-id=50, method-id=20)一般等于true的话 用于一个队列只能有一个消费者来消费的场景
            // 参数4 autoDelete:是否自动删除,当最后一个消费者断开连接之后队列是否自动被删除,可以通过RabbitMQ Management,查看某个队列的消费者数量,当consumers = 0时队列就会自动删除
            // 参数5 arguments:  参数
            //channel.queueDeclare("queue1", false, false, true, null);
            // 消息内容
             String message = "Hello World!";
            // 参数1 交换机,此处无
            // 参数2 发送到哪个队列
            // 参数3 属性
            // 参数4 内容
             channel.basicPublish("", "queue1", null, message.getBytes());
            // 将消息发动到数据库
             System.out.println(" 发送数据 '" + message + "'");
            //关闭通道和连接
             channel.close();
             connection.close();
        }
    }

Spring Boot 整合 RabbitMQ

  1. 消息发送者

    1. 创建 SpringBoot 项目,导入以下依赖:

      1. Spring Web

      2. Thymeleaf

      3. Spring for RabbitMQ

    2. 在项目中新增RabbitMQ支持导入以下依赖

      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-amqp</artifactId>
      </dependency>
      
      <dependency>
          <groupId>org.springframework.amqp</groupId>
          <artifactId>spring-rabbit-test</artifactId>
          <scope>test</scope>
      </dependency>
    3. 在application.yml配置RabbitMQ服务器连接属性

      spring:
        application:
          name: mq-sender-demo
        rabbitmq:
          host: 服务器地址
          port: 5672
          username: 用户名
          password: 密码
          virtual-host: 用户信息
      
      server:
        port: 9001
    4. 配置RabbitMQ创建队列(Quence)

      @Configuration
      public class RabbitMQConfig {
      
          @Bean
          public Queue queue9() {
              return new Queue("queue9");
          }
          @Bean
          public Queue queue10() {
              return new Queue("queue10");
          }
          /**
           * 声明交换机,fanout 类型
           */
          @Bean
          public FanoutExchange fanoutExchange() {
              FanoutExchange fanoutExchange = new FanoutExchange("fanoutExchange");
              return fanoutExchange;
          }
          /**
           * 将队列和交换机绑定
           */
          @Bean
          public Binding bindingFanoutExchange1(Queue queue9, FanoutExchange fanoutExchange) {
              return BindingBuilder.bind(queue9).to(fanoutExchange);
          }
          @Bean
          public Binding bindingFanoutExchange2(Queue queue10, FanoutExchange fanoutExchange) {
              return BindingBuilder.bind(queue10).to(fanoutExchange);
          }
      
      
          @Bean
          public Queue queue11() {
              return new Queue("queue11");
          }
          @Bean
          public Queue queue12() {
              return new Queue("queue12");
          }
          /**
           * 声明交换机,direct 类型
           */
          @Bean
          public DirectExchange directExchange() {
              DirectExchange directExchange = new DirectExchange("directExchange");
              return directExchange;
          }
          /**
           * 将队列和交换机绑定
           */
          @Bean
          public Binding bindingDirectExchange(Queue queue11, DirectExchange directExchange) {
              return BindingBuilder.bind(queue11).to(directExchange).with("rk1");
          }
      
          @Bean
          public Binding bindingDirectExchange2(Queue queue12, DirectExchange directExchange) {
              return BindingBuilder.bind(queue12).to(directExchange).with("rk2");
          }
      }
    5. 在消息发送者中注入AmqpTemplate对象即可发送消息

      @Service
      public class SendMsgService {
      
          @Autowired
          private AmqpTemplate amqpTemplate;
      
          public void sendMsg(String message){
              amqpTemplate.convertAndSend("wfx-simple",message);
      
              //amqpTemplate.convertAndSend("fanout", "", message);
      
              //amqpTemplate.convertAndSend("direct", "rk1", message);
          }
      }
  2. 消息接收者

    监听队列

    @Component
    @RabbitListener(queues = "wfx-direct-quence1")
    public class ReceiveDirectHanlder1 {
    
        @RabbitHandler//标记当前方法是用来处理消息的
        public void recevieMsg(String message) {
            System.out.println("收到wfx-direct-quence1消息: =>"+message);
        }
    }

使用 RabbitMQ 发送-接收对象

将对象转换成字符串进行传递

  1. 对象序列号实现

    1. bean 实现序列号接口

    2. 生产者消费者的 bean 包名必须一致

    3. 消息生产者

      @Service
      public class SendMsgService {
      
          @Autowired
          private AmqpTemplate amqpTemplate;
      
          public void sendMsg(Goods goods) {
              byte[] bytes = SerializationUtils.serialize(goods);
              amqpTemplate.convertAndSend("queue1",bytes);
          }
      }
    4. 消息消费者

      @Service
      @RabbitListener(queues = "queue1")
      public class ReviceMsgService {
      
          @RabbitHandler
          public void receiveMsg(byte[] bs) {
              Goods goods = (Goods) SerializationUtils.deserialize(bs);
              System.out.println(goods);
          }
      
      }
  2. JSON 字符串实现

    1. 消费生产者

      @Service
      public class SendMsgService {
      
          @Autowired
          private AmqpTemplate amqpTemplate;
      
              public void sendMsg(Goods goods) throws JsonProcessingException {
      
                  ObjectMapper mapper=new ObjectMapper();
                  String message=mapper.writeValueAsString(goods);
                  amqpTemplate.convertAndSend("queue1",message);
      
              }
      }
    2. 消息消费者

      @Service
      @RabbitListener(queues = "queue1")
      public class ReviceMsgService {
      
          @RabbitHandler
          public void receiveMsg(String msg) throws JsonProcessingException {
              ObjectMapper mapper=new ObjectMapper();
              Goods goods=mapper.readValue(msg,Goods.class);
              System.out.println(goods);
          }
      
      }

基于Java的交换机队列管理

  1. Java 代码管理

    交换机、队列创建及绑定

    //1. 声明队列-HelloWorld
    //参数1:queue - 指定队列的名称
    //参数2:durable - 当前队列是否需要持久化(true)
    //参数3:exclusive - 是否排外(conn.close() - 当前队列会被自动删除,当前队列只能被一个消费者消费)
    //参数4:autoDelete - 如果这个队列没有消费者在消费,队列自动删除
    //参数5:arguments - 指定当前队列的其他信息
    channel.queueDeclare("HelloWorld",true,false,false,null);
    
    //2. 创建exchange - 绑定某一个队列
    //参数1: exchange的名称
    //参数2: 指定exchange的类型  FANOUT - pubsub ,   DIRECT - Routing , TOPIC - Topics
    channel.exchangeDeclare("pubsub-exchange", BuiltinExchangeType.FANOUT);
    channel.queueBind("pubsub-queue1","pubsub-exchange","");
    channel.queueBind("pubsub-queue2","pubsub-exchange","");
  2. SpringBoot Java 配置管理

    配置RabbitMQ创建队列(Quence)

    @Configuration
    public class RabbitMQConfiguration {
    
        @Bean
        public Queue queue() {
            return new Queue("wfx-quence");
        }
    
        @Bean
        public Queue fanoutQuence() {
            return new Queue("wfx-fanout-quence");
        }
        /**
         * 声明交换机,fanout 类型
         */
        @Bean
        public FanoutExchange fanoutExchange() {
            FanoutExchange fanoutExchange = new FanoutExchange("fanoutExchange");
            return fanoutExchange;
        }
        /**
         * 将队列和交换机绑定
         */
        @Bean
        public Binding bindingFanoutExchange(Queue fanoutQuence, FanoutExchange fanoutExchange) {
            return BindingBuilder.bind(fanoutQuence).to(fanoutExchange);
        }
    
    
        @Bean
        public Queue directQuence1() {
            return new Queue("wfx-direct-quence1");
        }
        @Bean
        public Queue directQuence2() {
            return new Queue("wfx-direct-quence2");
        }
        /**
         * 声明交换机,direct 类型
         */
        @Bean
        public DirectExchange directExchange() {
            DirectExchange directExchange = new DirectExchange("directExchange");
            return directExchange;
        }
        /**
         * 将队列和交换机绑定
         */
        @Bean
        public Binding bindingDirectExchange(Queue directQuence1, DirectExchange directExchange) {
           return BindingBuilder.bind(directQuence1).to(directExchange).with("rk1");
        }
    
        @Bean
        public Binding bindingDirectExchange2(Queue directQuence2, DirectExchange directExchange) {
            return BindingBuilder.bind(directQuence2).to(directExchange).with("rk2");
        }
    
    }

消息可靠性

  1. RabbitMQ 事务

    //1.开启事务
    channel.txSelect();
    
    //2.提交事务
    channel.txCommit();
    
    //3.事务回滚
    channel.txRollback();
  2. 消息确认机制

    消息确认机制和 return 机制

    1. Maven 项目的消息确认

      1. 普通 confirm 方式

        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        String message = "Hello World!";
        
        //1.开启消息确认
        channel.confirmSelect();
        //2.发送消息
        channel.basicPublish("ex2", "c", null, message.getBytes());
        //3.获取确认
        boolean b = channel.waitForConfirms();
        System.out.println("消息发送"+(b?"成功":"失败"));
        
        channel.close();
        connection.close();
      2. 批量 confirm 方式

        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        String message = "Hello World!";
        
        //1.开启消息确认
        channel.confirmSelect();
        //2.发送消息
        for (int i=1; i<=10; i++) {
            message += i;
            channel.basicPublish("ex2", "c", null, message.getBytes());
        }
        //3.批量确认:发送的所有消息中有一个失败就直接全部失败,抛出IO异常
        boolean b = channel.waitForConfirms();
        
        channel.close();
        connection.close();
      3. 异步 confirm 方式

        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        String message = "Hello World!";
        
        //1.开启消息确认
        channel.confirmSelect();
        //2.发送消息
        for (int i=1; i<=10; i++) {
            message += i;
            channel.basicPublish("ex2", "c", null, message.getBytes());
        }
        
        //3.开启异步confirm
        channel.addConfirmListener(new ConfirmListener() {
            //参数l表示返回的消息标识,参数b表示是否为批量confirm
            public void handleAck(long l, boolean b) throws IOException {
                System.out.println("----消息发送成功");
            }
            public void handleNack(long l, boolean b) throws IOException {
                System.out.println("----消息发送失败");
            }
        });
        
        channel.close();
        connection.close();
      4. return 机制

        • 发送消息之前开启return机制

        • 发送消息时指定mandatory参数为true

        • 由于return机制是异步处理,所以在发送消息之后不关闭channel

        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        String message = "Hello World!";
        
        channel.addReturnListener(new ReturnListener() {
            public void handleReturn(int i, String s, String s1, String s2, AMQP.BasicProperties basicProperties,
                                     byte[] bytes) throws IOException {
                System.out.println("消息未分发到队列中");
            }
        });
        //channel.basicPublish("", "queue1", null, message.getBytes());
        channel.basicPublish("ex2", "c", true,null, message.getBytes());
    2. springboot 应用消息确认

      1. 配置 application.yml

        spring:
          rabbitmq:
            publisher-confirm-type: simple
            publisher-returns: true
        
      2. 开启 confirm 和 return 监听

        import org.slf4j.Logger;
        import org.slf4j.LoggerFactory;
        import org.springframework.amqp.core.Message;
        import org.springframework.amqp.rabbit.connection.CorrelationData;
        import org.springframework.amqp.rabbit.core.RabbitTemplate;
        import org.springframework.stereotype.Component;
        
        import javax.annotation.PostConstruct;
        import javax.annotation.Resource;
        
        @Component
        public class PublisherConfireAndReturnConfig implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
        
            Logger logger = LoggerFactory.getLogger(PublisherConfireAndReturnConfig.class);
        
            @Resource
            private RabbitTemplate rabbitTemplate;
        
            @PostConstruct
            public void initMethod(){
                rabbitTemplate.setConfirmCallback(this);
                rabbitTemplate.setReturnCallback(this);
            }
        
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String s) {
                if(ack){
                    logger.info("--------消息发送(到交换机)成功");
                }else{
                    logger.warn("--------消息发送(到交换机)失败");
                }
            }
        
            @Override
            public void returnedMessage(Message message, int i, String s, String s1, String s2) {
                logger.info("~~~~~~~~消息发送到交换机但未分发到队列!!!");
            }
        }
  3. 避免消息重复消费

    重复消费消息,会对非幂等行操作造成问题 重复消费消息的原因是,消费者没有给RabbitMQ一个ack
    
    为了解决消息重复消费的问题,可以采用Redis,在消费者消费消息之前,现将消息的id放到Redis中,
    
        id-0(正在执行业务)
    
        id-1(执行业务成功)
    
    如果ack失败,在RabbitMQ将消息交给其他的消费者时,先执行setnx,如果key已经存在,获取他的值,如果是0,当前消费者就什么都不做,如果是1,直接ack。
    
    极端情况:第一个消费者在执行业务时,出现了死锁,在setnx的基础上,再给key设置一个生存时间。
    1. Maven 项目避免重复消费

      1. 生产者,发送消息时,指定 messageld

        AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
            .deliveryMode(1)     //指定消息书否需要持久化 1 - 需要持久化  2 - 不需要持久化
            .messageId(UUID.randomUUID().toString())
            .build();
        String msg = "Hello-World!";
        channel.basicPublish("","HelloWorld",true,properties,msg.getBytes());
      2. 消费者,在消费消息时,根据具体业务逻辑去操作 redis

        DefaultConsumer consume = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                Jedis jedis = new Jedis("192.168.199.109",6379);
                String messageId = properties.getMessageId();
                //1. setnx到Redis中,默认指定value-0
                String result = jedis.set(messageId, "0", "NX", "EX", 10);
                if(result != null && result.equalsIgnoreCase("OK")) {
                    System.out.println("接收到消息:" + new String(body, "UTF-8"));
                    //2. 消费成功,set messageId 1
                    jedis.set(messageId,"1");
                    channel.basicAck(envelope.getDeliveryTag(),false);
                }else {
                    //3. 如果1中的setnx失败,获取key对应的value,如果是0,return,如果是1
                    String s = jedis.get(messageId);
                    if("1".equalsIgnoreCase(s)){
                        channel.basicAck(envelope.getDeliveryTag(),false);
                    }
                }
            }
        };
    2. springboot 应用避免重复消费

      1. 导入依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
      2. 编写配置文件

        spring:
          redis:
            host: 47.96.11.185
            port: 6379
      3. 修改生产者

        @Test
        void contextLoads() throws IOException {
            CorrelationData messageId = new CorrelationData(UUID.randomUUID().toString());
            rabbitTemplate.convertAndSend("boot-topic-exchange","slow.red.dog","红色大狼狗!!",messageId);
            System.in.read();
        }
      4. 修改消费者

        @Autowired
        private StringRedisTemplate redisTemplate;


        @RabbitListener(queues = "boot-queue")
        public void getMessage(String msg, Channel channel, Message message) throws IOException {
          //0. 获取MessageId
          String messageId = message.getMessageProperties().getHeader("spring_returned_message_correlation");
          //1. 设置key到Redis
          if(redisTemplate.opsForValue().setIfAbsent(messageId,"0",10, TimeUnit.SECONDS)) {
              //2. 消费消息
              System.out.println("接收到消息:" + msg);

              //3. 设置key的value为1
              redisTemplate.opsForValue().set(messageId,"1",10,TimeUnit.SECONDS);
              //4. 手动ack
              channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
          }else {
              //5. 获取Redis中的value即可 如果是1,手动ack
              if("1".equalsIgnoreCase(redisTemplate.opsForValue().get(messageId))){
                  channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
              }
          }
        }

消息延迟实现

RabbitMQ 延迟机制(TTL) -- SpringBoot

  1. 延迟队列概念

    1. 什么是延迟队列

      延迟队列存储的对象肯定是对应的延时消息,所谓”延时消息”是指当消息被发送以后,并不想让消费者立即拿到消息,而是等待指定时间后,消费者才拿到这个消息进行消费。

    2. RabbitMQ 如何实现延迟队列?

      AMQP协议和RabbitMQ队列本身没有直接支持延迟队列功能,但是可以通过TTL(Time To Live)特性模拟出延迟队列的功能。

    3. 消息的TTL

      消息的TTL就是消息的存活时间。RabbitMQ可以对队列和消息分别设置TTL。对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做单独的设置。超过了这个时间,我们认为这个消息就死了,称之为死信。可以通过设置消息的expiration字段或者x-message-ttl属性来设置时间.

    4. 实现延迟队列

      延迟任务通过消息的TTL来实现。我们需要建立2个队列,一个用于发送消息,一个用于消息过期后的转发目标队列。

      生产者输出消息到Queue1,并且这个消息是设置有有效时间的,比如60s。消息会在Queue1中等待60s,如果没有消费者收掉的话,它就是被转发到Queue2,Queue2有消费者,收到处理延迟任务。

  2. 创建延迟交换机

    1. 创建路由交换机

    2. 创建死信队列

    3. 创建死信转发队列

    4. 交换机队列绑定

  3. SpringBoot 实现延迟队列

    1. 添加 MQ 依赖

      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-amqp</artifactId>
      </dependency>
      <dependency>
        <groupId>org.springframework.amqp</groupId>
        <artifactId>spring-rabbit-test</artifactId>
        <scope>test</scope>
      </dependency>
    2. 在 application.yaml 配置 RabbitMQ 服务器连接属性

      spring:
      application:
        name: mq-sender-demo
      rabbitmq:
        host: 47.96.11.185
        port: 5672
        username: ytao
        password: admin123
        virtual-host: wfx_host
        # 手动ACK 不开启自动ACK模式,目的是防止报错后未正确处理消息丢失 默认 为 none
        listener:
          simple:
            acknowledge-mode: manual
    3. 创建生产者

      @Component
      @Slf4j
      public class RabbitProduct{

        @Autowired
        private RabbitTemplate rabbitTemplate;
         
        public void sendDelayMessage(List<Integer> list) {
        //这里的消息可以是任意对象,无需额外配置,直接传即可
              log.info("===============延时队列生产消息====================");
              log.info("发送时间:{},发送内容:{}", LocalDateTime.now(), list.toString());
              this.rabbitTemplate.convertAndSend(
                      "delay_exchange",
                      "delay_key",
                      list,
                      message -> {
                      //注意这里时间要是字符串形式
                      message.getMessageProperties().setExpiration("60000");
                          return message;
                      }
              );
          log.info("{}ms后执行", 60000);
        }
      }
    4. 创建消费者

      @Component
      @Slf4j
      public class RabbitConsumer {
        @Autowired
        private CcqCustomerCfgService ccqCustomerCfgService;

        /**
          * 默认情况下,如果没有配置手动ACK, 那么Spring Data AMQP 会在消息消费完毕后自动帮我们去ACK
          * 存在问题:如果报错了,消息不会丢失,但是会无限循环消费,一直报错,如果开启了错误日志很容易就吧磁盘空间耗完
          * 解决方案:手动ACK,或者try-catch 然后在 catch 里面将错误的消息转移到其它的系列中去
          * spring.rabbitmq.listener.simple.acknowledge-mode = manual
          * @param list 监听的内容
          */
        @RabbitListener(queues = "receive_queue")
        public void cfgUserReceiveDealy(List<Integer> list, Message message, Channel channel) throws IOException {
            log.info("===============接收队列接收消息====================");
            log.info("接收时间:{},接受内容:{}", LocalDateTime.now(), list.toString());
            //通知 MQ 消息已被接收,可以ACK(从队列中删除)了
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
            try {
                dosomething.....
            } catch (Exception e) {
                log.error("============消费失败,尝试消息补发再次消费!==============");
                log.error(e.getMessage());
                /**
                  * basicRecover方法是进行补发操作,
                  * 其中的参数如果为true是把消息退回到queue但是有可能被其它的consumer(集群)接收到,
                  * 设置为false是只补发给当前的consumer
                  */
                channel.basicRecover(false);
            }
        }
      }

 

 

 

 

 



posted on 2021-01-27 09:05  白糖℃  阅读(92)  评论(0编辑  收藏  举报