RabbitMQ

一、什么是 RabbitMQ

RabbitMQ是一个开源的遵循AMQP协议实现的基于Erlang语言编写,支持多种客户端(语言)。用于在分布式系统中存储消息,转发消息,具有高可用,高可扩性,易用性等特征的中间件。

二、RabbitMQ 相关概念

1、RabbitMQ 核心概念

image

  • Server:又称Broker ,接受客户端的连接,实现AMQP实体服务。 安装rabbitmq-server
  • Connection:连接,应用程序与Broker的网络连接 TCP/IP/ 三次握手和四次挥手
  • Channel:网络信道,几乎所有的操作都在Channel中进行,Channel是进行消息读写的通道,客户端可以建立对各Channel,每个Channel代表一个会话任务。
  • Message :消息,服务与应用程序之间传送的数据,由Properties和body组成,Properties可是对消息进行修饰,比如消息的优先级,延迟等高级特性,Body则就是消息体的内容。
  • Virtual Host 虚拟地址,用于进行逻辑隔离,最上层的消息路由,一个虚拟主机里可以有若干个Exhange和- Queueu,同一个虚拟主机里面不能有相同名字的Exchange
  • Exchange:交换机,接受消息,根据路由键发送消息到绑定的队列。(不具备消息存储的能力)
  • Bindings:Exchange和Queue之间的虚拟连接,binding中可以保存多个routing key.
  • Routing key:是一个路由规则,虚拟机可以用它来确定如何路由一个特定消息。
  • Queue:队列:也成为Message Queue,消息队列,保存消息并将它们转发给消费者。

RabbitMQ 的管理界面中就可以看到这些相关的概念:
image

2、RabbitMQ 运行流程

image

3、RabbitMQ 支持消息的模式

参考官网:https://www.rabbitmq.com/getstarted.html

  • 简单模式 Simple:不指定交换机,会使用默认交换机
    image

  • 工作模式 Work
    类型:无 ;特点:分发机制

  • 发布订阅模式
    类型:fanout;特点:Fanout—发布与订阅模式,是一种广播机制,它是没有路由key的模式。

  • 路由模式
    类型:direct;特点:有routing-key的匹配模式

  • 主题Topic模式
    类型:topic;特点:模糊的routing-key的匹配模式

  • 参数模式
    类型:headers;特点:参数匹配模式

4、RabbitMQ 使用场景

解耦:RabbitMQ可以实现不同应用之间的解耦,应用之间不直接进行通信,而是通过MQ来建立桥接。

削峰:以秒杀场景为例,会瞬时产生大量的请求,应用本身一时无法处理得完,因此可以先将请求都放入消息队列,引用再去慢慢处理。

异步:将比较耗时而且不需要即时(同步)返回结果的操作,作为消息放入消息队列,以此减少请求响应时间,提高系统性能。

三、以代码入门

先创建一个maven工程,pom 引入RabbitMQ:

<dependency>
    <groupId>com.rabbitmq</groupId>
    <artifactId>amqp-client</artifactId>
    <version>5.10.0</version>
</dependency>

使用MQ一般主要就是两个角色,生产者(producer)和消费者(consumer),RabbitMQ创建生产者和消费者步骤类似,主要分为一下几步:

  • 1、创建连接工厂
  • 2、创建连接Connection
  • 3、通过链接获取通道channel
  • 4、通过通道,可以创建交换机、声明队列、绑定关系、路由key、发送消息、接收消息
  • 5、主备消息内容
  • 6、发送消息给队列queue
  • 7、关闭连接
  • 8、关闭通道

下面讲解下各个模式下得代码实现。

1、简单模式

不声明交换机(exchange),使用默认交换机
image

生产者

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;

public class Producer {
    public static void main(String[] args) {
        // 1、创建连接工程
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.10.136");
        factory.setPort(5672);
        factory.setUsername("guest");
        factory.setPassword("guest");
        factory.setVirtualHost("/");

        Connection connection = null;
        Channel channel = null;
        try {
            // 2、创建连接 Connection
            connection = factory.newConnection("生产者");
            // 3、通过链接获取通道 Channel
            channel = connection.createChannel();
            // 4、通过通道,可以创建交换机、声明队列、绑定关系、路由key、发送消息、接收消息
            // 这里我们声明一个队列
            String queueName = "queue-jinsh";
            /*
              @params1:队列名称,
              @params2:是否持久化,就是消息是否存盘。其实非持久化也会存盘,但会伴随服务重启丢失
              @params3:排他性,是否是一个独占队列
              @params4:是否自动删除,随着最后一个消费者消费完毕消息后是否把队列删除
              @params5:携带一些附加参数
             */
            channel.queueDeclare(queueName, false, false, false, null);
            // 5、准备消息内容
            String message = "信其雌蛙一次莫黑多刺";
            // 6、发送消息给队列queue
            /*
              @params1:交换机,这里没有指定,会使用默认的交换机
              @params2:队列、路由key
              @params3:消息的状态控制
              @params4:消息主题
             */
            channel.basicPublish("", queueName, null, message.getBytes());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 7、关闭通道
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            // 8、关闭连接
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

消费者

import com.rabbitmq.client.*;

import java.io.IOException;

public class Consumer {

    /**
     * 1、创建连接工厂
     * 2、创建连接Connection
     * 3、通过链接获取通道channel
     * 4、通过通道,可以创建交换机、声明队列、绑定关系、路由key、发送消息、接收消息
     * 5、主备消息内容
     * 6、发送消息给队列queue
     * 7、关闭连接
     * 8、关闭通道
     */
    public static void main(String[] args) {
        // 1、创建连接工程
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.10.136");
        factory.setPort(5672);
        factory.setUsername("guest");
        factory.setPassword("guest");
        factory.setVirtualHost("/");

        Connection connection = null;
        Channel channel = null;
        try {
            // 2、创建连接 Connection
            connection = factory.newConnection("消费者");
            // 3、通过链接获取通道 Channel
            channel = connection.createChannel();
            // 4、通过通道,可以创建交换机、声明队列、绑定关系、路由key、发送消息、接收消息
            // 消费消息
            String queueName = "queue1";
            channel.basicConsume(queueName, true, new DeliverCallback() {
                public void handle(String consumerTag, Delivery message) throws IOException {
                    System.out.println("收到消息是:" + new String(message.getBody(), "utf-8"));
                }
            }, new CancelCallback() {
                public void handle(String consumerTag) throws IOException {
                    System.out.println("接收失败");
                }
            });

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 7、关闭通道
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            // 8、关闭连接
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

2、发布订阅模式

发布订阅模式在声明交换机(exchange)时需要指定交换机类型(type)为fanout,当生产者发布消息,绑定(binding)得所有消费者都将收到消息。
image

生产者

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class Producer {

    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.10.136");
        factory.setPort(5672);
        factory.setUsername("guest");
        factory.setPassword("guest");
        factory.setVirtualHost("/");

        Connection connection = null;
        Channel channel = null;
        try {
            connection = factory.newConnection("生产者");
            channel = connection.createChannel();
            channel.queueDeclare("queue4", false, false, false, null);
            channel.queueDeclare("queue5", false, false, false, null);
            String message = "信其雌蛙一次莫黑多刺";
            // 准备交换机
            String exchangeName = "fanout-exchange";
            // 定义路由key
            String routeKey = "";
            // 交换机类型
            String type = "fanout";
            channel.exchangeDeclare(exchangeName, type, false, false, false, null);
            channel.queueBind("queue4", exchangeName, "");
            channel.queueBind("queue5", exchangeName, "");
            channel.basicPublish(exchangeName, routeKey, null, message.getBytes());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (TimeoutException e) {
                    e.printStackTrace();
                }
            }
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

消费者

import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class Consumer{

    private static Runnable runnable = () -> {
        // 1、创建连接工程
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.10.136");
        factory.setPort(5672);
        factory.setUsername("guest");
        factory.setPassword("guest");
        factory.setVirtualHost("/");

        Connection connection = null;
        Channel channel = null;
        try {
            // 2、创建连接 Connection
            connection = factory.newConnection("消费者");
            // 3、通过链接获取通道 Channel
            channel = connection.createChannel();
            // 4、通过通道,可以创建交换机、声明队列、绑定关系、路由key、发送消息、接收消息
            // 消费消息
            String queueName = Thread.currentThread().getName();
            channel.basicConsume(queueName, true, new DeliverCallback() {
                public void handle(String consumerTag, Delivery message) throws IOException {
                    System.out.println(queueName + ":收到消息是:" + new String(message.getBody(), "utf-8"));
                }
            }, new CancelCallback() {
                public void handle(String consumerTag) throws IOException {
                    System.out.println("接收失败");
                }
            });

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 7、关闭通道
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (TimeoutException e) {
                    e.printStackTrace();
                }
            }
            // 8、关闭连接
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    };

    public static void main(String[] args) {
        new Thread(runnable, "queue4").start();
        new Thread(runnable, "queue5").start();
    }
}

3、路由模式

路由模式在声明交换机(exchange)时需要指定交换机类型(type)为direct,每个消费者和生产者绑定时需要指定一个路由key(routing-key),当生产者发布消息时也需要指定路由key,只有与发布时的路由key相同的绑定时的路由key对应的消费者才能消费到消息。
image

生产者

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class Producer {

    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.10.136");
        factory.setPort(5672);
        factory.setUsername("guest");
        factory.setPassword("guest");
        factory.setVirtualHost("/");

        Connection connection = null;
        Channel channel = null;
        try {
            connection = factory.newConnection("生产者");
            channel = connection.createChannel();
            // 申明queue
            channel.queueDeclare("queue2", false, false, false, null);
            channel.queueDeclare("queue3", false, false, false, null);
            String message = "信其雌蛙一次莫黑多刺";
            // 准备交换机
            String exchangeName = "direct-exchange";
            // 定义路由key
            String routeKey = "email";
            // 交换机类型
            String type = "direct";
            // 申明exchange
            channel.exchangeDeclare(exchangeName, type, false, false, false, null);

            channel.queueBind("queue2", exchangeName, "email");
            channel.queueBind("queue3", exchangeName, "sms");
            channel.basicPublish(exchangeName, routeKey, null, message.getBytes());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (TimeoutException e) {
                    e.printStackTrace();
                }
            }
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

消费者

import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class Consumer{

    private static Runnable runnable = () -> {
        // 1、创建连接工程
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.10.136");
        factory.setPort(5672);
        factory.setUsername("guest");
        factory.setPassword("guest");
        factory.setVirtualHost("/");

        Connection connection = null;
        Channel channel = null;
        try {
            // 2、创建连接 Connection
            connection = factory.newConnection("消费者");
            // 3、通过链接获取通道 Channel
            channel = connection.createChannel();
            // 4、通过通道,可以创建交换机、声明队列、绑定关系、路由key、发送消息、接收消息
            // 消费消息
            String queueName = Thread.currentThread().getName();
            channel.basicConsume(queueName, true, new DeliverCallback() {
                public void handle(String consumerTag, Delivery message) throws IOException {
                    System.out.println(queueName + ":收到消息是:" + new String(message.getBody(), "utf-8"));
                }
            }, new CancelCallback() {
                public void handle(String consumerTag) throws IOException {
                    System.out.println("接收失败");
                }
            });

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 7、关闭通道
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (TimeoutException e) {
                    e.printStackTrace();
                }
            }
            // 8、关闭连接
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    };

    public static void main(String[] args) {
        new Thread(runnable, "queue2").start();
        new Thread(runnable, "queue3").start();
    }
}

4、主题Topic模式

路由模式在声明交换机(exchange)时需要指定交换机类型(type)为topic,与路由模式相同,也需要routing-key的匹配,只不过主题模式的routing-key是模糊匹配,匹配规则如下:

  • * :必须匹配一个单词

  • # :匹配0个或1个或多个单词

例如生产者和消费者绑定时的routing-key="#.aaa.*" ,那么生产者发送消息时的routing-key的值为"dd.aaa.b"可以匹配上,或"dd.cc.aaa.b"也可以匹配上,但"dd.aaa.b.cc"是匹配不上的。
image

生产者

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class Producer {

    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.10.136");
        factory.setPort(5672);
        factory.setUsername("guest");
        factory.setPassword("guest");
        factory.setVirtualHost("/");

        Connection connection = null;
        Channel channel = null;
        try {
            connection = factory.newConnection("生产者");
            channel = connection.createChannel();
            channel.queueDeclare("queue6", false, false, false, null);
            channel.queueDeclare("queue7", false, false, false, null);
            channel.queueDeclare("queue8", false, false, false, null);
            String message = "信其雌蛙一次莫黑多刺";
            // 准备交换机
            String exchangeName = "topic-exchange";
            // 定义路由key
            String routeKey = "com.jinsh.user";
            // 交换机类型
            String type = "topic";
            channel.exchangeDeclare(exchangeName, type, false, false, false, null);

            channel.queueBind("queue6", exchangeName, "#.jinsh.#");
            channel.queueBind("queue7", exchangeName, "com.*");
            channel.queueBind("queue8", exchangeName, "com.#");
            channel.basicPublish(exchangeName, routeKey, null, message.getBytes());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (TimeoutException e) {
                    e.printStackTrace();
                }
            }
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

消费者

import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class Consumer{

    private static Runnable runnable = () -> {
        // 1、创建连接工程
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.10.136");
        factory.setPort(5672);
        factory.setUsername("guest");
        factory.setPassword("guest");
        factory.setVirtualHost("/");

        Connection connection = null;
        Channel channel = null;
        try {
            // 2、创建连接 Connection
            connection = factory.newConnection("消费者");
            // 3、通过链接获取通道 Channel
            channel = connection.createChannel();
            // 4、通过通道,可以创建交换机、声明队列、绑定关系、路由key、发送消息、接收消息
            // 消费消息
            String queueName = Thread.currentThread().getName();
            channel.basicConsume(queueName, true, new DeliverCallback() {
                public void handle(String consumerTag, Delivery message) throws IOException {
                    System.out.println(queueName + ":收到消息是:" + new String(message.getBody(), "utf-8"));
                }
            }, new CancelCallback() {
                public void handle(String consumerTag) throws IOException {
                    System.out.println("接收失败");
                }
            });

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 7、关闭通道
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (TimeoutException e) {
                    e.printStackTrace();
                }
            }
            // 8、关闭连接
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    };

    public static void main(String[] args) {
        new Thread(runnable, "queue6").start();
        new Thread(runnable, "queue7").start();
        new Thread(runnable, "queue8").start();
    }
}

5、工作模式

当有多个消费者时,我们的消息会被哪个消费者消费呢,我们又该如何均衡消费者消费信息的多少呢?
主要有两种模式:
1、轮询模式的分发:一个消费者一条,按均分配;
2、公平分发:根据消费者的消费能力进行公平分发,处理快的处理的多,处理慢的处理的少;按劳分配;
image

轮询模式

生产者

public class Producer {
    public static void main(String[] args) {
        // 1: 创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 2: 设置连接属性
        factory.setHost("192.168.10.136");
        factory.setPort(5672);
        factory.setUsername("guest");
        factory.setPassword("guest");
        factory.setVirtualHost("/");
        Connection connection = null;
        Channel channel = null;
        try {
            // 3: 从连接工厂中获取连接
            connection = factory.newConnection("生产者");
            // 4: 从连接中获取通道channel
            channel = connection.createChannel();
            // 6: 准备发送消息的内容
            for (int i = 1; i <= 20; i++) {
                //消息的内容
                String msg = "学相伴:" + i;
                // 7: 发送消息给中间件rabbitmq-server
                // @params1: 交换机exchange
                // @params2: 队列名称/routingkey
                // @params3: 属性配置
                // @params4: 发送消息的内容
                channel.basicPublish("", "queue1", null, msg.getBytes());
            }
            System.out.println("消息发送成功!");
        } catch (Exception ex) {
            ex.printStackTrace();
            System.out.println("发送消息出现异常...");
        } finally {
            // 7: 释放连接关闭通道
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
            if (connection != null) {
                try {
                    connection.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
}

消费者1

public class Work1 {
    public static void main(String[] args) {
        // 1: 创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 2: 设置连接属性
        factory.setHost("192.168.10.136");
        factory.setPort(5672);
        factory.setUsername("guest");
        factory.setPassword("guest");
        factory.setVirtualHost("/");
        Connection connection = null;
        Channel channel = null;
        try {
            // 3: 从连接工厂中获取连接
            connection = factory.newConnection("消费者-Work1");
            // 4: 从连接中获取通道channel
            channel = connection.createChannel();
            // 5: 申明队列queue存储消息
            // 这里如果queue已经被创建过一次了,可以不需要定义
//            channel.queueDeclare("queue1", false, false, false, null);
            // 同一时刻,服务器只会推送一条消息给消费者
            // 6: 定义接受消息的回调
            Channel finalChannel = channel;
            finalChannel.basicConsume("queue1", true, new DeliverCallback() {
                @Override
                public void handle(String s, Delivery delivery) throws IOException {
                    try{
                        System.out.println("Work1-收到消息是:" + new String(delivery.getBody(), "UTF-8"));
                        Thread.sleep(2000);
                    }catch(Exception ex){
                        ex.printStackTrace();
                    }
                }
            }, new CancelCallback() {
                @Override
                public void handle(String s) throws IOException {
                }
            });
            System.out.println("Work1-开始接受消息");
            System.in.read();
        } catch (Exception ex) {
            ex.printStackTrace();
            System.out.println("发送消息出现异常...");
        } finally {
            // 7: 释放连接关闭通道
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
}

消费者2

public class Work2 {
    public static void main(String[] args) {
        // 1: 创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 2: 设置连接属性
        factory.setHost("192.168.10.136");
        factory.setPort(5672);
        factory.setUsername("guest");
        factory.setPassword("guest");
        factory.setVirtualHost("/");
        Connection connection = null;
        Channel channel = null;
        try {
            // 3: 从连接工厂中获取连接
            connection = factory.newConnection("消费者-Work2");
            // 4: 从连接中获取通道channel
            channel = connection.createChannel();
            // 5: 申明队列queue存储消息
            // 这里如果queue已经被创建过一次了,可以不需要定义
            //channel.queueDeclare("queue1", false, true, false, null);
            // 同一时刻,服务器只会推送一条消息给消费者
            //channel.basicQos(1);
            // 6: 定义接受消息的回调
            Channel finalChannel = channel;
            finalChannel.basicConsume("queue1", true, new DeliverCallback() {
                @Override
                public void handle(String s, Delivery delivery) throws IOException {
                    try{
                        System.out.println("Work2-收到消息是:" + new String(delivery.getBody(), "UTF-8"));
                        Thread.sleep(200);
                    }catch(Exception ex){
                        ex.printStackTrace();
                    }
                }
            }, new CancelCallback() {
                @Override
                public void handle(String s) throws IOException {
                }
            });
            System.out.println("Work2-开始接受消息");
            System.in.read();
        } catch (Exception ex) {
            ex.printStackTrace();
            System.out.println("发送消息出现异常...");
        } finally {
            // 7: 释放连接关闭通道
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
}

公平分发模式

公平分发模式生产者和消费者代码与轮询模式一样,只是消费者的代码略有修改:

  • 新增finalChannel.basicQos(1);,表示每次消息的消费数量,即一次消费1个消息
  • basicConsume方法autoAck参数改为false,不自定应答

image

SpringBoot 中使用 RabbitMQ

首先pom文件引入

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

其次配置文件配置连接

spring:
  rabbitmq:
    host: 192.168.10.136
    port: 5672
    username: guest
    password: guest
    virtual-host: /

声明交换机、声明队列、交换机绑定队列
这个类是即可以放在生产者项目这边,也可以放在消费者项目那边,一般选择放消费者项目那边写。

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class rabbitMqConfiguration {

    // 1、声明交换机类型
    @Bean
    public Exchange exchange() {
        return new Exchange("order_exchange", true, false);
    }

    // 2、声明队列
    @Bean
    public Queue emailQueue() {
        return new Queue("email.queue", true);
    }

    // 3、绑定交换机和队列
    @Bean
    public Binding binding() {
        return BindingBuilder.bind(emailQueue()).to(exchange());
    }
}

生产者

@Service
public class ProduceService {

    @Autowired
    private RabbitTemplate rabbitTemplate;

	public void sendMessage() {
		// 参数1:交换机,参数2:路由key/queue队列名,参数3:消息内容
        rabbitTemplate.convertAndSend("order_exchange", "", orderId, "hahahahahahahaha");
	}
}

消费者

import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@RabbitListener(queues = {"email.queue"})
@Component
public class SmsDirectConsumer {

    @RabbitHandler
    public void receiveMessage(String message) {
        System.out.println("收到消息:" + message);
    }

}

对于上面“声明交换机、声明队列、交换机绑定队列”这一步,也可以通过注解的方式声明,写在消费类里:

import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.stereotype.Component;

@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "email.topic.queue", durable = "true", autoDelete = "false"),
        exchange = @Exchange(value = "topic_order_exchange", type = ExchangeTypes.TOPIC),
        key = "*.email.#"
))
@Component
public class EmailTopicConsumer {

    @RabbitHandler
    public void receiveMessage(String message) {
        System.out.println("email topic 收到订单消息:" + message);
    }
}

1、发布订阅模式

声明交换机、声明队列、交换机绑定队列

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FanoutRabbitMqConfiguration {

    // 1、声明交换机类型
    @Bean
    public FanoutExchange fanoutExchange() {
        return new FanoutExchange("fanout_order_exchange", true, false);
    }

    // 2、声明队列
    @Bean
    public Queue fanoutEmailQueue() {
        return new Queue("email.fanout.queue", true);
    }
    @Bean
    public Queue fanoutSmsQueue() {
        return new Queue("sms.fanout.queue", true);
    }
    @Bean
    public Queue fanoutWxQueue() {
        return new Queue("wx.fanout.queue", true);
    }

    // 3、绑定交换机和队列
    @Bean
    public Binding fanoutEmailBinding() {
        return BindingBuilder.bind(fanoutEmailQueue()).to(fanoutExchange());
    }
    @Bean
    public Binding fanoutSmsBinding() {
        return BindingBuilder.bind(fanoutSmsQueue()).to(fanoutExchange());
    }
    @Bean
    public Binding fanoutWxBinding() {
        return BindingBuilder.bind(fanoutWxQueue()).to(fanoutExchange());
    }
}

消费者这里只展示了1个其余两个类似

import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@RabbitListener(queues = {"email.fanout.queue"})
@Component
public class EmailFanoutConsumer {

    @RabbitHandler
    public void receiveMessage(String message) {
        System.out.println("email fanout 收到订单消息:" + message);
    }

}

生产者

    public void makeOrderFanout() {
        String orderId = UUID.randomUUID().toString();
        System.out.println("订单生产成功:" + orderId);
        // 3、通过rabbitmq完成消息分发
        String exchange = "fanout_order_exchange";
        // 参数1:交换机,参数2:路由key/queue队列名,参数3:消息内容
        rabbitTemplate.convertAndSend(exchange, "", orderId, postProcessor);
    }

2、路由模式

声明交换机、声明队列、交换机绑定队列

@Configuration
public class DirectRabbitMqConfiguration {

    // 1、声明交换机类型
    @Bean
    public DirectExchange directExchange() {
        return new DirectExchange("direct_order_exchange", true, false);
    }

    // 2、声明队列
    @Bean
    public Queue directEmailQueue() {
        return new Queue("email.direct.queue", true, );
    }
    @Bean
    public Queue directSmsQueue() {
        return new Queue("sms.direct.queue", true);
    }
    @Bean
    public Queue directWxQueue() {
        return new Queue("wx.direct.queue", true);
    }

    // 3、绑定交换机和队列
    @Bean
    public Binding directEmailBinding() {
        return BindingBuilder.bind(directEmailQueue()).to(directExchange()).with("email");
    }
    @Bean
    public Binding directSmsBinding() {
        return BindingBuilder.bind(directSmsQueue()).to(directExchange()).with("sms");
    }
    @Bean
    public Binding directWxBinding() {
        return BindingBuilder.bind(directWxQueue()).to(directExchange()).with("wx");
    }
}

消费者与上面类似这里不展示了
生产者

public void makeOrderDirect() {
        String orderId = UUID.randomUUID().toString();
        System.out.println("订单生产成功:" + orderId);
        // 3、通过rabbitmq完成消息分发
        String exchange = "direct_order_exchange";
        // 参数1:交换机,参数2:路由key/queue队列名,参数3:消息内容
        rabbitTemplate.convertAndSend(exchange, "email", orderId);
        rabbitTemplate.convertAndSend(exchange, "sms", orderId);
    }

3、主题模式

声明交换机、声明队列、交换机绑定队列

@Configuration
public class TopicRabbitMqConfiguration {

    // 1、声明交换机类型
    @Bean
    public DirectExchange topicExchange() {
        return new DirectExchange("topic_order_exchange", true, false);
    }

    // 2、声明队列
    @Bean
    public Queue topicEmailQueue() {
        return new Queue("email.topic.queue", true, );
    }
    @Bean
    public Queue directSmsQueue() {
        return new Queue("sms.topic.queue", true);
    }
    @Bean
    public Queue directWxQueue() {
        return new Queue("wx.topic.queue", true);
    }

    // 3、绑定交换机和队列
    @Bean
    public Binding topicEmailBinding() {
        return BindingBuilder.bind(topicEmailQueue()).to(topicExchange()).with("#.email.#");
    }
    @Bean
    public Binding topicSmsBinding() {
        return BindingBuilder.bind(topicSmsQueue()).to(topicExchange()).with("#.sms.*");
    }
    @Bean
    public Binding topicWxBinding() {
        return BindingBuilder.bind(topicWxQueue()).to(topicExchange()).with("*.wx.*");
    }
}

消费者与上面类似这里不展示了
生产者

public void makeOrderTopic() {
        String orderId = UUID.randomUUID().toString();
        System.out.println("订单生产成功:" + orderId);
        // 3、通过rabbitmq完成消息分发
        String exchange = "topic_order_exchange";
        // 参数1:交换机,参数2:路由key/queue队列名,参数3:消息内容
        rabbitTemplate.convertAndSend(exchange, "com.sms.aa", orderId);
        rabbitTemplate.convertAndSend(exchange, "aa.email.cc.vv", orderId);
    }

4、过期队列 TTL

过期队列要在队列声明时设置

@Bean
    public Queue ttlQueue() {
        Map<String, Object> arg = new HashMap<String, Object>();
        // 设置过期时间5秒
        arg.put("x-message-ttl", 5000);
        // 设置队列最多容纳几个消息
        // arg.put("x-max-length", 5);
        return new Queue("ttl.direct.queue", true, false, false, arg);
    }

即消息发布到队列中5秒内还没有被消费,则将废弃。

以上时对整个队列中的消息过期设置,还有一种是对单个消息的过期时间设置,在生产者端消息发送时配置:

public void makeOrderFanout() {
        String orderId = UUID.randomUUID().toString();
        System.out.println("订单生产成功:" + orderId);
        String exchange = "fanout_order_exchange";
        // 给消息设置过期时间
        MessagePostProcessor postProcessor = new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
				// 设置5秒过期
                message.getMessageProperties().setExpiration("5000");
                message.getMessageProperties().setContentEncoding("UTF-8");
                return message;
            }
        };
        // 参数1:交换机,参数2:路由key/queue队列名,参数3:消息内容
        rabbitTemplate.convertAndSend(exchange, "", orderId, postProcessor);
    }

当两种方式同时存在时,过期时间短的生效。

5、死信队列 DLX

所谓死信队列就是一个接盘用的队列,声明方式与普通队列一样

@Configuration
public class DeadQueueConfiguration {

    // 1、声明交换机类型
    @Bean
    public DirectExchange deadExchange() {
        return new DirectExchange("dead_order_exchange", true, false);
    }

    // 2、声明队列
    @Bean
    public Queue deadQueue() {
        return new Queue("dead.direct.queue", true);
    }

    // 3、绑定交换机和队列
    @Bean
    public Binding deadBinding() {
        return BindingBuilder.bind(deadQueue()).to(deadExchange()).with("dead");
    }
}

死信队列的作用就是用于存放失效的消息,以便再次业务处理。例如过期未消费的消息,过期后会被从原队列移动到死信队列里,需要在原队列里配置死信队列:

    @Bean
    public Queue ttlQueue() {
        Map<String, Object> arg = new HashMap<String, Object>();
        // 设置过期时间5秒
        arg.put("x-message-ttl", 5000);
        // 过期则或超过消息最大个数则进入死信队列
        arg.put("x-dead-letter-exchange", "dead_order_exchange");
        arg.put("x-dead-letter-routing-key", "dead");
        return new Queue("ttl.direct.queue", true, false, false, arg);
    }

6、延时队列

延时队列其实就是过期队列与死信队列的配合使用,达到消息延时处理的目的。例如,消息延时一分钟后处理,那么消息先发布到超时队列中,超时时间设置为60000毫秒,时间到了,消息就会进入死信队列,此时我们再在死信队列里处理这条消息就行了。

7、消息确认机制的配置

消息确认就是当生产者发布消息成功后,可以收到确认回调。
配置文件中开启publisher-confirm-type:

spring:
  rabbitmq:
    host: 192.168.10.136
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    publisher-confirm-type: correlated

publisher-confirm-type:

  • NONE值是禁用发布确认模式,是默认值
  • CORRELATED值是发布消息成功到交换器后会触发回调方法,如1示例
  • SIMPLE值经测试有两种效果,其一效果和CORRELATED值一样会触发回调方法,其二在发布消息成功后使用rabbitTemplate调用waitForConfirms或waitForConfirmsOrDie方法等待broker节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是waitForConfirmsOrDie方法如果返回false则会关闭channel,则接下来无法发送消息到broker;

确认回调类

package com.xuexiangban.rabbitmq.springbootorderrabbitmqproducer.callback;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

public class MessageConfirmCallback implements RabbitTemplate.ConfirmCallback {
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if(ack){
            System.out.println("消息确认成功!!!!");
        }else{
            System.out.println("消息确认失败!!!!");
        }
    }
}

生产者发布消息时设置

public void makeOrderTopic(){
        String orderId = UUID.randomUUID().toString();
        System.out.println("保存订单成功:id是:" + orderId);
        // 设置消息确认机制
        rabbitTemplate.setConfirmCallback(new MessageConfirmCallback());
        rabbitTemplate.convertAndSend("topic_order_ex","com.email.sms.xxx",orderId);
    }

8、消息重发次数与手动应答

消息消费过程中出现异常默认会再次发送消息,继续消费,然后继续异常,以此类推发生死循环。
解决这个问题主要有两种方案:

  • 配置消息的重发次数
  • try-catch捕捉异常,然后手动应答

两种方案最好还要配上死信队列,将异常消息最终放入死信队列里,再根据业务逻辑处理。

配置文件

spring:
  rabbitmq:
    host: 192.168.10.136
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    listener:
      simple:
        retry:
          enabled: true # 开启重试
          max-attempts: 3 # 最大重试次数
          initial-interval: 2000ms # 重试间隔时间
       #  acknowledge-mode: manual # 手动应答

消息消费出现异常

@RabbitListener(queues = {"email.direct.queue"})
@Component
public class EmailDirectConsumer {

    @RabbitHandler
    public void receiveMessage(String message, Channel channel, CorrelationData correlationData,
                            @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
        try {
            System.out.println("email direct 收到订单消息:" + message);

            // 消息消费过程中出现异常,会发生死循环
            int a = 1/0;

            channel.basicAck(tag, false);
        } catch (Exception e) {
            // 如果出现异常情况,根据实际情况去进行重发
            // 参数1:消息的tag,参数2:多条处理,参数3:requeue 重发
            // requeue = false,消息不会重发,会把消息打入死信队列
            // requeue = true,会死循环重发,如果使用true的话建议使用“解决方案1:控制重发次数”,通过重发次数去限制循环,
            // 不要使用try-catch方案,try-catch方案配置acknowledge-mode: manual,手动形式会使重发次数的配置失效。
            channel.basicNack(tag, false, false);
        }
    }
}
posted @ 2021-10-08 16:44  金盛年华  阅读(131)  评论(0编辑  收藏  举报