RabbitMQ学习笔记

385 阅读18分钟

安装

本次使用Docker安装rabbitmq。

  • rabbitmq暴露端口如下,需要做映射

1645499638181.png

  • 数据卷相关

    • /var/lib/rabbitmq:保存数据(默认挂载的数据卷)
    • /etc/rabbitmq:保存配置
    • /var/log/rabbitmq:保存日志
# 1. 拉取镜像。注意:一定要拉取带management的镜像
[root@aliyunECS ~]# docker pull rabbitmq:3.9.13-management
3.9.13-management: Pulling from library/rabbitmq
08c01a0ec47e: Pull complete 
1ceb23964d6c: Pull complete 
64631f643e2c: Pull complete 
67525726a753: Pull complete 
4b28b554c25b: Pull complete 
3afdc08bc87a: Pull complete 
1e22dd212ab8: Pull complete 
b585ed397156: Pull complete 
68de743a7628: Pull complete 
68b52ebaee48: Pull complete 
Digest: sha256:e2b65c7fa8b09ef99786881925bea3d0465c50dc32663d45c78f8bd077ada648
Status: Downloaded newer image for rabbitmq:3.9.13-management
docker.io/library/rabbitmq:3.9.13-management

# 2. 启动容器
# 注意:
# 	|- 端口映射
#   |- 使用具名挂载
[root@aliyunECS ~]# docker run -id --name=my-mq -p 5672:5672 -p 15672:15672 -p 25672:25672 -p 15692:15692 -v rabbitmq_data:/var/lib/rabbitmq -v rabbitmq_conf:/etc/rabbitmq -v rabbitmq_log:/var/log/rabbitmq  --hostname=myrabbit rabbitmq:3.9.13-management

1a685f59f83a5140117a7ad822b4bbb27e59392047d974fe1c230b370adc0a3d
[root@aliyunECS ~]# docker ps
CONTAINER ID   IMAGE                        COMMAND                  CREATED         STATUS         PORTS                                                                                                                                                 NAMES
1a685f59f83a   rabbitmq:3.9.13-management   "docker-entrypoint.s…"   4 seconds ago   Up 2 seconds   4369/tcp, 5671/tcp, 0.0.0.0:5672->5672/tcp, :::5672->5672/tcp, 15671/tcp, 15691-15692/tcp, 25672/tcp, 0.0.0.0:15672->15672/tcp, :::15672->15672/tcp   my-mq


# 3. 开启防火墙端口5672和15672。如果是阿里云服务器则还需要在安全组打开相应端口
[root@aliyunECS ~]# firewall-cmd --zone=public --add-port=5672/tcp --add-port=15672/tcp --permanent
[root@aliyunECS ~]# systemctl restart firewalld.service

以上步骤做完,在本地输入 weixu.top:15672 访问,使用guest/guest登录,可正常进入管理控制台页面。

如果报错User can only log in via localhost。由于guest用户只能在服务器端本地访问,因此这里考虑新建一个用户做解决方案:

1.首先进入容器 docker exec -it my-mq /bin/bash

2.创建用户 rabbitmqctl add_user admin admin

3.给用户授权角色 rabbitmqctl set_user_tags admin administrator

4.给用户添加权限 rabbitmqctl set_permissions -p / admin "." "." ".*"

参考阅读:python.iitter.com/other/98438…

Rabbitmq架构

1645882583296.png

相关概念解释:

  • Broker:接收和分发消息的应用,RabbitMQ Server就是 Message Broker

  • Virtual host:出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多个vhost,每个用户在自己的 vhost 创建 exchange/queue 等

  • Connection:publisher/consumer 和 broker 之间的 TCP 连接

  • Channel:如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP Connection的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的 channel 进行通讯,AMQP method 包含了channel id 帮助客户端和message broker 识别 channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的 Connection 极大减少了操作系统建立 TCP connection 的开销

  • Exchange:message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到queue 中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast)

  • Queue:消息最终被送到这里等待 consumer 取走

  • Binding:exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key。Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据

Rabbitmq六种模式

1、简单模式

1645881060878.png

特点:

  • 没有交换机(使用默认交换机)
  • 一个发送方一个消费者
  • routing key为队列的名字

2、Work Queue模式

1645881178675.png

与简单模式相比,增加以下特点:

  • 多个消费者监听一个队列。多个消费者之间竞争消费消息

3、Publisher/Subscriber模式(Fanout模式)

1645881266085.png

特点:

  • 有一个交换机,绑定多个队列
  • 没有routing key,即发送到X的消息会直接转发到所有绑定的队列

4、Routing模式(Direct模式)

1645881365853.png

特点:

  • 交换机-routing key-队列 绑定
  • 消息携带的routing key必须完全相同才会被X指派到指定的队列上

5、Topic模式

1645882209048.png

topic模式和routing模式几乎相同,不同点在于:

topic模式的routing key可以使用通配符

通配符规则:

  • #:匹配一个或多个单词
  • *:匹配一个单词

例如,item.#可以匹配item.apple.insert、item.banana.update等消息;item.*可以匹配item.insert、item.update等消息,但不能匹配item.apple.insert消息。

基本代码实现

  • 创建父工程mq-demo
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.kkb</groupId>
    <artifactId>mq-demo</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>

    <modules>
        <module>rabbitmq-demo</module>
        <module>spring-rabbitmq-producer</module>
        <module>spring-rabbitmq-consumer</module>
        <module>springboot-rabbitmq-producer</module>
        <module>springboot-rabbitmq-consumer</module>
    </modules>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <java.version>8</java.version>
        <springcloud.version>2020.0.1</springcloud.version>
        <springcloud-alibaba.version>2021.1</springcloud-alibaba.version>
        <nacos.version>1.4.1</nacos.version>
        <sentinel.version>1.8.0</sentinel.version>
        <rocketMQ.version>4.4.0</rocketMQ.version>
        <dubbo.version>2.7.8</dubbo.version>
        <seata.version>1.3.0</seata.version>
        <swagger.version>2.9.2</swagger.version>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.2</version>
        <relativePath/> <!-- lookup paent from repository -->
    </parent>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${springcloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${springcloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
  • 创建rabbitmq-demo模块,导入以下依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>mq-demo</artifactId>
        <groupId>com.kkb</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>rabbitmq-demo</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

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

</project>

简单模式

  • 编写ConnectionUtil来方便创建连接
package com.kkb.hd.util;

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

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

/**
 * @className: ChannelUtil
 * @description: 产生channel的帮助类
 * @author: HanDing
 * @date: 2022/2/23
 **/
public class ConnectionUtil {
    private static ConnectionFactory connectionFactory = new ConnectionFactory();
    private static final String DEFAULT_HOST = "120.78.228.224";
    private static final int AMQP_PORT = 5672;
    private static final String DEFAULT_VIRTUAL_HOST = "/xzk";
    private static final String DEFAULT_USERNAME = "handing";
    private static final String DEFAULT_PASS = "123456";
    public static Connection getDefaultConnection() throws IOException, TimeoutException {
        connectionFactory.setUsername(DEFAULT_USERNAME);
        connectionFactory.setPassword(DEFAULT_PASS);
        connectionFactory.setHost(DEFAULT_HOST);
        connectionFactory.setPort(AMQP_PORT);
        connectionFactory.setVirtualHost(DEFAULT_VIRTUAL_HOST);
        return connectionFactory.newConnection();
    }

}

  • provider
package com.kkb.hd.simple;

import com.kkb.hd.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

/**
 * @className: Producer
 * @description: TODO 类描述
 * @author: HanDing
 * @date: 2022/2/22
 **/
public class Producer {
    public static final String QUEUE_NAME = "simple_queue";
    public static void main(String[] args) throws Exception{

        //创建连接
        Connection connection = ConnectionUtil.getDefaultConnection();

        //创建频道
        Channel channel = connection.createChannel();

        //声明队列
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);

        //发布消息
        //简单模式:使用默认交换机"",routing key为队列名称
        channel.basicPublish("", QUEUE_NAME, null, "你好,小兔子".getBytes());

        System.out.println("消息发送完成");

        //释放资源
        channel.close();
        connection.close();
    }
}

  • consumer
package com.kkb.hd.simple;

import com.kkb.hd.util.ConnectionUtil;
import com.rabbitmq.client.*;
import com.sun.xml.internal.ws.api.streaming.XMLStreamReaderFactory;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;

/**
 * @className: MyConsumer
 * @description: TODO 类描述
 * @author: HanDing
 * @date: 2022/2/23
 **/
public class MyConsumer {
    public static void main(String[] args) throws Exception {
        //创建连接
        Connection connection = ConnectionUtil.getDefaultConnection();

        Channel channel = connection.createChannel();

        channel.queueDeclare(Producer.QUEUE_NAME, true, false, false, null);

        // autoAck: 接收成功后是否自动向消息队列发送确认。消息队列接受到确认信息后会自动删除消息。如设置为false需要手动发送确认信息
        channel.basicConsume(Producer.QUEUE_NAME, true, new DefaultConsumer(channel) {
           @Override
           public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
               System.out.println("消息id:" + envelope.getDeliveryTag());
               System.out.println("exchange:" + envelope.getExchange());
               System.out.println("routing key" + envelope.getRoutingKey());
               System.out.println("收到消息:" + new String(body, StandardCharsets.UTF_8));
           }
       });

       //不能关闭连接,这里要让消费者一直阻塞等待队列的消息
    }
}

  • MyDefaultConsumer:消息消费的回调类,上面DefaultConsumer匿名内部类抽象出来的一个类
package com.kkb.hd.consumer;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
 * @className: MyConsumer
 * @description: TODO 类描述
 * @author: HanDing
 * @date: 2022/2/23
 **/
public class MyDefaultConsumer extends DefaultConsumer {
    /**
     * Constructs a new instance and records its association to the passed-in channel.
     *
     * @param channel the channel to which this consumer is attached
     */
    public MyDefaultConsumer(Channel channel) {
        super(channel);
    }

    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        System.out.println("消息id:" + envelope.getDeliveryTag());
        System.out.println("exchange:" + envelope.getExchange());
        System.out.println("routing key:" + envelope.getRoutingKey());
        System.out.println("收到消息:" + new String(body, StandardCharsets.UTF_8));
    }
}

Work queue模式

和简单模式类似,只要多创建一个消费者即可。

Publish/Subscribe模式

  • Producer
package com.kkb.hd.fanout;

import com.kkb.hd.util.ConnectionUtil;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.sun.org.apache.xml.internal.resolver.readers.ExtendedXMLCatalogReader;

/**
 * @className: Producer
 * @description: TODO 类描述
 * @author: HanDing
 * @date: 2022/2/22
 **/
public class Producer {
    public static final String QUEUE_NAME_1 = "fanout_queue_1";
    public static final String QUEUE_NAME_2 = "fanout_queue_2";
    public static final String EXCHANGE_NAME = "fanout_exchange";
    public static void main(String[] args) throws Exception{

        //创建连接
        Connection connection = ConnectionUtil.getDefaultConnection();

        //创建频道
        Channel channel = connection.createChannel();

        //声明交换机exchange
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);

        //声明两个队列
        channel.queueDeclare(QUEUE_NAME_1, true, false, false, null);
        channel.queueDeclare(QUEUE_NAME_2, true, false, false, null);

        //将两个队列分别和交换机进行绑定
        //fanout模式交换机将消息广播到所有绑定的queue上,因此不用指定routing queue
        channel.queueBind(QUEUE_NAME_1, EXCHANGE_NAME, "");
        channel.queueBind(QUEUE_NAME_2, EXCHANGE_NAME, "");

        //发布消息
        for(int i = 1; i <=10; i++){
            String s = "你好,小兔子==fanout模式===消息" + i;
            channel.basicPublish(EXCHANGE_NAME, "", null, s.getBytes());
        }


        System.out.println("消息发送完成");

        //释放资源
        channel.close();
        connection.close();
    }
}

  • 消费者1:监听fanout_queue_1
package com.kkb.hd.fanout;

import com.kkb.hd.util.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
 * @className: MyConsumer1
 * @description: 消费queue 1的消息
 * @author: HanDing
 * @date: 2022/2/23
 **/
public class MyConsumer1 {
    public static void main(String[] args) throws Exception {
        //创建连接
        Connection connection = ConnectionUtil.getDefaultConnection();


        Channel channel = connection.createChannel();

        channel.queueDeclare(Producer.QUEUE_NAME_1, true, false, false, null);

        // autoAck: 接收成功后是否自动向消息队列发送确认。消息队列接受到确认信息后会自动删除消息。如设置为false需要手动发送确认信息
        channel.basicConsume(Producer.QUEUE_NAME_1, true, new DefaultConsumer(channel) {
           @Override
           public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
               System.out.println("消息id:" + envelope.getDeliveryTag());
               System.out.println("exchange:" + envelope.getExchange());
               System.out.println("routing key:" + envelope.getRoutingKey());
               System.out.println("收到消息:" + new String(body, StandardCharsets.UTF_8));
           }
       });

       //不能关闭连接,这里要让消费者一直阻塞等待队列的消息
    }
}
  • 消费者2:监听fanout_queue_2
package com.kkb.hd.fanout;

import com.kkb.hd.util.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
 * @className: MyConsumer2
 * @description: 消费queue 2的消息
 * @author: HanDing
 * @date: 2022/2/23
 **/
public class MyConsumer2 {
    public static void main(String[] args) throws Exception {
        //创建连接
        Connection connection = ConnectionUtil.getDefaultConnection();

        Channel channel = connection.createChannel();

        channel.queueDeclare(Producer.QUEUE_NAME_2, true, false, false, null);

        // autoAck: 接收成功后是否自动向消息队列发送确认。消息队列接受到确认信息后会自动删除消息。如设置为false需要手动发送确认信息
        channel.basicConsume(Producer.QUEUE_NAME_2, true, new DefaultConsumer(channel) {
           @Override
           public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
               System.out.println("消息id:" + envelope.getDeliveryTag());
               System.out.println("exchange:" + envelope.getExchange());
               System.out.println("routing key:" + envelope.getRoutingKey());
               System.out.println("收到消息:" + new String(body, StandardCharsets.UTF_8));
           }
       });

       //不能关闭连接,这里要让消费者一直阻塞等待队列的消息
    }
}

Routing模式

上述把Exchange创建类型修改为DIRECT即可:

  • provider
package com.kkb.hd.direct;

import com.kkb.hd.util.ConnectionUtil;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

import java.nio.charset.StandardCharsets;

/**
 * @className: Producer
 * @description: TODO 类描述
 * @author: HanDing
 * @date: 2022/2/22
 **/
public class Producer {
    public static final String QUEUE_UPDATE = "update_queue";
    public static final String QUEUE_INSERT = "insert_queue";
    public static final String DIRECT_EXCHANGE = "direct_exchange";
    public static void main(String[] args) throws Exception{

        //创建连接
        Connection connection = ConnectionUtil.getDefaultConnection();

        //创建频道
        Channel channel = connection.createChannel();

        //声明交换机exchange
        channel.exchangeDeclare(DIRECT_EXCHANGE, BuiltinExchangeType.DIRECT);

        //声明两个队列
        channel.queueDeclare(QUEUE_INSERT, true, false, false, null);
        channel.queueDeclare(QUEUE_UPDATE, true, false, false, null);

        //将两个队列分别和交换机进行绑定
        //fanout模式交换机将消息广播到所有绑定的queue上,因此不用指定routing queue
        channel.queueBind(QUEUE_INSERT, DIRECT_EXCHANGE, "insert");
        channel.queueBind(QUEUE_UPDATE, DIRECT_EXCHANGE, "update");

        //发布消息 -> insert-queue
        channel.basicPublish(DIRECT_EXCHANGE, "insert", null, "新增商品完成".getBytes(StandardCharsets.UTF_8));

        channel.basicPublish(DIRECT_EXCHANGE, "update", null, "商品更新完成".getBytes(StandardCharsets.UTF_8));

        System.out.println("消息发送完成");

        //释放资源
        channel.close();
        connection.close();
    }
}

  • InsertConsumer:监听insert_queue的消息
package com.kkb.hd.direct;

import com.kkb.hd.consumer.MyDefaultConsumer;
import com.kkb.hd.util.ConnectionUtil;
import com.rabbitmq.client.*;

/**
 * @className: MyConsumer1
 * @description: 消费queue 1的消息
 * @author: HanDing
 * @date: 2022/2/23
 **/
public class InsertConsumer {
    public static void main(String[] args) throws Exception {
        //创建连接
        Connection connection = ConnectionUtil.getDefaultConnection();

        
        Channel channel = connection.createChannel();

        channel.queueDeclare(Producer.QUEUE_INSERT, true, false, false, null);

        // autoAck: 接收成功后是否自动向消息队列发送确认。消息队列接受到确认信息后会自动删除消息。如设置为false需要手动发送确认信息
        channel.basicConsume(Producer.QUEUE_INSERT, true, new MyDefaultConsumer(channel));

       //不能关闭连接,这里要让消费者一直阻塞等待队列的消息
    }
}

  • UpdateConsumer:监听update_queue的消息
package com.kkb.hd.direct;

import com.kkb.hd.consumer.MyDefaultConsumer;
import com.kkb.hd.util.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
 * @className: MyConsumer2
 * @description: 消费queue 2的消息
 * @author: HanDing
 * @date: 2022/2/23
 **/
public class UpdateConsumer {
    public static void main(String[] args) throws Exception {
        //创建连接
        Connection connection = ConnectionUtil.getDefaultConnection();

        Channel channel = connection.createChannel();

        channel.queueDeclare(Producer.QUEUE_UPDATE, true, false, false, null);

        // autoAck: 接收成功后是否自动向消息队列发送确认。消息队列接受到确认信息后会自动删除消息。如设置为false需要手动发送确认信息
        channel.basicConsume(Producer.QUEUE_UPDATE, true, new MyDefaultConsumer(channel));

       //不能关闭连接,这里要让消费者一直阻塞等待队列的消息
    }
}

Topic模式

routing key可以使用通配符==#==或者==*== 。

  • Producer:发送三个消息,分别为item.insert、item.update、item.delete
package com.kkb.hd.topic;

import com.kkb.hd.util.ConnectionUtil;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

/**
 * @className: Producer
 * @description: TODO 类描述
 * @author: HanDing
 * @date: 2022/2/22
 **/
public class Producer {
    public static final String INSERT_UPDATE_QUEUE = "insert_update_queue"; //可以接收item.insert和item.update路由消息
    public static final String QUEUE_ALL = "queue_all"; //可以接收item.*的路由消息
    public static final String TOPIC_EXCHANGE = "topic_exchange";
    public static void main(String[] args) throws Exception{

        //创建连接
        Connection connection = ConnectionUtil.getDefaultConnection();

        //创建频道
        Channel channel = connection.createChannel();

        //声明交换机exchange
        channel.exchangeDeclare(TOPIC_EXCHANGE, BuiltinExchangeType.TOPIC);

        //声明两个队列
        channel.queueDeclare(INSERT_UPDATE_QUEUE, true, false, false, null);
        channel.queueDeclare(QUEUE_ALL, true, false, false, null);

        //将两个队列分别和交换机进行绑定
        //fanout模式交换机将消息广播到所有绑定的queue上,因此不用指定routing queue
        channel.queueBind(QUEUE_ALL, TOPIC_EXCHANGE, "item.*");
        channel.queueBind(INSERT_UPDATE_QUEUE, TOPIC_EXCHANGE, "item.insert");
        channel.queueBind(INSERT_UPDATE_QUEUE, TOPIC_EXCHANGE, "item.update");

        //添加商品
        String s = "增加商品item.insert";
        channel.basicPublish(TOPIC_EXCHANGE, "item.insert", null, s.getBytes());

        //更新商品
        s = "更新商品item.update";
        channel.basicPublish(TOPIC_EXCHANGE, "item.update", null, s.getBytes());

        //删除商品
        s = "删除商品item.delete";
        channel.basicPublish(TOPIC_EXCHANGE, "item.delete", null, s.getBytes());

        System.out.println("消息发送完成");

        //释放资源
        channel.close();
        connection.close();
    }
}
  • 消费者1:监听insert_update_queue队列,可以消费到两条消息item.insert和item.update
package com.kkb.hd.topic;

import com.kkb.hd.consumer.MyDefaultConsumer;
import com.kkb.hd.util.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
 * @className: MyConsumer1
 * @description: 消费queue 1的消息
 * @author: HanDing
 * @date: 2022/2/23
 **/
public class MyConsumer1 {
    public static void main(String[] args) throws Exception {
        //创建连接
        Connection connection = ConnectionUtil.getDefaultConnection();

        Channel channel = connection.createChannel();

        channel.queueDeclare(Producer.INSERT_UPDATE_QUEUE, true, false, false, null);

        // autoAck: 接收成功后是否自动向消息队列发送确认。消息队列接受到确认信息后会自动删除消息。如设置为false需要手动发送确认信息
        channel.basicConsume(Producer.INSERT_UPDATE_QUEUE, true, new MyDefaultConsumer(channel));

       //不能关闭连接,这里要让消费者一直阻塞等待队列的消息
    }
}

  • 消费者2:监听queue_all队列,可以消费到所有三条消息
package com.kkb.hd.topic;

import com.kkb.hd.util.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
 * @className: MyConsumer2
 * @description: 消费queue 2的消息
 * @author: HanDing
 * @date: 2022/2/23
 **/
public class MyConsumer2 {
    public static void main(String[] args) throws Exception {
        //创建连接
        Connection connection = ConnectionUtil.getDefaultConnection();

        Channel channel = connection.createChannel();

        channel.queueDeclare(Producer.QUEUE_ALL, true, false, false, null);

        // autoAck: 接收成功后是否自动向消息队列发送确认。消息队列接受到确认信息后会自动删除消息。如设置为false需要手动发送确认信息
        channel.basicConsume(Producer.QUEUE_ALL, true, new DefaultConsumer(channel) {
           @Override
           public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
               System.out.println("消息id:" + envelope.getDeliveryTag());
               System.out.println("exchange:" + envelope.getExchange());
               System.out.println("routing key:" + envelope.getRoutingKey());
               System.out.println("收到消息:" + new String(body, StandardCharsets.UTF_8));
           }
       });

       //不能关闭连接,这里要让消费者一直阻塞等待队列的消息
    }
}

整合spring

这里使用topic模式做示例。

producer模块

  • 创建spring-rabbitmq-producer模块
  • 导入依赖(spring-rabbit)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>mq-demo</artifactId>
        <groupId>com.kkb</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>rabbitmq-spring</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
        </dependency>
    </dependencies>

</project>

注意:这里应该使用继承的依赖,不要自己指定版本号,容易出现版本冲突

rabbitmq配置文件

  • rabbitmq.properties
rabbit.host=120.78.228.224
rabbit.port=5672
rabbit.virtual-host=/xzk
rabbit.username=handing
rabbit.password=123456

xml文件声明连接工厂、队列、交换机、rabbitTemplate

rabbit.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/rabbit
       http://www.springframework.org/schema/rabbit/spring-rabbit.xsd
">
    <context:property-placeholder location="classpath:rabbitmq.properties"/>

    <!--创建连接工厂-->
    <rabbit:connection-factory id="connectionFactory"
                               host="${rabbit.host}"
                               port="${rabbit.port}"
                               username="${rabbit.username}"
                               password="${rabbit.password}"
                               virtual-host="${rabbit.virtual-host}"
    />

    <!--声明队列和交换机-->
    <rabbit:admin id="admin" connection-factory="connectionFactory" />

    <rabbit:queue id="spring-simple-queue" name="spring-simple-queue" auto-declare="true" durable="true" />
    <rabbit:queue id="spring-work-queue-1" name="spring-work-queue-1" auto-declare="true" durable="true" />
    <rabbit:queue id="spring-work-queue-2" name="spring-work-queue-2" auto-declare="true" durable="true" />
    <rabbit:queue id="spring-direct-queue" name="spring-direct-queue" auto-declare="true" durable="true" />
    <rabbit:queue id="spring-update-queue" name="spring-update-queue" auto-declare="true" durable="true" />
    <rabbit:queue id="spring-insert-queue" name="spring-insert-queue" auto-declare="true" durable="true" />
    <rabbit:queue id="spring-topic-queue-star" name="spring-topic-queue-star" auto-declare="true" durable="true" />
    <rabbit:queue id="spring-topic-queue-well" name="spring-topic-queue-well" auto-declare="true" durable="true" />

    <rabbit:fanout-exchange id="spring-fanout-exchange" name="spring-fanout-exchange" durable="true" auto-declare="true" >
        <rabbit:bindings>
            <rabbit:binding queue="spring-work-queue-1" />
            <rabbit:binding queue="spring-work-queue-2" />
        </rabbit:bindings>
    </rabbit:fanout-exchange>

    <rabbit:direct-exchange id="spring-direct-exchange" name="spring-direct-exchange" auto-declare="true" durable="true" >
        <rabbit:bindings>
            <rabbit:binding queue="spring-direct-queue" key="direct"></rabbit:binding>
        </rabbit:bindings>
    </rabbit:direct-exchange>

    <rabbit:topic-exchange id="spring-topic-exchange" name="spring-topic-exchange" durable="true" auto-declare="true">
        <rabbit:bindings>
            <rabbit:binding pattern="hd.*" queue="spring-topic-queue-star" />
            <rabbit:binding pattern="hd.#" queue="spring-topic-queue-well" />
        </rabbit:bindings>
    </rabbit:topic-exchange>

    <rabbit:template id="rabbitTemplate" connection-factory="connectionFactory" />

</beans>

发送消息:rabbitTemplate.convertAndSend

package com.kkb.hd;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 * @className: Test
 * @description: TODO 类描述
 * @author: HanDing
 * @date: 2022/2/24
 **/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:rabbit.xml")
public class RabbitProviderTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSimple(){
        rabbitTemplate.convertAndSend("","spring-simple-queue", "simple模式发送消息");
    }

    @Test
    public void testDirectExchange(){
        rabbitTemplate.convertAndSend("spring-direct-exchange","direct", "direct模式发送消息");
    }

    @Test
    public void testDirectExchangeMore(){
        for(int i = 0; i < 100; i++) {
            rabbitTemplate.convertAndSend("spring-direct-exchange", "direct", "direct模式发送消息----- " + i);
        }
    }

    @Test
    public void testFanoutExchange(){
        rabbitTemplate.convertAndSend("spring-fanout-exchange", "", "fanout消息");
    }

    @Test
    public void testTopicExchange(){
        rabbitTemplate.convertAndSend("spring-topic-exchange", "hd.color", "hd.color消息");
        rabbitTemplate.convertAndSend("spring-topic-exchange", "hd.mos.hello", "hd.mos.hello消息");

    }

}

consumer模块

  • 创建spring-rabbitmq-consumer模块
  • 导入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>mq-demo</artifactId>
        <groupId>com.kkb</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>spring-rabbitmq-consumer</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
        </dependency>
    </dependencies>
</project>

rabbitmq配置文件

  • properties文件和producer里的一致,直接拷贝即可
  • rabbit.xml文件:主要声明监听器<rabbitmq:listener>以及其监听的队列
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/rabbit
       http://www.springframework.org/schema/rabbit/spring-rabbit.xsd
">
    <context:property-placeholder location="classpath:rabbitmq.properties"/>

    <!--创建连接工厂-->
    <rabbit:connection-factory id="connectionFactory"
                               host="${rabbit.host}"
                               port="${rabbit.port}"
                               username="${rabbit.username}"
                               password="${rabbit.password}"
                               virtual-host="${rabbit.virtual-host}"
    />

    <!--声明队列和交换机-->
    <rabbit:admin id="admin" connection-factory="connectionFactory" />

    <bean id="simple-listener" class="com.kkb.hd.listener.SimpleListener" />
    <bean id="insert-listener" class="com.kkb.hd.listener.InsertListener" />
    <bean id="update-listener" class="com.kkb.hd.listener.UpdateListener" />
    <bean id="topic-listener-star" class="com.kkb.hd.listener.TopicListenerStar" />
    <bean id="topic-listener-well" class="com.kkb.hd.listener.TopicListenerWell" />
    <bean id="work-listener-1" class="com.kkb.hd.listener.WorkListener1" />
    <bean id="work-listener-2" class="com.kkb.hd.listener.WorkListener2" />

    <rabbit:listener-container connection-factory="connectionFactory">
        <rabbit:listener ref="simple-listener" queue-names="spring-simple-queue" />
        <rabbit:listener ref="insert-listener" queue-names="spring-insert-queue" />
        <rabbit:listener ref="update-listener" queue-names="spring-update-queue" />
        <rabbit:listener ref="topic-listener-star" queue-names="spring-topic-queue-star" />
        <rabbit:listener ref="topic-listener-well" queue-names="spring-topic-queue-well" />
        <rabbit:listener ref="work-listener-1" queue-names="spring-work-queue-1" />
        <rabbit:listener ref="work-listener-2" queue-names="spring-work-queue-2" />
    </rabbit:listener-container>

    <rabbit:template id="rabbitTemplate" connection-factory="connectionFactory" />

</beans>

定义监听器:实现MessageListener端口

package com.kkb.hd.listener;

import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageListener;
import org.springframework.amqp.core.MessageProperties;

import java.nio.charset.StandardCharsets;

/**
 * @className: SimpleListener
 * @description: TODO 类描述
 * @author: HanDing
 * @date: 2022/2/25
 **/
public class SimpleListener implements MessageListener {
    @Override
    public void onMessage(Message message) {
        MessageProperties messageProperties = message.getMessageProperties();
        System.out.println("接收到的消息:" + new String(message.getBody(), StandardCharsets.UTF_8));
        System.out.println("交换机:" + messageProperties.getReceivedExchange());
        System.out.println("队列:" + messageProperties.getConsumerQueue());
        System.out.println("Routing key:" + messageProperties.getReceivedRoutingKey());
    }
}

其余监听器均是SimpleListener的子类,类内容为空,这里省略。

public class InsertListener extends SimpleListener{
}

测试类

package com.kkb.hd;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 * @className: ConsumerTest
 * @description: TODO 类描述
 * @author: HanDing
 * @date: 2022/2/25
 **/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:rabbit.xml")
public class ConsumerTest {
    @Test
    public void test(){
        while(true){

        }
    }

}

测试

  • 首先在producer模块里发送消息
  • 其次启动consumer模块测试

注意:先启动生产者还是消费者理论上没有差别。但实际情况下应该==先启动有绑定交换机和队列的模块==。没有绑定的话生产者发送的消息因为找不到队列会被自动丢弃(因为exchange不保存消息只转发)

整合springboot

springboot最大的优势:省略复杂的xml配置,使用全注解配置。

为了更好的复习回顾之前所学习的nacos配置中心,这里我将生产者和消费者模块的配置全部放到远程nacos上了。

producer模块

  • 创建springboot-rabbitmq-producer模块
  • 导入配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>mq-demo</artifactId>
        <groupId>com.kkb</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springboot-rabbitmq-producer</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>


    <dependencies>

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

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

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
		
        <!--使用bootstrap连接配置中心时务必导入此依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>

    </dependencies>

</project>

小坑:因为有一段时间没写nacos了,一开始编写的时候犯了一个错误,没有导入spring-cloud-starter-bootstrap依赖。这样一来bootstrap.yml中所定义的配置中心会失效,所有配置都使用默认的,比如rabbitmq连接到了默认的localhost:5672。因此务必导入==spring-cloud-starter-bootstrap==这个依赖。

另一方面,如果导入了此依赖但还是没有连接到配置中心,则检查一下==版本冲突==。

bootstrap.yml

spring:
  application:
    name: rabbitmq-producer
  profiles:
    active: dev
  cloud:
    nacos:
      config:
        server-addr: 120.78.228.224:1111
        file-extension: yaml
        extension-configs[0]:
          data-id: common.yaml
          group: DEFAULT_GROUP
          refresh: true

rabbitmq-producer-dev.yaml

spring:
  rabbitmq:
    host: 120.78.228.224 
    port: 5672
    virtual-host: /xzk
    username: handing
    password: 123456
    publisher-confirm-type: correlated # 生产者消息确认机制
    publisher-returns: true # 生产者消息返回机制
    template:
      mandatory: true 

配置config

做三件事:==创建队列、创建交换机、两者绑定==

  • RabbitMQNormalConfig
package com.kkb.hd.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @className: RabbitMQNormalConfig
 * @description: TODO 类描述
 * @author: HanDing
 * @date: 2022/2/25
 **/
@Configuration
public class RabbitMQNormalConfig {

    public static final String ITEM_TOPIC_EXCHANGE = "item-topic-exchange";
    public static final String ITEM_TOPIC_QUEUE = "item-topic-queue";


    @Bean
    public Exchange itemTopicExchange(){
        return ExchangeBuilder.topicExchange(ITEM_TOPIC_EXCHANGE).durable(true).build();
    }

    @Bean
    public Queue itemTopicQueue(){
        return QueueBuilder.durable(ITEM_TOPIC_QUEUE).build();
    }

    @Bean
    public Binding itemBinding(@Qualifier("itemTopicQueue") Queue queue, @Qualifier("itemTopicExchange") Exchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with("item.#").noargs();
    }

}

消息发送测试

package com.kkb.hd;

import com.kkb.hd.config.RabbitMQDlxConfig;
import com.kkb.hd.config.RabbitMQNormalConfig;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;


/**
 * @className: ProducerApplicationTest
 * @description: TODO 类描述
 * @author: HanDing
 * @date: 2022/2/25
 **/

@RunWith(SpringRunner.class)
@SpringBootTest
public class ProducerApplicationTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void sendNormalMessage(){
        rabbitTemplate.convertAndSend(RabbitMQNormalConfig.ITEM_TOPIC_EXCHANGE, "item.insert", "新增商品");
        rabbitTemplate.convertAndSend(RabbitMQNormalConfig.ITEM_TOPIC_EXCHANGE, "item.update", "更新商品");
        rabbitTemplate.convertAndSend(RabbitMQNormalConfig.ITEM_TOPIC_EXCHANGE, "item.delete", "删除商品");
        System.out.println("消息发送完毕!");
    }
}

consumer模块

bootstrap.yml

spring:
  application:
    name: rabbitmq-consumer
  profiles:
    active: dev

  cloud:
    nacos:
      config:
        server-addr: 120.78.228.224:1111
        file-extension: yaml
        extension-configs:
          - dataId: rabbitmq-producer-dev.yaml
            group: DEFAULT_GROUP
            refresh: true

创建listener

package com.kkb.hd.listener;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;

/**
 * @className: MyListener
 * @description: TODO 类描述
 * @author: HanDing
 * @date: 2022/2/25
 **/
@Component
public class MyListener {

    @RabbitListener(queues = "item-topic-queue")
    public void listener1(String message, Channel channel){
        System.out.println("监听到的消息:" + message);
    }
}

测试

  • producer使用测试类发送消息
  • consumer启动Application接收消息

高级特性

可靠性投递

在使用 RabbitMQ 的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景。RabbitMQ 为我们提供了两种方式用来控制消息的投递可靠性模式。

  • confirm 确认模式
  • return 退回模式

rabbitmq 整个消息投递的路径为: producer--->rabbitmq broker--->exchange--->queue--->consumer

  • 消息从 producer 到 exchange 则会返回一个 confirmCallback 。
  • 消息从 exchange-->queue 投递失败则会返回一个 returnCallback 。

我们将利用这两个 callback 控制消息的可靠性投递。

ConfirmCallback

  • 在生产者配置文件中增加配置:
spring:
	rabbitmq:
		publisher-confirm-type: correlated
  • rabbitTemplate消息发送时设置ConfirmCallback参数
	@Test
    public void testConfirm(){
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            System.out.println("执行confirmCallback方法");
            if(ack){
                System.out.println("消息成功发送");
            }else{
                System.out.println("消息发送失败,原因" + cause);
            }
        });
        //测试发送到一个不存在的交换机
        rabbitTemplate.convertAndSend("xxxxx", "item.insert", "新增商品");
    }

ReturnCallback

  • 在生产者中设置回退模式:
spring:
	rabbitmq:
		publisher-returns: true
  • rabbitTemplate需要设置setMandatory(true)

    • false:如果消息没有路由到Queue,则丢弃消息(默认)

    • true:如果消息没有路由到Queue,返回给消息发送方ReturnCallBack
  • rabbitTemplate设置setReturnsCallback参数

	@Test
    public void testReturn(){
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setReturnsCallback(returnedMessage -> {
            System.out.println("return执行了...");
            String exchange = returnedMessage.getExchange();
            String routingKey = returnedMessage.getRoutingKey();
            //String queue = returnedMessage.getMessage().getMessageProperties().getConsumerQueue();
            System.out.println("消息从" + exchange + "到路由key为" + routingKey);
            System.out.println("消息为:" + new String(returnedMessage.getMessage().getBody(), StandardCharsets.UTF_8));
        });

        //测试发送一个不存在的routing key
        rabbitTemplate.convertAndSend("spring-direct-exchange", "kkkk", "新增商品" );
        try {
            TimeUnit.SECONDS.sleep(1L); //延时一秒从而等待exchange消息发送线程回调函数执行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

也可以在配置文件中设置 spring.rabbitmq.template.mandatory=true

注意:如果测试的时候ReturnCallback没有执行,参考这篇博客:blog.csdn.net/u013165358/…

Consumer ack

消费端确认。

消费者默认收到消息后就自动向queue发送一个ack,表示收到消息,queue收到ack之后删除消息。

但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失。如果设置了手动确认方式,则需要在业务处理成功后,调用==channel.basicAck()==,手动签收,如果出现异常,则调用==channel.basicNack()==方法,让其自动重新发送消息。

三种确认方式:

  • 自动确认:acknowledge="none"
  • 手动确认:acknowledge="manual"
  • 根据异常情况确认:acknowledge="auto",(这种方式使用麻烦,不作讲解)

消费者配置

开启手动ack

  • spring:在<rabbit:listener-container>标签中设置
    <rabbit:listener-container connection-factory="connectionFactory" acknowledge="manual">
        <rabbit:listener ref="simple-listener" queue-names="spring-simple-queue" />
    </rabbit:listener-container>
  • springboot中的配置:
spring:
	rabbitmq:
        listener:
        	type: simple
        	simple:
                acknowledge-mode: manual
                prefetch: 1 #消费者限流配置:每个线程每次只取一个消息
                concurrency: 3 #启动三个消费线程接收消息
          

消息发送后手动ack

  • spring方式

实现ChannelAwareMessageListener接口,里面的方法含有Channel参数,可以直接使用channel.basicAck或者channel.basicNack。

public class AnotherListener implements ChannelAwareMessageListener {
    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        //参考springboot方式
    }
}
  • springboot方式

在MyListener中添加如下方法

@RabbitListener(queues = "spring-direct-queue")
    public void listener2(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            String msg = new String(message.getBody(), StandardCharsets.UTF_8);
            int i = 3 / 0; //模拟消息消费失败的情况
            System.out.println("消息接收成功,为:" + msg);
            channel.basicAck(deliveryTag, true);
        }catch(Exception e){
            e.printStackTrace();
            System.out.println("后续业务操作失败,消息回退到queue");
            channel.basicNack(deliveryTag, true, true);
        }
    }

消费端限流

1645888428446.png

在MyListener中修改listener2方法:

@RabbitListener(queues = "spring-direct-queue")
    public void listener2(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            // 在yml中定义了三个消费线程,每个线程每次取一个消息。这里设置每个线程有1秒延时。因此最后效果为每1秒消费3个消息
            TimeUnit.SECONDS.sleep(1L); //模拟测试消费端限流:每个线程每秒取一个消息
            String msg = new String(message.getBody(), StandardCharsets.UTF_8);
            System.out.println("消息接收成功,为:" + msg);
            channel.basicAck(deliveryTag, true);
        }catch(Exception e){
            e.printStackTrace();
            System.out.println("后续业务操作失败,消息回退到queue");
            channel.basicNack(deliveryTag, true, true);
        }
    }

TTL

队列可以设置TTL,从而使得在TTL之后所有消息丢弃。

队列配置TTL

	@Bean
    public Queue itemTopicQueue(){
        // 声明一个具有消息过期时间的队列:过期时间为10秒
        return QueueBuilder.durable(ITEM_TOPIC_QUEUE).ttl(10000).build();
    }

消息配置TTL

	@Test
    public void sendNormalMessage(){
        rabbitTemplate.convertAndSend(RabbitMQNormalConfig.ITEM_TOPIC_EXCHANGE, "item.insert", "新增商品", new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                message.getMessageProperties().setExpiration("5000"); //单独设置消息的过期时间为5秒
                // 注意:这种具有过期时间的消息必须在消息队列的最上面(最先发送的),否则会不生效
                return message;
            }
        });
        rabbitTemplate.convertAndSend(RabbitMQNormalConfig.ITEM_TOPIC_EXCHANGE, "item.update", "更新商品");
        rabbitTemplate.convertAndSend(RabbitMQNormalConfig.ITEM_TOPIC_EXCHANGE, "item.delete", "删除商品");
        System.out.println("消息发送完毕!");
    }

注意:

  • 消息和队列都配置TTL时,以短的为准
  • 消息如果配置了TTL,则它必须是最前面的消息(第一个发送的)才会生效

死信队列/死信交换机

死信队列,英文缩写:DLX 。Dead Letter Exchange(死信交换机),当消息成为Dead message后,可以被重新发送到另一个交换机,这个交换机就是DLX。

1645888922418.png

消息成为死信的几种情况:

  • 消息超时(TTL)
  • 消息数量超过了队列的容量上限
  • 消息没有被消费端正常消费(消费端返回Nack)

注意:如果要测试第三种情况,消费者返回Nack这种情况,需要在basicNack中设置不重回队列 requeue=false channel.basicNack(deliveryTag,true,false);

生产者代码:

package com.kkb.hd.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @className: RabbitMQDlxConfig
 * @description: 死信队列测试
 * @author: HanDing
 * @date: 2022/2/25
 **/
@Configuration
public class RabbitMQDlxConfig {

    public static final String TEST_DLX_EXCHANGE = "test-dlx-exchange"; //测试交换机
    public static final String DLX_EXCHANGE = "dlx-exchange"; //死信交换机
    public static final String TEST_DLX_QUEUE = "test-dlx-queue"; //测试队列
    public static final String DLX_QUEUE = "dlx-queue"; //死信队列
    
    //首先创建:1、test交换机 2、test队列 3、绑定
    @Bean
    public Exchange testDlxExchange(){
        return ExchangeBuilder.topicExchange(TEST_DLX_EXCHANGE).durable(true).build();
    }

    @Bean
    public Queue testDlxQueue(){
        return QueueBuilder.durable(TEST_DLX_QUEUE)
                .ttl(10000) //设置队列的超时时间:超时后所有消息变为死信
                .maxLength(10) //设置队列的最大消息数量:超过的消息直接为死信
                .deadLetterExchange(DLX_EXCHANGE) //绑定死信交换机:死信消息直接发送到该交换机上
                .deadLetterRoutingKey("dlx.abc") //发送到死信交换机时使用的routingKey
                .build();
    }

    @Bean
    public Binding testBinding(@Qualifier("testDlxQueue") Queue queue, @Qualifier("testDlxExchange") Exchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with("test.dlx.#").noargs();
    }


    //其次创建: 1、dlx交换机  2、dlx队列  3、绑定
    @Bean
    public Exchange dlxExchange(){
        return ExchangeBuilder.topicExchange(DLX_EXCHANGE).durable(true).build();
    }

    @Bean
    public Queue dlxQueue(){
        return QueueBuilder.durable(DLX_QUEUE).build();
    }

    @Bean
    public Binding bindDlx(@Qualifier("dlxExchange") Exchange exchange, @Qualifier("dlxQueue") Queue queue){
        return BindingBuilder.bind(queue).to(exchange).with("dlx.#").noargs();
    }

}

消费端略。

延迟队列

延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。

需求:

  1. 下单后,30分钟未支付,取消订单,回滚库存。
  2. 新用户注册成功7天后,发送短信问候。

实现方式:

  1. 定时器
  2. 延迟队列

Rabbitmq中没有延迟队列,使用TTL+死信队列间接实现。

1645889720110.png

  • TTL:超时时间
  • 死信队列:超时之后消息发送到的地方。死信队列由处理超时消息的监听器处理。

具体实现和死信队列的代码类似,这里省略。