java整合RabbitMq和SpringBoot整合RabbitMq基本操作

1,845 阅读15分钟

建立rabbitMq的连接

引入依赖

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

建立rbbitMq连接工具类

public class ConnectionUtil {
    /**
     * 建立与RabbitMQ的连接
     * @return
     * @throws Exception
     */
    public static Connection getConnection() throws Exception {
        //定义连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置服务地址
        factory.setHost("192.168.1.103");
        //端口
        factory.setPort(5672);
        //设置账号信息,用户名、密码、vhost
        factory.setVirtualHost("/kavito");//设置虚拟机,一个mq服务可以设置多个虚拟机,每个虚拟机就相当于一个独立的mq
        factory.setUsername("kavito");
        factory.setPassword("123456");
        // 通过工厂获取连接
        Connection connection = factory.newConnection();
        return connection;
    }
}

简单模式

生产者

public class Send {
 
    private final static String QUEUE_NAME = "simple_queue";
 
    public static void main(String[] argv) throws Exception {
        // 1、获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 2、从连接中创建通道,使用通道才能完成消息相关的操作
        Channel channel = connection.createChannel();
        
        // 3、声明(创建)队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 4、消息内容
        String message = "Hello World!";
        
        // 向指定的队列中发送消息
        channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
        System.out.println(" [x] Sent '" + message + "'");
        
        //关闭通道和连接(资源关闭最好用try-catch-finally语句处理)
        channel.close();
        connection.close();
    }
}

queueDeclare

当使用RabbitMQ时,为了发送和接收消息,我们需要先创建一个队列。queueDeclare方法就是用于创建队列的。

该方法的语法如下:


public AMQP.Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments) throws IOException;

参数说明:

  • queue:要声明的队列名称。
  • durable:指定队列是否持久化,如果设置为true,则队列将在服务器重启后继续存在,否则队列将被删除。
  • exclusive:指定队列是否独占。如果设置为true,则只有创建这个队列的连接可以使用它,其他连接将无法访问该队列。
  • autoDelete:指定队列是否自动删除。如果设置为true,则当消费者断开与该队列的连接后,该队列将被删除。
  • arguments:用于设置队列的其他属性,例如队列的过期时间等。

调用queueDeclare方法时,如果队列不存在,则会创建一个新的队列,并返回一个包含队列信息的对象(AMQP.Queue.DeclareOk类型)。如果队列已经存在,则该方法不会做任何更改,并返回现有队列的信息。

需要注意的是,队列的声明必须在使用之前进行。如果尝试在消费者或生产者连接到队列后才声明队列,可能会导致问题。

basicPublish

basicPublish是RabbitMQ客户端API中的一个方法,用于将消息发送到指定的交换机。它有以下语法:


public void basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) throws IOException;

参数说明:

  • exchange:要把消息发布到哪个交换机,如果该字符串为空,则消息会被直接发送到队列上。
  • routingKey:用于路由消息到特定队列的键值。需要指定特定的交换机,只有特定交换机才存在routingKey的功能
  • props:消息的属性对象,用于设置一些消息元数据,例如消息的优先级、过期时间等。
  • body:要发布的消息主体内容,以字节数组形式传递。

调用该方法时,如果交换机不存在,则消息将丢失。如果设置了正确的路由键和交换机,则消息将被路由到相应的队列中。如果没有任何队列绑定到交换机,那么这条消息将被丢弃。

需要注意的是,basicPublish方法只是在客户端将消息发送到RabbitMQ服务器,而不关心是否能够成功投递到目标队列。因此,可以通过消息确认机制来确认消息是否已经成功地发送到了目标队列。

basicPublish的exchange为""

如果在basicPublish方法中将exchange参数设置为空字符串"",则消息会被发送到默认的交换机(Default Exchange)。默认交换机是一个没有名称的直接交换机,它根据消息的路由键将消息路由到与之绑定的队列中。

当使用默认交换机时,路由键和队列名必须完全匹配。例如,如果将消息的路由键设置为“myqueue”,那么只有名为“myqueue”的队列才会接收到这条消息。

需要注意的是,默认交换机不能像其他类型的交换机一样进行配置。如果需要更复杂的路由逻辑,则应该创建自己的交换机并将其绑定到队列上。

消费者

public class Recv {
    private final static String QUEUE_NAME = "simple_queue";
 
    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        //创建会话通道,生产者和mq服务所有通信都在channel通道中完成
        Channel channel = connection.createChannel();
        
        // 声明队列 重复创建队列,也不会存在问题
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        
        //实现消费方法
        DefaultConsumer consumer = new DefaultConsumer(channel){
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            /**
             * 当接收到消息后此方法将被调用
             * @param consumerTag  消费者标签,用来标识消费者的,在监听队列时设置channel.basicConsume
             * @param envelope 信封,通过envelope
             * @param properties 消息属性
             * @param body 消息内容
             * @throws IOException
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                //交换机
                String exchange = envelope.getExchange();
                //消息id,mq在channel中用来标识消息的id,可用于确认消息已接收
                long deliveryTag = envelope.getDeliveryTag();
                // body 即消息体
                String msg = new String(body,"utf-8");
                System.out.println(" [x] received : " + msg + "!");
            }
        };
        
        // 监听队列,第二个参数:是否自动进行消息确认。
        //参数:String queue, boolean autoAck, Consumer callback
        /**
         * 参数明细:
         * 1、queue 队列名称
         * 2、autoAck 自动回复,当消费者接收到消息后要告诉mq消息已接收,如果将此参数设置为tru表示会自动回复mq,如果设置为false要通过编程实现回复
         * 3、callback,消费方法,当消费者接收到消息要执行的方法
         */
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

DefaultConsumer类

DefaultConsumer是RabbitMQ Java客户端库中的一个类,它实现了Consumer接口并提供了一些默认的行为。它可用于消费者应用程序中,通过订阅队列来接收消息并处理它们。

在RabbitMQ Java客户端库中,DefaultConsumer类是一个抽象类,它实现了Consumer接口并提供了一些默认的方法用于消费消息。使用时需要继承该类并实现handleDelivery方法来处理接收到的消息。通过调用Channel的basicConsume方法,并传入继承自DefaultConsumer的实例,即可开始从队列中消费消息。

handleDelivery

"handleDelivery" 是一个方法名称,通常用于 RabbitMQ 或 Apache Kafka 等消息队列系统中的消费者客户端编程。在 Java 中,使用 RabbitMQ 的 AMQP 协议时,可以创建一个类继承 DefaultConsumer 类,并重写其中的 handleDelivery 方法来处理接收到的消息。handleDelivery 方法会在消费者接收到消息后被调用,将消息作为参数传入方法中,开发人员可以在该方法中自定义对消息的处理逻辑。

"handleDelivery" 方法是 DefaultConsumer 类中的一个抽象方法。在 RabbitMQ 中,消费者客户端可以通过实现 Consumer 接口或继承 DefaultConsumer 类来接收消息并进行处理。

当消费者接收到消息时,RabbitMQ 会调用这个方法,并将接收到的消息传递给它作为参数。开发人员可以在这个方法中编写自己的业务逻辑,对消息进行处理,例如解析消息体、持久化、转发等操作。

DefaultConsumer 是 RabbitMQ 提供的一个默认的消费者实现类,其中包含了很多有用的方法和属性,比如解析 AMQP 协议头等。因此,使用 DefaultConsumer 可以大大简化消费者的代码实现。

在继承 DefaultConsumer 类后,需要重写 handleDelivery 方法,以便在接收到消息时执行自定义的业务逻辑。例如:


    class MyConsumer extends DefaultConsumer {
    public MyConsumer(Channel channel) {
        super(channel);
    }

    @Override
    public void handleDelivery(String consumerTag, Envelope envelope,
            AMQP.BasicProperties properties, byte[] body) throws IOException {
        String message = new String(body, "UTF-8");
        System.out.println("Received message: " + message);
        // 在这里编写自己的业务逻辑
    }
}

上面的示例代码演示了如何继承 DefaultConsumer 类,并重写其中的 handleDelivery 方法来处理接收到的消息。在该方法中,首先将接收到的消息体转换成字符串,然后输出到控制台,并在接下来的代码中编写自己的业务逻辑。

basicConsume

basicConsume是RabbitMQ消息系统中的一个方法,它允许消费者(也称为订阅者或接收器)注册以从特定队列接收消息。一旦注册,消费者将会在消息发布到队列时收到这些消息

  1. queue 队列名称
  2. autoAck 自动回复,当消费者接收到消息后要告诉mq消息已接收,如果将此参数设置为tru表示会自动回复mq,如果设置为false要通过编程实现回复
  3. callback,消费方法,当消费者接收到消息要执行的方法

消息确认机制(ACK)

通过刚才的案例可以看出,消息一旦被消费者接收,队列中的消息就会被删除。

那么问题来了:RabbitMQ怎么知道消息被接收了呢?

如果消费者领取消息后,还没执行操作就挂掉了呢?或者抛出了异常?消息消费失败,但是RabbitMQ无从得知,这样消息就丢失了!

因此,RabbitMQ有一个ACK机制。当消费者获取消息后,会向RabbitMQ发送回执ACK,告知消息已经被接收。不过这种回执ACK分两种情况:

  1. 自动ACK:消息一旦被接收,消费者自动发送ACK

  2. 手动ACK:消息接收后,不会发送ACK,需要手动调用

public class Recv2 {
    private final static String QUEUE_NAME = "simple_queue";
 
    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 创建通道
        final Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [x] received : " + msg + "!");
                
                // 手动进行ACK
                /*
                 *  void basicAck(long deliveryTag, boolean multiple) throws IOException;
                 *  deliveryTag:用来标识消息的id
                 *  multiple:是否批量.true:将一次性ack所有小于deliveryTag的消息。
                 */
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        // 监听队列,第二个参数false,手动进行ACK
        channel.basicConsume(QUEUE_NAME, false, consumer);
    }
}

设置手动提交

channel.basicConsume(QUEUE_NAME, false, consumer);

工作队列Work模式

image.png

work queues与入门程序相比,多了一个消费端,两个消费端共同消费同一个队列中的消息,但是一个消息只能被一个消费者获取。

这个消息模型在Web应用程序中特别有用,可以处理短的HTTP请求窗口中无法处理复杂的任务。

接下来我们来模拟这个流程:

P:生产者:任务的发布者

C1:消费者1:领取任务并且完成任务,假设完成速度较慢(模拟耗时)

C2:消费者2:领取任务并且完成任务,假设完成速度较快

生产者

循环发送50条信息在rabbitMq中

public class Send {
    private final static String QUEUE_NAME = "test_work_queue";
 
    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 循环发布任务
        for (int i = 0; i < 50; i++) {
            // 消息内容
            String message = "task .. " + i;
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            System.out.println(" [x] Sent '" + message + "'");
 
            Thread.sleep(i * 2);
        }
        // 关闭通道和连接
        channel.close();
        connection.close();
    }
}

消费者

常见两个消费者,两边代码一模一样。接收同一个队列

public class Recv {
    private final static String QUEUE_NAME = "test_work_queue";
 
    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        //创建会话通道,生产者和mq服务所有通信都在channel通道中完成
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        //实现消费方法
        DefaultConsumer consumer = new DefaultConsumer(channel){
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body,"utf-8");
                System.out.println(" [消费者1] received : " + msg + "!");
                //模拟任务耗时1s
                try { TimeUnit.SECONDS.sleep(1); } catch (Exception e) { e.printStackTrace(); }
            }
        };
        // 监听队列,第二个参数:是否自动进行消息确认。
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

当存在一个队列,多个消费者进行消费。那么这个队列的消息就会被多个消费者平分

当一个 RabbitMQ 队列被多个消费者绑定时,消息会被平均分配给这些消费者。这种行为被称为"负载均衡"。每个消费者将从队列中接收一部分消息并进行处理。如果某个消费者无法处理其分配的消息,则它们将被重新排队,并尝试被重新分配到其他可用的消费者。

交换机

RabbitMQ 是一个消息代理,它使用交换机(Exchange)将消息路由到队列(Queue)。交换机是 RabbitMQ 中的核心组件之一,负责接收生产者发送的消息并将其路由到一个或多个与之绑定的队列。

RabbitMQ 支持四种类型的交换机:

  1. Direct Exchange:直接交换机,根据消息的 routing key(路由键),把消息转发到与之绑定的唯一队列。可以理解为点对点模式。
  2. Fanout Exchange:扇形(广播)交换机,将消息转发到所有与之绑定的队列。可以理解为广播模式。
  3. Topic Exchange:主题交换机,根据消息的 routing key(路由键)和通配符匹配规则,把消息转发到一个或多个与之绑定的队列。可以理解为发布订阅模式。
  4. Headers Exchange:首部交换机,根据消息的 headers(消息头)中的键值对匹配规则,把消息转发到一个或多个与之绑定的队列。

不同类型的交换机有不同的路由策略,可以根据业务需求选择适合自己的交换机类型。

交换机说明

RabbitMQ的发送消息流程如下:

  1. 应用程序连接到RabbitMQ服务器。
  2. 应用程序创建一个消息,并将其发送到一个exchange(交换机)。
  3. Exchange接收到消息后,根据指定的routing key(路由键)将消息路由到一个或多个queue(队列)中。
  4. 如果消息被路由到了多个队列,那么这些队列中的消费者将竞争接收该消息。
  5. 消费者从相应的队列中接收消息并进行处理。

需要注意的是,发送消息的应用程序和接收消息的消费者可能并不在同一台计算机上。同时,如果Exchange无法找到任何匹配的队列,那么消息将会被丢弃。

前面没有指定交换机:采用了默认交换机

发布订阅模式 fanout交换机

Publish/subscribe

一个生产者发送消息到交换机。通过交换机发送到多个队列

一个消费者一个队列。从而实现每个消费者都有一份生产者的数据

image.png

生产者

声明交换机,消息只考虑推送到交换机中

public class Send {
 
    private final static String EXCHANGE_NAME = "test_fanout_exchange";
 
    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明exchange,指定类型为fanout
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
        
        // 消息内容
        String message = "注册成功!!";
        // 发布消息到Exchange
        channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
        System.out.println(" [生产者] Sent '" + message + "'");
 
        channel.close();
        connection.close();
    }
}

消费者

创建队列,绑定到交换机中指定 队列名称,交换机名称,routingKey

接收队列的中的消息

public class Recv {
    private final static String QUEUE_NAME = "fanout_exchange_queue_sms";//短信队列
 
    private final static String EXCHANGE_NAME = "test_fanout_exchange";
 
    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
 
        // 绑定队列到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
 
        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [短信服务] received : " + msg + "!");
            }
        };
        // 监听队列,自动返回完成
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

queueBind

queueBind是RabbitMQ中用于将一个队列(queue)绑定(bind)到一个交换机(exchange)上的操作。

这个操作需要指定三个参数:队列名称、交换机名称和路由键(routing key)

路由键是一个字符串,用于匹配消息的路由规则。当一个交换机接收到一条消息时,它会根据消息的路由键将消息发送到与之绑定的队列中。

因此,在进行queueBind操作时,需要确保队列和交换机已经被创建,并且使用相同的路由键来绑定它们。

Routing 路由模型(交换机类型:direct)

image.png

P:生产者,向Exchange发送消息,发送消息时,会指定一个routing key。

X:Exchange(交换机),接收生产者的消息,然后把消息递交给 与routing key完全匹配的队列

C1:消费者,其所在队列指定了需要routing key 为 error 的消息

C2:消费者,其所在队列指定了需要routing key 为 info、error、warning 的消息

生产者

生产者发送消息的指定routingKey

这里可以通过逻辑来指定不同的routingKey从而发送到不同的队列

public class Send {
    private final static String EXCHANGE_NAME = "test_direct_exchange";
 
    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明exchange,指定类型为direct
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        // 消息内容,
        String message = "注册成功!请短信回复[T]退订";
        // 发送消息,并且指定routing key 为:sms,只有短信服务能接收到消息
        channel.basicPublish(EXCHANGE_NAME, "sms", null, message.getBytes());
        System.out.println(" [x] Sent '" + message + "'");
 
        channel.close();
        connection.close();
    }
}

消费者

public class Recv {
    private final static String QUEUE_NAME = "direct_exchange_queue_sms";//短信队列
    private final static String EXCHANGE_NAME = "test_direct_exchange";
 
    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        
        // 绑定队列到交换机,同时指定需要订阅的routing key。可以指定多个
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "sms");//指定接收发送方指定routing key为sms的消息
        //channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "email");
 
        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [短信服务] received : " + msg + "!");
            }
        };
        // 监听队列,自动ACK
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

消费指定绑定队列和routingKey

// 绑定队列到交换机,同时指定需要订阅的routing key。可以指定多个
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "sms");//指定接收发送方指定routing key为sms的消息
//channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "email");

Topics 通配符模式(交换机类型:topics)

image.png

每个消费者监听自己的队列,并且设置带统配符的routingkey,生产者将消息发给broker,由交换机根据routingkey来转发消息到指定的队列。

与路由模式一样都是通过routingKey进行分发到队列,但是指定的routingKey可以进行通配符使用

与路由模式一样都是通过routingKey进行分发到队列,但是指定的routingKey可以进行通配符使用

通配符

RabbitMQ 支持三种通配符来处理 routing key:

  1. *:匹配单个单词,可以出现在任何位置。例如,如果一个队列绑定到 *.nyse,那么它将接收所有 routing key 的最后一个单词是 nyse 的消息,例如 stock.usd.nyseweather.nasdaq.nyse
  2. #:匹配零个或多个单词,只能出现在末尾。例如,一个队列绑定到 stock.#,它将接收所有 routing key 以 stock. 开头的消息,例如 stock.usd.nysestock.eur.lse
  3. {}:用于绑定时指定参数值,也称为变量。例如,一个队列绑定到 stock.{market}.nyse,它将接收所有 routing key 的第二个单词是 nyse,并将第一个单词作为参数传递给消费者。例如,如果一个消息的 routing key 是 stock.usd.nyse,那么消费者可以得到 {market: "usd"} 这个参数值。参考 AMQP 0-9-1 规范的 Section 3.1.5 for more information。

不匹配任意队列,被丢弃

不匹配任意队列,被丢弃

生产者

public class Send {
    private final static String EXCHANGE_NAME = "test_topic_exchange";
 
    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明exchange,指定类型为topic
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
        // 消息内容
        String message = "这是一只行动迅速的橙色的兔子";
        // 发送消息,并且指定routing key为:quick.orange.rabbit
        channel.basicPublish(EXCHANGE_NAME, "quick.orange.rabbit", null, message.getBytes());
        System.out.println(" [动物描述:] Sent '" + message + "'");
 
        channel.close();
        connection.close();
    }
}
public class Recv {
    private final static String QUEUE_NAME = "topic_exchange_queue_Q1";
    private final static String EXCHANGE_NAME = "test_topic_exchange";
 
    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        
        // 绑定队列到交换机,同时指定需要订阅的routing key。订阅所有的橙色动物
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "*.orange.*");
 
        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [消费者1] received : " + msg + "!");
            }
        };
        // 监听队列,自动ACK
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

消息不丢失,持久化

交换机持久化

在RabbitMQ中,交换机(Exchange)持久化是指将交换机及其元数据保存到磁盘上,以便在RabbitMQ服务器重启后仍然可用。与队列持久化类似,交换机默认情况下也是非持久化的。

要使交换机持久化,需要在创建交换机时将durable参数设置为true,例如:

channel.exchangeDeclare("myexchange", "direct", true);

此外,在使用持久化交换机时,建议同时将相关的队列和消息也进行持久化,以确保整个消息传递路径的可靠性。

请注意,与队列持久化类似,交换机持久化也不能完全避免数据丢失的可能性,因此建议在关键任务中使用事务或发布确认机制来确保消息的可靠性。

队列持久化

在RabbitMQ中,队列持久化是指将队列及其元数据(包括消息)保存到磁盘上,以便在RabbitMQ服务器重启后仍然可用。默认情况下,队列和消息都是非持久化的,这意味着它们只存在于内存中,并且在RabbitMQ服务器重新启动时会丢失。

要使队列持久化,需要在创建队列时将durable参数设置为true,例如:

channel.queueDeclare("myqueue", true, false, false, null);

此外,发送到持久化队列的消息也必须是持久化的,可以通过将消息的deliveryMode属性设置为2来实现:

byte[] messageBodyBytes = "Hello, world!".getBytes();
BasicProperties props = new BasicProperties().builder()
                          .deliveryMode(2)
                          .build();
channel.basicPublish("", "myqueue", props, messageBodyBytes);

请注意,在使用持久化队列时,仍然存在一些数据丢失的可能性,例如在消息投递过程中发生了网络故障等情况。因此,建议在关键任务中使用事务或发布确认机制来确保消息的可靠性。

消息持久化

在RabbitMQ中,消息持久化是指将消息保存到磁盘上,以便在RabbitMQ服务器重启后仍然可用。默认情况下,消息是非持久化的。

要使消息持久化,需要将消息的deliveryMode属性设置为2,例如:

byte[] messageBodyBytes = "Hello, world!".getBytes();
BasicProperties props = new BasicProperties().builder()
                          .deliveryMode(2)
                          .build();
channel.basicPublish("", "myqueue", props, messageBodyBytes);

此外,在使用持久化消息时,建议同时将相关的队列和交换机也进行持久化,以确保整个消息传递路径的可靠性。

消息消费后会被清除

请注意,虽然使用持久化消息可以增加数据的可靠性,但仍然存在一些数据丢失的可能性,例如在消息投递过程中发生了网络故障等情况。因此,建议在关键任务中使用事务或发布确认机制来确保消息的可靠性。

消息持久化只是将消息存储到磁盘中,以确保在RabbitMQ服务器重启后仍然可用。如果消费者已经成功接收和处理了持久化消息,则该消息将被从队列中删除,无论它是否已经被持久化。

当消息被发送到持久化队列时,它不会立即被删除。相反,它会一直保留在队列中,直到被确认或者过期。如果消费者成功地从队列中获取并确认了消息,则该消息将被标记为已传递,并从队列中删除。如果消费者无法成功处理消息,例如由于应用程序崩溃或断电等原因,则消息将保留在队列中,直到重新连接或手动确认处理失败的消息。

因此,即使使用消息持久化,仍然需要确保消费者能够及时地处理消息,以避免队列堆积和数据丢失。

整合Boot操作

添加依赖

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

配置文件,绑定rabbitMq

server:
  port: 8021
spring:
  #给项目来个名字
  application:
    name: rabbitmq-provider
  #配置rabbitMq 服务器
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: root
    password: root
    #虚拟host 可以不设置,使用server默认host
    virtual-host: JCcccHost

配置类

配置类主要用来创建 队列 交换机 绑定关系 。

当然这些操作,我们同样可以在rabbitMq中创建好,但是在程序中再次写好,保证不会因为队列,交换机等不存在。

因为存在过的再次创建也不会影响

我们可以定义一个 RabbitMQConfig 类,并使用 @Configuration 注解将其标记为配置类。然后,我们可以在这个类中定义需要创建的队列、交换机等。

例如,我们可以定义一个名为 myQueue 的队列,代码如下:

@Bean
public Queue myQueue() {
    return new Queue("myQueueName", true, false, false);
}

在上面的代码中,我们使用 @Bean 注解将一个名为 myQueue 的队列注册为一个 Bean,这样这个队列就可以被其他组件注入和使用了。在创建队列时,我们使用 new Queue() 来实例化一个队列对象,传入参数分别为队列名称、是否持久化、是否独占、是否自动删除。

同样的,我们也可以定义一个名为 myExchange 的交换机:

@Bean
public TopicExchange myExchange() {
    return new TopicExchange("myExchangeName");
}

在上面的代码中,我们使用 TopicExchange 类来实例化一个主题交换机对象,传入参数为交换机名称。

最后,我们可以使用 BindingBuilder 类的 bind() 方法来将队列和交换机进行绑定:

@Bean
public Binding binding() {
    return BindingBuilder.bind(myQueue()).to(myExchange()).with("myRoutingKey");
}

在上面的代码中,我们使用 BindingBuilder 类的 bind() 方法来创建一个绑定对象,并传入要绑定的队列、要绑定到的交换机以及路由键。这里的路由键为 myRoutingKey,表示只有当消息的路由键为该值时,才会被发送到该队列。

除了队列、交换机和绑定外,还可以在配置类中定义其他的 RabbitMQ 相关组件,比如 RabbitTemplateSimpleMessageListenerContainer 等。需要根据实际情况来选择创建哪些组件。

配置类创建队列,交换机,绑定关系

@Configuration
public class RabbitMQConfig {

    public static final String QUEUE_NAME = "myQueue";
    public static final String EXCHANGE_NAME = "myExchange";

    @Bean
    public Queue myQueue() {
        return new Queue(QUEUE_NAME, true, false, false);
    }

    @Bean
    public TopicExchange myExchange() {
        return new TopicExchange(EXCHANGE_NAME);
    }

    @Bean
    public Binding binding() {
        return BindingBuilder.bind(myQueue()).to(myExchange()).with("myRoutingKey");
    }

}

@RabbitListener

@RabbitListener 是一个注解,用于将一个方法标记为 RabbitMQ 的消息监听器。当有消息到达指定的队列时,该方法就会被自动调用。

使用 @RabbitListener 注解时,需要指定要监听的队列名称或者是队列对象等相关信息。例如:

java复制代码@Component
public class MyConsumer {

    @RabbitListener(queues = "myQueue")
    public void handleMessage(Message message) {
        // 处理消息
    }
}

在上面的代码中,我们定义了一个名为 handleMessage() 的方法,并通过 @RabbitListener 注解将它标记为监听器。其中,queues 参数指定了要监听的队列名称为 myQueue。当有消息到达 myQueue 队列时,handleMessage() 方法就会被自动调用,并接收到这个消息。

除了 queues 参数外,@RabbitListener 注解还可以指定其他参数来控制监听行为。例如:

  • id: 监听器的唯一标识符。
  • containerFactory: 消息容器工厂,用于创建并配置 SimpleMessageListenerContainer 对象。
  • concurrency: 监听器的并发消费者数。
  • autoStartup: 监听器是否自动启动。
  • exclusive: 是否独占队列,只能由一个消费者进行消费。
  • priority: 消费者优先级。
  • admin: 用于创建和管理队列、交换机等组件的 RabbitAdmin 对象。
  • bindings: 绑定关系,可以指定多个队列和交换机的绑定关系。

需要注意的是,使用 @RabbitListener 注解时,默认情况下会创建一个 SimpleMessageListenerContainer 对象,并将这个方法注册为监听器。如果需要更细粒度的控制,可以手动创建 SimpleMessageListenerContainer 对象,并通过参数传递给 @RabbitListener 注解。

@RabbitListener注解可以用在类级别和方法级别上。

当@RabbitListener注解被用在类级别上时,它指定了一个默认的队列或Exchange,所有使用@RabbitHandler注解的方法都将监听该队列或Exchange。例如:

java复制代码@Component
@RabbitListener(queues = "myQueue")
public class MyMessageConsumer {
    
    @RabbitHandler
    public void handleMessage(String message) {
        // 处理消息
    }
    
}

当@RabbitListener注解被用在方法级别上时,它指定了特定的队列或Exchange,只有该方法才会监听该队列或Exchange。例如:

java复制代码@Component
public class MyMessageConsumer {
    
    @RabbitListener(queues = "myQueue")
    @RabbitHandler
    public void handleMessage(String message) {
        // 处理消息
    }
    
}

在这个例子中,只有使用@RabbitHandler注解的handleMessage方法才会监听"myQueue"队列,并处理到达该队列的消息。

@RabbitHandler

类似与@Override

@RabbitHandler是Spring AMQP提供的一个注解,它的作用是标识一个方法为处理RabbitMQ消息的方法。当消费者接收到消息后,根据消息中指定的路由键来找到相应的@RabbitHandler方法并调用它来处理消息。

@RabbitListener创建队列,交换机,绑定等

可以省略配置类创建队列,交换机,绑定

  1. exchange = @Exchange(value = EventConfig.exchangeGR,表示接收的交换器名称是EventConfig.exchangeGR,自己根据项目情况灵活配置上。
  2. value = @Queue(value = queueName表示接收的消息队列名称是queueName,自己根据项目情况灵活配置上。
  3. type = ExchangeTypes.FANOUT表示接收消息的类型,分为常用的三种FANOUT,广播模式,只要连接上,消息就全部通过接收到;DIRECT,直连模式,表示消息单独发;TOPIC,订阅模式,根据key匹配规则,匹配符合规则的消息数据
  4. key=xxx,表示接收消息时候的key,TOPIC模式必须配置上
  5. 消息接收消费后,一定要channel.basicAck(deliveryTag, false)删除消息。
  @RabbitListener(bindings = @QueueBinding(
            exchange = @Exchange("myOrder"),
            key = "computer",
            value = @Queue("computerOrder")
    ))
    public void receiveComputer(String msg){
        log.info(" receiveComputer service = {}" , msg );
    }


@RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = queueName, durable = "true", ignoreDeclarationExceptions = "true"),
            exchange = @Exchange(value = EventConfig.exchangeGR, type = ExchangeTypes.FANOUT, durable = "true"),
            key = "orderRoutingKey", ignoreDeclarationExceptions = "true"))
    public void save(@Payload GoodsReciptEvent event, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag){
        log.info("SaveEvent: " + event.toString());
        try {
            channel.basicAck(deliveryTag, false);
        }catch (Exception e){
        
            log.info("event json -->"+JSONObject.toJSONString(event));
            try {
                channel.basicAck(deliveryTag, false);
            }catch (Exception ee){
                log.error("save failed !",e);
            }
        }


示例一(直连交换机)

省略了 依赖 和配置文件

配置类

@Configuration
public class DirectRabbitConfig {
 
    //队列 起名:TestDirectQueue
    @Bean
    public Queue TestDirectQueue() {
        return new Queue("TestDirectQueue",true);
    }
 
    //Direct交换机 起名:TestDirectExchange
    @Bean
    DirectExchange TestDirectExchange() {
        return new DirectExchange("TestDirectExchange");
    }
 
    //绑定  将队列和交换机绑定, 并设置用于匹配键:TestDirectRouting
    @Bean
    Binding bindingDirect() {
        return BindingBuilder.bind(TestDirectQueue()).to(TestDirectExchange()).with("TestDirectRouting");
    }
}

生产者

@RestController
public class SendMessageController {
 
    @Autowired
    RabbitTemplate rabbitTemplate;  //使用RabbitTemplate,这提供了接收/发送等等方法
 
    @GetMapping("/sendDirectMessage")
    public String sendDirectMessage() {
        String messageId = String.valueOf(UUID.randomUUID());
        String messageData = "test message, hello!";
        String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        Map<String,Object> map=new HashMap<>();
        map.put("messageId",messageId);
        map.put("messageData",messageData);
        map.put("createTime",createTime);
        //将消息携带绑定键值:TestDirectRouting 发送到交换机TestDirectExchange
        rabbitTemplate.convertAndSend("TestDirectExchange", "TestDirectRouting", map);
        return "ok";
    }
 
 
}

消费者

@Component
@RabbitListener(queues = "TestDirectQueue")//监听的队列名称 TestDirectQueue
public class DirectReceiver {
 
    @RabbitHandler
    public void process(Map testMessage) {
        System.out.println("DirectReceiver消费者收到消息  : " + testMessage.toString());
    }
 
}

示例二 (Topic交换机)

配置类

@Configuration
public class TopicRabbitConfig {
    //绑定键
    public final static String man = "topic.man";
    public final static String woman = "topic.woman";
 
    @Bean
    public Queue firstQueue() {
        return new Queue(TopicRabbitConfig.man);
    }
 
    @Bean
    public Queue secondQueue() {
        return new Queue(TopicRabbitConfig.woman);
    }
 
    @Bean
    TopicExchange exchange() {
        return new TopicExchange("topicExchange");
    }
 
 
    //将firstQueue和topicExchange绑定,而且绑定的键值为topic.man
    //这样只要是消息携带的路由键是topic.man,才会分发到该队列
    @Bean
    Binding bindingExchangeMessage() {
        return BindingBuilder.bind(firstQueue()).to(exchange()).with(man);
    }
 
    //将secondQueue和topicExchange绑定,而且绑定的键值为用上通配路由键规则topic.#
    // 这样只要是消息携带的路由键是以topic.开头,都会分发到该队列
    @Bean
    Binding bindingExchangeMessage2() {
        return BindingBuilder.bind(secondQueue()).to(exchange()).with("topic.#");
    }
 
}

生产者

    @GetMapping("/sendTopicMessage1")
    public String sendTopicMessage1() {
        String messageId = String.valueOf(UUID.randomUUID());
        String messageData = "message: M A N ";
        String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        Map<String, Object> manMap = new HashMap<>();
        manMap.put("messageId", messageId);
        manMap.put("messageData", messageData);
        manMap.put("createTime", createTime);
        rabbitTemplate.convertAndSend("topicExchange", "topic.man", manMap);
        return "ok";
    }
 
    @GetMapping("/sendTopicMessage2")
    public String sendTopicMessage2() {
        String messageId = String.valueOf(UUID.randomUUID());
        String messageData = "message: woman is all ";
        String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        Map<String, Object> womanMap = new HashMap<>();
        womanMap.put("messageId", messageId);
        womanMap.put("messageData", messageData);
        womanMap.put("createTime", createTime);
        rabbitTemplate.convertAndSend("topicExchange", "topic.woman", womanMap);
        return "ok";
    }
}

消费者


@Component
public class TopicReceiver {
 
    @RabbitListener(queues = "topic.man")
    @RabbitHandler
    public void topicManReceiver(Map testMessage) {
        System.out.println("TopicManReceiver消费者收到消息  : " + testMessage.toString());
    }
    
    @RabbitListener(queues = "topic.woman")
    @RabbitHandler
    public void topicTotalReceiver(Map testMessage) {
        System.out.println("TopicTotalReceiver消费者收到消息  : " + testMessage.toString());
    }


}

TopicManReceiver监听队列1,绑定键为:topic.man TopicTotalReceiver监听队列2,绑定键为:topic.#

TopicManReceiver() 只能监听到 消费者1的消息

TopicManReceiver() 可以监听到 消费者1和消费者2的消息

示例三(Fanout交换机)

配置类

@Configuration
public class FanoutRabbitConfig {
 
    /**
     *  创建三个队列 :fanout.A   fanout.B  fanout.C
     *  将三个队列都绑定在交换机 fanoutExchange 上
     *  因为是扇型交换机, 路由键无需配置,配置也不起作用
     */
 

    @Bean
    public Queue queueA() {
        return new Queue("fanout.A");
    }
 
    @Bean
    public Queue queueB() {
        return new Queue("fanout.B");
    }
 
    @Bean
    public Queue queueC() {
        return new Queue("fanout.C");
    }
 
    @Bean
    FanoutExchange fanoutExchange() {
        return new FanoutExchange("fanoutExchange");
    }
 
    @Bean
    Binding bindingExchangeA() {
        return BindingBuilder.bind(queueA()).to(fanoutExchange());
    }
 
    @Bean
    Binding bindingExchangeB() {
        return BindingBuilder.bind(queueB()).to(fanoutExchange());
    }
 
    @Bean
    Binding bindingExchangeC() {
        return BindingBuilder.bind(queueC()).to(fanoutExchange());
    }
}

生产者

    @GetMapping("/sendFanoutMessage")
    public String sendFanoutMessage() {
        String messageId = String.valueOf(UUID.randomUUID());
        String messageData = "message: testFanoutMessage ";
        String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        Map<String, Object> map = new HashMap<>();
        map.put("messageId", messageId);
        map.put("messageData", messageData);
        map.put("createTime", createTime);
        rabbitTemplate.convertAndSend("fanoutExchange", null, map);
        return "ok";
    }

消费者

@Component
public class FanoutReceiver {
 
    @RabbitListener(queues = "fanout.A")
    @RabbitHandler
    public void processA(Map testMessage) {
        System.out.println("FanoutReceiverA消费者收到消息  : " +testMessage.toString());
    }
 
    @RabbitListener(queues = "fanout.B")
    @RabbitHandler
    public void processB(Map testMessage) {
        System.out.println("FanoutReceiverB消费者收到消息  : " +testMessage.toString());
    }
    
    @RabbitListener(queues = "fanout.C")
    @RabbitHandler
    public void processC(Map testMessage) {
        System.out.println("FanoutReceiverC消费者收到消息  : " +testMessage.toString());
    }
    
}

rabbitMQ的推拉模式 后续更新