一文学会rabbitMQ

321 阅读12分钟

这是我参与更文挑战的第7天,活动详情查看: 更文挑战 RabbitMQ是一个开源的消息代理和队列服务器,用来通过普通协议在完全不同的应用之间共享数据,RabbitMQ是使用erlang语言来编写的,并且RabbitMQ是基于AMQP协议。

RabbitMQ高性能的原因

使用Erlang语言

erlang语言最初在于交换机领域的架构模型,这样使得RabbitMQ在Broker之间进行数据交互的性能是非常优秀的

Erlang语言的优点:Erlang有着和原生Socket一样的延迟

AMQP

全称:Advanced Message Queuing Protocol (高级消息队列协议)

AMQP的定义

具有现代特征的二进制协议。是一个提供统一消息服务的应用层标准的消息队列协议,是应用层协议的一个开放标准,问面向消息的中间件设计。

AMQP协议模型

image-20200905155230335.png

AMQP核心概念

Server/BRoker:接受客户端的连接,实现AMQP实体服务

Commection:连接,应用与Broker的网络连接

Channel:网络信道,几乎所有的操作都在Channel中进行,Channel是一个进行消息读写的通道。客户端可以建立多个Channel,每一个Channel代表一个会话任务。

Message:消息,服务器和应用程序之间传送的数据,由Properties和Body组成。Properties可以对消息进行修饰,例如消息的优先级、延迟等高级特征;而Body就是消息体的内容

Virtual host:虚拟地址,用于进行逻辑隔离,最上层的消息路由。一个Virtual host里面可以有若干个Exchange和Queue,同一个Virtual host里面不能有相同名称的Exchange或Queue.

Exchange:交换机,接收消息,根据路由键转发消息到绑定的队列。

binding:Exchange和Queue之间的虚拟链接,binding可以包含routing key

Routing key:一个路由规则,虚拟机可以用它来确定如何路由一个特定的消息

Queue:Message Queue 消息队列,保存消息并将他们交给消费者消费。

RabbitMQ 架构

RabbitMQ Center.jpg

image-20200905162131622.png

RabbitMQ安装

使用RabbitMQ 3.6.5 版本进行操作:

  • 环境搭建:

  • 官网地址:www.rabbitmq.com/

  • 环境描述:Linux(centos7 Redhat7)

## 1. 首先在Linux上进行一些软件的准备工作,yum下来一些基础的软件包
um install build-essential openssl openssl-devel unixODBC unixODBC-devel make gcc gcc-c++ kernel-devel m4 ncurses-devel tk tc xz

# 配置好主机名称:/etc/hosts /etc/hostname

# 2. 下载RabbitMQ所需软件包(本神在这里使用的是 RabbitMQ3.6.5 稳定版本)
get www.rabbitmq.com/releases/erlang/erlang-18.3-1.el7.centos.x86_64.rpm
get http://repo.iotti.biz/CentOS/7/x86_64/socat-1.7.3.2-1.1.el7.lux.x86_64.rpm
get www.rabbitmq.com/releases/rabbitmq-server/v3.6.5/rabbitmq-server-3.6.5-1.noarch.rpm

# 3. 安装服务命令
pm -ivh erlang-18.3-1.el7.centos.x86_64.rpm 
pm -ivh socat-1.7.3.2-1.1.el7.x86_64.rpm
pm -ivh rabbitmq-server-3.6.5-1.noarch.rpm

# 4. 修改用户登录与连接心跳检测,注意修改
im /usr/lib/rabbitmq/lib/rabbitmq_server-3.6.5/ebin/rabbit.app
改点1:loopback_users 中的 <<"guest">>,只保留guest (用于用户登录)
改点2:heartbeat 为10(用于心跳连接)

# 5. 安装管理插件

# 5.1 首先启动服务(后面 | 包含了停止、查看状态以及重启的命令)
etc/init.d/rabbitmq-server start | stop | status | restart

# 5.2 查看服务有没有启动: lsof -i:5672 (5672是Rabbit的默认端口)
abbitmq-plugins enable rabbitmq_management

# 5.3 可查看管理端口有没有启动: 
sof -i:15672 或者 netstat -tnlp | grep 15672

# 6. 一切OK 我们访问地址,输入用户名密码均为 guest :
# http://你的ip地址:15672/

# 7. 如果一切顺利,那么到此为止,我们的环境已经安装完啦

RabbitMQ 核心

Exchange

Direct Exchange

Direct Exchange - 处理路由键。需要将一个队列绑定到交换机上,要求该消息与一个特定的路由键完全匹配。这是一个完整的匹配。如果一个队列绑定到该交换机上要求路由键 “dog”,则只有被标记为“dog” 的消息才被转发,不会转发dog.puppy,也不会转发dog.guard,只会转发dog。

任何发送到Direct Exchange的消息都会被转发到RouteKey中指定的Queue。

1.一般情况可以使用rabbitMQ自带的Exchange:(该Exchange的名字为空字符串,下文称其为default Exchange)。

2.这种模式下不需要将Exchange进行任何绑定(binding)操作

3.消息传递时需要一个“RouteKey”,可以简单的理解为要发送到的队列名字。

4.如果vhost中不存在RouteKey中指定的队列名,则该消息会被抛弃。

案例

生产者

public class DirectExchangeSender {
    public static void main(String[] args) throws  Exception{
        //  1 创建ConnectionFactory(链接工厂)
        ConnectionFactory connectionFactory=new ConnectionFactory();
        connectionFactory.setHost("192.168.123.171");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        //2.生成连接
        Connection connection=connectionFactory.newConnection();
        //3.创建channel
        Channel channel=connection.createChannel();
        //4 声明
        String exchangeName = "test_direct_exchange";
        String routingKey = "test_direct_routingKey";
        //  参数: queue名字,是否持久化,独占的queue(仅供此连接),不使用时是否自动删除, 其他参数
        channel.queueDeclare(routingKey,false,false,false,null);
        String msg="Hello World RabbitMQ 4  Direct Exchange Message ... ";
        channel.basicPublish(exchangeName, routingKey , null , msg.getBytes());
    }
}

消费者

public class DirectExchangeReceiver {
    public static void main(String[] args)  throws Exception  {
        //  1 创建ConnectionFactory(链接工厂)
        ConnectionFactory connectionFactory=new ConnectionFactory();
        connectionFactory.setHost("192.168.123.171");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setAutomaticRecoveryEnabled(true);
        connectionFactory.setNetworkRecoveryInterval(3000);
        Connection connection=connectionFactory.newConnection();

        Channel channel = connection.createChannel();
         //4 声明
        String exchangeName = "test_direct_exchange";
        String exchangeType = "direct";
        String queueName = "test_direct_queue";
        String routingKey = "test_direct_routingKey";

         //  参数:队列名称、是否自动ACK、Consumer
        channel.exchangeDeclare(exchangeName, exchangeType, true, false, false, null);
        channel.queueDeclare(queueName, false, false, false, null);
        channel.queueBind(queueName, exchangeName, routingKey);

           //durable 是否持久化消息
        QueueingConsumer consumer = new QueueingConsumer(channel);
          //参数:队列名称、是否自动ACK、Consumer
        channel.basicConsume(queueName, true, consumer);
        while (true){
            QueueingConsumer.Delivery delivery= consumer.nextDelivery();
            String msg =new String(delivery.getBody());
            System.out.println(msg);
        }
    }
}

Topic Exchange

Topic Exchange – 将路由键和某模式进行匹配。此时队列需要绑定要一个模式上。符号“#”匹配一个或多个词,符号“**”匹配不多不少一个词。因此“dog.#”能够匹配到“dog.jacquesh.boy”,但是“dog.*” 只会匹配到“dog.jacquesh”

任何发送到Topic Exchange的消息都会被转发到所有关心RouteKey中指定话题的Queue上

  1. 这种模式较为复杂,简单来说,就是每个队列都有其关心的主题,所有的消息都带有一个“标题”(RouteKey),Exchange会将消息转发到所有关注主题能与RouteKey模糊匹配的队列。

  2. 这种模式需要RouteKey,也许要提前绑定Exchange与Queue。

  3. 在进行绑定时,要提供一个该队列关心的主题,如“#.log.#”表示该队列关心所有涉及log的消息(一个RouteKey为”MQ.log.error”的消息会被转发到该队列)。

  4. “#”表示0个或若干个关键字,“**”表示一个关键字。如“log.*”能与“log.warn”匹配,无法与“log.warn.timeout”匹配;但是“log.#”能与上述两者匹配。

  5. 同样,如果Exchange没有发现能够与RouteKey匹配的Queue,则会抛弃此消息。

案例

生产者

public class TopicExchangeSender {
    public static void main(String[] args) throws  Exception {
        ConnectionFactory connectionFactory=new ConnectionFactory();
        connectionFactory.setHost("192.168.123.171");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");

        Connection connection=connectionFactory.newConnection();
        Channel channel = connection.createChannel();
        String exchangeName = "test_topic_exchange";
        String routingKey1 = "user.save";
        String routingKey2 = "user.update";
        String routingKey3 = "user.delete.abc";
        //5 发送

        String msg = "Hello World RabbitMQ 4 Topic Exchange Message ...";
        //basicPublish(String exchange, String routingKey, boolean mandatory, boolean immediate, BasicProperties props, byte[] body)
        channel.basicPublish(exchangeName,routingKey1,false,false,null,msg.getBytes());
        channel.basicPublish(exchangeName,routingKey2,false,false,null,msg.getBytes());
        channel.basicPublish(exchangeName,routingKey3,false,false,null,msg.getBytes());
        channel.close();
        connection.close();
    }
}

消费者

public class TopicExchangeReceiver {
    public static void main(String[] args) throws  Exception {
        ConnectionFactory connectionFactory=new ConnectionFactory();
        connectionFactory.setHost("192.168.123.171");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        Connection connection=connectionFactory.newConnection();
        Channel channel=connection.createChannel();
        //4 声明
        String exchangeName = "test_topic_exchange";
        String exchangeType = "topic";
        String queueName = "test_topic_queue";
        //String routingKey = "user.*";
        String routingKey = "user.#";
        //声明Exchange
        channel.exchangeDeclare(exchangeName, exchangeType, true, false, false, null);
        //声明队列
        channel.queueDeclare(queueName, false, false, false, null);
        //队列绑定
        channel.queueBind(queueName, exchangeName, routingKey);
        QueueingConsumer consumer = new QueueingConsumer(channel);
        channel.basicConsume(queueName, true, consumer);
        while (true){
          QueueingConsumer.Delivery delivery =consumer.nextDelivery();
            System.out.println(new String(delivery.getBody()));
        }
    }
}

Fanout Exchange

Fanout Exchange – 不处理路由键。你只需要简单的将队列绑定到交换机上。一个发送到交换机的消息都会被转发到与该交换机绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。Fanout交换机转发消息是最快的。

任何发送到Fanout Exchange的消息都会被转发到与该Exchange绑定(Binding)的所有Queue上。

  1. 可以理解为路由表的模式

  2. 这种模式不需要RouteKey

  3. 这种模式需要提前将Exchange与Queue进行绑定,一个Exchange可以绑定多个Queue,一个Queue可以同多个Exchange进行绑定。

  4. 如果接受到消息的Exchange没有与任何Queue绑定,则消息会被抛弃。

案例

生产者

public class FanoutExchangeSender {
    public static void main(String[] args) throws Exception {
        //创建connectionFactory
        ConnectionFactory connectionFactory=new ConnectionFactory();
        connectionFactory.setHost("192.168.123.171");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        Connection connection=connectionFactory.newConnection();
        Channel channel=connection.createChannel();
         String msg="test Fanout";
         String exchange="fanout_exchange";
         channel.basicPublish(exchange,"",null,msg.getBytes());
    }
}

消费者

public class FanoutExchangeRecevier {
    public static void main(String[] args) throws Exception {
        //创建connectionFactory
        ConnectionFactory connectionFactory=new ConnectionFactory();
        connectionFactory.setHost("192.168.123.171");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        Connection connection=connectionFactory.newConnection();
        Channel channel=connection.createChannel();
        String exchange="fanout_exchange";
        String exchangeType = "fanout";
        String queueName = "fanout_queue";

        //String exchange, String type, boolean durable, boolean autoDelete, boolean internal, Map<String, Object> arguments
        channel.exchangeDeclare(exchange, exchangeType, true, false, false, null);
        //String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
        channel.queueDeclare(queueName,true,false,false,null);
        channel.queueBind(queueName, exchange, "");

        QueueingConsumer queueingConsumer=new QueueingConsumer(channel);
        channel.basicConsume(queueName, true, queueingConsumer);
      while (true){
          QueueingConsumer.Delivery delivery=   queueingConsumer.nextDelivery();
           System.out.println(new String(delivery.getBody()));
      }

    }
}

生产者的可靠性投递

持久化方案

可靠性投递1.jpg

STEP1、STEP 2 : 在消息发送前对发送的消息进行持久化操作

STEP 3:发送消息给Broker

STEP 4:Broker将消息发送给消费者(有可能出现网路闪断导致消息不能到达)

STEP 5:接收到消息对消息状态进行更改

STEP 6,STEP 7,STEP 8:对消息状态未更新的定时的进行重新发送

Comfirm 机制

Comfirm机制,是指生产者投递消息后,如果Broker收到消息,则会给我们生产者一个应答

生产者进行接收应答,用来确定这条消息是否正常的发送到Broker,这种方式也是消息的可靠性投递的核心保障

//发送消息前开启confirmSelect
channel.confirmSelect();
//添加confirm监听
channel.addConfirmListener(new ConfirmListener() {
            @Override
            public void handleAck(long l, boolean b) throws IOException {
                System.out.println("发送成功");
            }
            @Override
            public void handleNack(long l, boolean b) throws IOException {
                System.out.println("发送失败");
            }
        });
//发送消息
channel.basicPublish("", queueName , null , msg.getBytes());

Return 机制

但是在某些情况下,如果我们在发送消息的时候,当前的exchange不存在或者指定的路由key路由不到,这个时候如果我们需要监听这种不可达的消息,就要使用Return Listener !

在基础API中有1个关键的配置项: Mandatory: 如果为true,则监听器会接收到路由不可达的消息,然后进行后续处理。

如果为false, 那么broker端自动删除该消息!

//添加return监听
channel.addReturnListener(new ReturnListener() {
   @Override
public void handleReturn(int i, String s, String s1,
                         String s2, AMQP.BasicProperties basicProperties,
                         byte[] bytes) throws IOException {
    
          System.out.println("发送失败:"+new String(bytes));
           }
    });
//Mandatory设置为true
 channel.basicPublish("", "bibi" , true ,null, msg.getBytes());

消费者的幂等性解决方案

业务唯一ID或指纹码机制,利用数据库主键去重

SELECT COUNT(1) FROM ORDER WHERE ID=业务唯一ID或指纹码

消费者的手动ACK

channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
       
channel.basicNack(delivery.getEnvelope().getDeliveryTag(),false,false);

TTL 与死信队列

TTL

TTL是Time To Live的缩写,也就是生存时间 RabbitMQ 支持消息的过期时间,在消息发送时可以进行指定 RabbitMQ支持队列的过期时间,从消息入队列开始计算,只要超过了队列的超时时间配置,那么消息会自动的清除

死信队列

DLX也是1个正常的Exchange,和一般的Exchange没有区别,它能在任何的队列上被指定,实际上就是设置某个队列的属性。 当这个队列中有死信时,RabbitMQ就会自动的将这个消息重新发布到设置的Exchange.上去,进而被路由到另一个队列。 可以监听这个队列中消息做相应的处理,这个特性可以弥补RabbitMQ3.0以前支持的immediate参数的功能。

DLX的条件

消息被拒绝(basic.reject/ basic.nack)并且requeue=false 消息TTL过期 队列达到最大长度

生产者

public class Sender4DLXExchange {

  
  public static void main(String[] args) throws Exception {
    
    //1 创建ConnectionFactory
    ConnectionFactory connectionFactory = new ConnectionFactory();
    connectionFactory.setHost("192.168.11.71");
    connectionFactory.setPort(5672);
    connectionFactory.setVirtualHost("/");
    
    //2 创建Connection
    Connection connection = connectionFactory.newConnection();
    //3 创建Channel
    Channel channel = connection.createChannel();  
    //4 声明
    String exchangeName = "test_dlx_exchange";
    String routingKey = "group.bfxy";
    //5 发送
    
    Map<String, Object> headers = new HashMap<String, Object>();
    
    AMQP.BasicProperties props = new AMQP.BasicProperties.Builder()
    .deliveryMode(2)
    .contentEncoding("UTF-8")
    //  TTL
    .expiration("6000")
    .headers(headers).build();
    
    String msg = "Hello World RabbitMQ 4 DLX Exchange Message ... ";
    channel.basicPublish(exchangeName, routingKey , props , msg.getBytes());     
    
  }
  
}

消费者

public class Receiver4DLXtExchange {

  public static void main(String[] args) throws Exception {
    
    
        ConnectionFactory connectionFactory = new ConnectionFactory() ;  
        
        connectionFactory.setHost("192.168.11.71");
        connectionFactory.setPort(5672);
    connectionFactory.setVirtualHost("/");
    
        connectionFactory.setAutomaticRecoveryEnabled(true);
        connectionFactory.setNetworkRecoveryInterval(3000);
        Connection connection = connectionFactory.newConnection();
        
        Channel channel = connection.createChannel();  
    //4 声明正常的 exchange queue 路由规则
    String queueName = "test_dlx_queue";
    String exchangeName = "test_dlx_exchange";
    String exchangeType = "topic";
    String routingKey = "group.*";
    //  声明 exchange
    channel.exchangeDeclare(exchangeName, exchangeType, true, false, false, null);
    
    
    //  注意在这里要加一个特殊的属性arguments: x-dead-letter-exchange
    Map<String, Object> arguments = new HashMap<String, Object>();
    arguments.put("x-dead-letter-exchange", "dlx.exchange");
    //arguments.put("x-dead-letter-routing-key", "dlx.*");
    //arguments.put("x-message-ttl", 6000);
    channel.queueDeclare(queueName, false, false, false, arguments);
    channel.queueBind(queueName, exchangeName, routingKey);
    
    
    //dlx declare:
    channel.exchangeDeclare("dlx.exchange", exchangeType, true, false, false, null);
    channel.queueDeclare("dlx.queue", false, false, false, null);
    channel.queueBind("dlx.queue", "dlx.exchange", "#");
    
    
        //  durable 是否持久化消息
        QueueingConsumer consumer = new QueueingConsumer(channel);
        //  参数:队列名称、是否自动ACK、Consumer
        channel.basicConsume(queueName, true, consumer);  
        //  循环获取消息  
        while(true){  
            //  获取消息,如果没有消息,这一步将会一直阻塞  
            Delivery delivery = consumer.nextDelivery();  
            String msg = new String(delivery.getBody());    
            System.out.println("收到消息:" + msg);  
        } 
  }
}

SpringBoot 集成 RabbitMQ

生产者

  1. pom
<!-- springboot rabbitmq(amqp) -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
  </dependency>  
  1. yaml
spring.rabbitmq.addresses=192.168.11.71:5672,192.168.11.72:5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
spring.rabbitmq.connection-timeout=15000
   
   ##  使用启用消息确认模式
spring.rabbitmq.publisher-confirms=true
   
   ##   设置return消息模式,注意要和mandatory一起去配合使用
   ##spring.rabbitmq.publisher-returns=true
   ##spring.rabbitmq.template.mandatory=true
   
spring.application.name=rabbit-producer
spring.http.encoding.charset=UTF-8
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
spring.jackson.default-property-inclusion=NON_NULL
   
@Component
public class RabbitSender {

  @Autowired
  private RabbitTemplate rabbitTemplate;
  
  /**
   *   这里就是确认消息的回调监听接口,用于确认消息是否被broker所收到
   */
  final ConfirmCallback confirmCallback = new RabbitTemplate.ConfirmCallback() {
    /**
     *   @param CorrelationData 作为一个唯一的标识
     *   @param ack broker 是否落盘成功 
     *   @param cause 失败的一些异常信息
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
    System.err.println("消息ACK结果:" + ack + ", correlationData: " +      correlationData.getId());
    }
  };
  
  /**
   *   对外发送消息的方法
   * @param message   具体的消息内容
   * @param properties  额外的附加属性
   * @throws Exception
   */
  public void send(Object message, Map<String, Object> properties) throws Exception {
    
    MessageHeaders mhs = new MessageHeaders(properties);
    Message<?> msg = MessageBuilder.createMessage(message, mhs);
    
    rabbitTemplate.setConfirmCallback(confirmCallback);
    
    //   指定业务唯一的iD
    CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
    
    MessagePostProcessor mpp = new MessagePostProcessor() {
      
      @Override
      public org.springframework.amqp.core.Message postProcessMessage(org.springframework.amqp.core.Message message)
          throws AmqpException {
        System.err.println("---> post to do: " + message);
        return message;
      }
    };
    
    rabbitTemplate.convertAndSend("exchange-1",
        "springboot.rabbit", 
        msg, mpp, correlationData);
    
  }
  

}

消费者

  1. yaml
spring.rabbitmq.addresses=192.168.11.71:5672,192.168.11.72:5672,192.168.11.71:5673
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
spring.rabbitmq.connection-timeout=15000

##   表示消费者消费成功消息以后需要手工的进行签收(ack),默认为auto
spring.rabbitmq.listener.simple.acknowledge-mode=manual
spring.rabbitmq.listener.simple.concurrency=5
spring.rabbitmq.listener.simple.max-concurrency=10
spring.rabbitmq.listener.simple.prefetch=1

##  
##  最好不要在代码里写死配置信息,尽量使用这种方式也就是配置文件的方式
##  在代码里使用   ${}  方式进行设置配置: ${spring.rabbitmq.listener.order.exchange.name}
spring.rabbitmq.listener.order.exchange.name=order-exchange
spring.rabbitmq.listener.order.exchange.durable=true
spring.rabbitmq.listener.order.exchange.type=topic
spring.rabbitmq.listener.order.exchange.key=order.*

spring.application.name=rabbit-producer
spring.http.encoding.charset=UTF-8
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
spring.jackson.default-property-inclusion=NON_NULL

@Component
public class RabbitReceive {
  
  /**
   *   组合使用监听
   *   @RabbitListener @QueueBinding @Queue @Exchange
   * @param message
   * @param channel
   * @throws Exception
   */
  @RabbitListener(bindings = @QueueBinding(
          value = @Queue(value = "queue-1", durable = "true"),
          exchange = @Exchange(name = "exchange-1",
          durable = "true",
          type = "topic",
          ignoreDeclarationExceptions = "true"),
          key = "springboot.*"
        )
      )
  @RabbitHandler
  public void onMessage(Message message, Channel channel) throws Exception {
    //  1. 收到消息以后进行业务端消费处理
    System.err.println("-----------------------");
    System.err.println("消费消息:" + message.getPayload());

//  2. 处理成功之后 获取deliveryTag 并进行手工的ACK操作, 因为我们配置文件里配置的是 手工签收
//  spring.rabbitmq.listener.simple.acknowledge-mode=manual
Long deliveryTag = (Long)message.getHeaders().get(AmqpHeaders.DELIVERY_TAG);
  channel.basicAck(deliveryTag, false);
  }
}