RabbitMQ的使用

消息队列

1.MQ相关概念

1.1 什么是MQ

MQ(message queue),从字面意思上看,本质是个队列,FIFO 先入先出,只不过队列中存放的内容是

message 而已,还是一种跨进程的通信机制,用于上下游传递消息

1.2 为什么使用MQ

流量削峰

  • 处理能力有限,如果只能处理1W请求,1W的之内都能即时响应,现在由于活动2W用户请求不能放弃掉1W用户,所以消息可以到消息队列,只不过处理用户下单的时间比之前长

流量削峰

应用解耦

应用解耦

异步处理

  • 有些服务间调用是异步的
  • 例如 A 调用 B,B 需要花费很长时间执行,但是 A 需要知道 B 什么时候可以执行完,
  • 以前一般有两种方式:
    • A 过一段时间去调用 B 的查询 api 查询
    • 或者 A 提供一个 callback api,B 执行完之后调用 api 通知 A 服务
  • 这两种方式都不是很优雅,使用消息总线,可以很方便解决这个问题
    • A 调用 B 服务后,只需要监听 B 处理完成的消息
    • 当 B 处理完成后,会发送一条消息给 MQ,MQ 会将此消息转发给 A 服务
      异步处理

1.3 常用MQ优缺点

ActiveMQ

  • 优点:单机吞吐量万级,时效性 ms 级,可用性高,基于主从架构实现高可用性,消息可靠性较低的概率丢失数据

  • 缺点:官方社区现在对 ActiveMQ 5.x 维护越来越少,高吞吐量场景较少使用

Kafka

  • 优点
    • 性能卓越,单机写入 TPS 约在百万条/秒,最大的优点,就是吞吐量高
    • 时效性 ms 级可用性非常高
    • kafka 是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用
    • 消费者采 用 Pull 方式获取消息, 消息有序, 通过控制能够保证所有消息被消费且仅被消费一次
    • 有优秀的第三方Kafka Web 管理界面 Kafka-Manager
    • 在日志领域比较成熟,被多家公司和多个开源项目使用
  • 缺点
  • 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用
  • 社区更新较慢

RocketMQ

  • RocketMQ 出自阿里巴巴的开源产品,用 Java 语言实现,在设计时参考了 Kafka,并做出了自己的一 些改进。被阿里巴巴广泛应用在订单,交易,充值,流计算,消息推送,日志流式处理,binglog 分发等场景
  • 优点
    • 单机吞吐量十万级,可用性非常高
    • 分布式架构,消息可以做到 0 丢失
    • MQ 功能较为完善,还是分布式的,扩展性好
    • 支持 10 亿级别的消息堆积,不会因为堆积导致性能下降,源码是 java 我们可以自己阅读源码
  • 缺点
    • 支持的客户端语言不多,目前是 java 及 c++,其中 c++不成熟
    • 社区活跃度一般,没有在 MQ 核心中去实现 JMS 等接口,有些系统要迁移需要修改大量代码

RabbitMQ

  • 2007 年发布,是一个在 AMQP(高级消息队列协议)基础上完成的,可复用的企业消息系统,是当前最主流的消息中间件之一
  • 优点:
    • 由于 erlang 语言的高并发特性,性能较好;吞吐量到万级
    • MQ 功能比较完备,健壮、稳定、易 用、跨平台、支持多种语言 如:
      • Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP 等
    • 支持 AJAX 文档齐全;开源提供的管理界面非常棒,用起来很好用,社区活跃度高;更新频率相当高
  • 缺点
    • 商业版需要收费,学习成本较高

1.4 MQ的选择

大型项目:RocketMQ

日志收集(就是跟大数据量打交道):kafka

小/中型项目:RabbitMQ

社区活跃度:

RabbitMQ > RocketMQ > kafka

消息持久化:

RabbitMQ、RocketMQ、kafka都支持持久化

高并发:

kafka = RocketMQ > RabbitMQ

吞吐量:

kafka = RocketMQ > RabbitMQ

2.RabbitMQ

RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)

2.1 前置概念

2.1.1 AMQP核心概念

AMQP核心概念

虚拟主机(virtual host)或(vhost
  • 一组交换机、队列和绑定器被称为 虚拟主机(vhost
  • RabbitMQ server 可以说就是一个消息队列服务器实体(Broker
  • Broker当中可以有多个用户,而用户只能在虚拟主机的粒度进行权限控制,所以RabbitMQ中需要多个虚拟主机
  • 每一个RabbitMQ服务器都有一个默认的虚拟主机
交换机(exchange
  • 指定消息按什么规则,路由到哪个队列。它可以被理解成具有路由表的路由程序。(发送消息的实体
  • 交换机可以存在多个,每个交换机在自己独立的进程当中执行,因此增加多个交换机就是增加多个进程,可以充分利用服务器上的CPU核以便达到更高的效率。
  • Exchange的类型
  • Direct:直接交换机通过消息上的路由键直接对消息进行分发,相当于精确匹配,一对一
  • Topic:这个交换机会将路由键和绑定上的模式进行通配符匹配,相当于模糊匹配,一对多
  • Fanout:交换机会将消息发送到所有和它进行绑定的队列上,广播,群发
  • Headers:消息头交换机使用消息头的属性进行消息路由,相当于模糊匹配(like header%),一对多
队列(queue
  • 队列是消息载体,每个消息都会被投入到一个或多个队列
  • 试图创建一个已经存在的队列,RabbitMQ会直接忽略这个请求(接收消息的实体)。
绑定器(bind
  • 作用:把exchange和queue按照路由规则绑定起来
  • 将交换器和队列连接起来,并且封装消息的路由信息

2.1.2 程序中连接与消息使用的两个关键概念

连接(Connection)
  • 与RabbitMQ Server建立的一个连接
  • 由ConnectionFactory创建
  • 每个connection只与一个物理的Server进行连接,此连接是基于Socket进行连接的
  • AMQP一般使用TCP
通道 (Channel)
  • 消息通道(主要进行相关定义,发送消息,获取消息,事务处理等)
  • 在客户端的每个连接里,可建立多个channel,每个channel代表一个会话任务
    RabbitMQ工作原理

2.2 七种工作模式

七种工作模式

  • 简单模式:一个生产者,一个消费者
  • work模式:一个生产者,多个消费者,每个消费者获取到的消息唯一
  • 订阅模式:一个生产者发送的消息会被多个消费者获取
  • 路由模式: 发送消息到交换机并且要指定路由key ,消费者将队列绑定到交换机时需要指定路由key
  • topic模式:将路由键和某模式进行匹配,此时队列需要绑定在一个模式上,“#”匹配一个词或多个词,“*”只匹配一个词
  • RPC模式:使用RabbitMQ构建RPC系统:客户端和可伸缩RPC服务器
  • 发布确认:与发布者进行可靠的发布确认

2.3 安装

# 1.拉取镜像
[root@izbp1j6sv2mtxgnuthuw6bz ~]# docker pull rabbitmq:3.8-management
# 2.查看镜像
[root@izbp1j6sv2mtxgnuthuw6bz ~]# docker images
# 3.启动
[root@izbp1j6sv2mtxgnuthuw6bz ~]# docker run -itd --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3.8-management
a5ca877169d4eda31a9c404f94565c118f1cc64de2cbb398ff17a66e72c682cd
# 4.查看启动情况
[root@izbp1j6sv2mtxgnuthuw6bz ~]# docker ps
CONTAINER ID   IMAGE                     COMMAND                  CREATED         STATUS         PORTS                                                                                                         NAMES
a5ca877169d4   rabbitmq:3.8-management   "docker-entrypoint.s…"   4 seconds ago   Up 3 seconds   4369/tcp, 5671/tcp, 0.0.0.0:5672->5672/tcp, 15671/tcp, 15691-15692/tcp, 25672/tcp, 0.0.0.0:15672->15672/tcp   rabbitmq
问题
  • 防火墙问题
  • 启动web管理界面
# 进入rabbitmq
[root@izbp1j6sv2mtxgnuthuw6bz ~]# docker exec -it rabbitmq bash
# 启动web管理界面
root@a5ca877169d4:/# rabbitmq-plugins enable rabbitmq_management

2.3.1 添加新用户

# 创建账号
root@a5ca877169d4:/# rabbitmqctl add_user admin admin
Adding user "admin" ...
Done. Don't forget to grant the user permissions to some virtual hosts! See 'rabbitmqctl help set_permissions' to learn more.

# 设置用户角色
root@a5ca877169d4:/# rabbitmqctl set_user_tags admin administrator
Setting tags for user "admin" to [administrator] ...

# 设置用户权限
# set_permissions [-p <vhostpath>] <user> <conf> <write> <read>
# 用户 admin 具有/vhost1 这个 virtual host 中所有资源的配置、写、读权限
root@a5ca877169d4:/# rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
Setting permissions for user "admin" in vhost "/" ...

# 查看当前用户和角色
root@a5ca877169d4:/# rabbitmqctl list_users
Listing users ...
user    tags
admin   [administrator]
guest   [administrator]

2.3 Hello World

  • 在本教程的这一部分中,我们将用 Java 编写两个程序;

  • 发送单个消息的生产者和接收消息并将其打印出来的消费者

  • 在下图中,“P”是我们的生产者,“C”是我们的消费者

  • 中间的盒子是一个队列——RabbitMQ 代表消费者保留的消息缓冲区
    Hello World

2.3.1 依赖

<!--指定 jdk 编译版本--> 
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <source>8</source>
                <target>8</target>
            </configuration>
        </plugin>
    </plugins>
</build> 
<dependencies>
    <!--rabbitmq 依赖客户端-->
    <dependency>
        <groupId>com.rabbitmq</groupId>
        <artifactId>amqp-client</artifactId>
        <version>5.8.0</version>
    </dependency>
    <!--操作文件流的一个依赖-->
    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
        <version>2.6</version>
    </dependency>
</dependencies>

2.3.2 消息生产者

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

public class Producer {
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {
        //创建一个连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("IP");
        factory.setUsername("admin");
        factory.setPassword("admin");
        //channel 实现了自动 close 接口 自动关闭 不需要显示关闭
        //创建连接
        Connection connection = factory.newConnection();
        //获取信道
        Channel channel = connection.createChannel();
        /**
         * 生成一个队列
         * 1.队列名称
         * 2.队列里面的消息是否持久化 也就是是否用完就删除
         * 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
         * 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
         * 5.其他参数 - 延迟、死信等
         */
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        String message = "hello world";
        /**
         * 发送一个消息
         * 1.发送到那个交换机
         * 2.路由的 key 是哪个
         * 3.其他的参数信息,传递的消息携带的properties
         * 4.发送消息的消息体
         */
        channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
        System.out.println("消息发送完毕");
    }
}

2.3.3 消息消费者

import com.rabbitmq.client.*;

public class Consumer {
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("IP");
        factory.setUsername("admin");
        factory.setPassword("admin");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        System.out.println("等待接收消息.........");

        //推送的消息如何进行消费的接口回调
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody());
            System.out.println(message);
        };
        //取消消费的一个回调接口 如在消费的时候队列被删除掉了
        CancelCallback cancelCallback = (consumerTag) -> {
            System.out.println("消息消费被中断");
        };
        /**
         * 消费者消费消息 - 接受消息
         * 1.消费哪个队列
         * 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
         * 3.消费者未成功消费的回调
         * 4.消息被取消时的回调
         */
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
    }
}

2.4 Work Queues

2.4.1 抽取工具类

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

public class RabbitMqUtils {
    //得到一个连接的 channel
    public static Channel getChannel() throws Exception {
        //创建一个连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("IP");
        factory.setUsername("admin");
        factory.setPassword("123456");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        return channel;
    }
}

2.4.2 启动两个工作线程来接收消息

import com.oddfar.utils.RabbitMqUtils;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;

// 这是一个工作线程,相当于之前的消费者
public class Worker01 {

    private static final String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {

        Channel channel = RabbitMqUtils.getChannel();
		channel.basicQos(1);
        //消息接受
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String receivedMessage = new String(delivery.getBody());
            System.out.println("接收到消息:" + receivedMessage);
        };
        //消息被取消
        CancelCallback cancelCallback = (consumerTag) -> {
            System.out.println(consumerTag + "消费者取消消费接口回调逻辑");

        };

        System.out.println("C1 消费者启动等待消费.................. ");
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);

    }
}

2.4.3 启动一个发送线程

public class Task01 {
    public static final String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {

        Channel channel = RabbitMqUtils.getChannel();
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            String message = scanner.next();
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            System.out.println("消息发送完成:" + message);
        }

    }
}

2.5 小结

server:服务器

v-host:名称上来说,虚拟主机

exchange:交换机(消息通常是发送到交换机)

queue:队列(存放消息的)

bind:将交换机和队列进行绑定

2.5.1 消息队列持久化

创建一个队列的时候,可以是非持久化的,也可以是持久化的

  • 非持久化:rabbitmq如果重启,该队列就会被删除
  • 持久化:重启不影响
  • 消息持久化必须要消息队列持久化
boolean durable = true;
channel.queueDeclare("task_queue", durable, false, false, null);

2.5.2 消息的持久化

消息持久化,可以一定程度上去预防消息丢失,需要设置MessageProperties.PERSISTENT_TEXT_PLAIN

注意:配置了消息持久化,并不能够完全保证消息不丢失,只能保证消息到了队列中不消失

消息丢失的方式:

  • 发送方发送到消息队列的时候,丢了
  • 交换机到队列中,丢了
  • 队列到消费者,丢了,目前所学的知识点,队列中还有消息,再次发送

后续的解决方案,参考确认Confirm,Return

channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());

2.5.3 ACK

默认情况下,消费者接收到消息的时候(但是通常接到消息就处理了),就会进行自动应答(ACK)

如果一个消费者处理消息时间长,或者异常了,所以需要手动Ack

  • 自动Ack
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
    String message = new String(delivery.getBody(), "UTF-8");
    System.out.println(" [x] Received '" + message + "'");
};
// true 表示自动ack:autoAck
channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });
  • 手动Ack
// 1.channel.basicConsume 将true改为false
channel.basicConsume(QUEUE_NAME, false, deliverCallback, consumerTag -> { });

// 2.在finally 中手动Ack
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);

2.5.4 消费不均匀

工作模式中,默认轮询,会出现一些问题,比如:两个消费者,亮跟大哥,实现功能(10个),

亮:1,3,5,7,9

大哥:2,4,6,8,10

但是大哥实现的快,出现一个问题,大哥3个小时完成了,摸鱼5小时,亮做了8小时

所以我们可以设置一个预抓取值,官网给的是一次抓取一个任务(每次抓取一个消息,当上一个消息没有ack的时候,不抓取新的消息)

int prefetchCount = 1;
channel.basicQos(prefetchCount);

注意:这里的预抓取值给的是1,这个值太小(消费者能力比较强,就会等待),但是具体还是看服务器性能跟消息复杂度,通常情况下100-300

2.6 Publish/SubscribeFanout

多出来的X,就是交换机,交换机接收发送方的消息,将消息发送到队列中,交换机还有不同的类型

Exchange的类型

  • Fanout:交换机会将消息发送到所有和它进行绑定的队列上,广播,群发
  • Direct:直接交换机通过消息上的路由键直接对消息进行分发,相当于精确匹配,一对一
  • Topic:这个交换机会将路由键和绑定上的模式进行通配符匹配,相当于模糊匹配,一对多
  • Headers:消息头交换机使用消息头的属性进行消息路由,相当于模糊匹配(like header%),一对多
    Publish/Subscribe

2.6.1 操作

  • 发送方
public class EmitLog {

    // 交换机名称
    private static final String EXCHANGE_NAME = "logs";

    public static void main(String[] argv) throws Exception {
        try (Channel channel = RabbitMqUtils.getChannel()) {
            // 创建交换机,并设置交换机类型
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
            Scanner scanner = new Scanner(System.in);
            while (true) {
                System.out.print("请输入要发送的消息:");
                String msg = scanner.next();
                channel.basicPublish(EXCHANGE_NAME, "", null, msg.getBytes("UTF-8"));
                System.out.println("消息已发送:" + msg);
            }
        }

    }
}
  • 消费方 01- 控制台打印
// 第一个消费者
public class ReceiveLogs01 {

    // 交换机名称
    private static final String EXCHANGE_NAME = "logs";

    public static void main(String[] argv) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        // 创建一个交换机,并设置类型
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
        // 创建一个非持久化队列
        String queueName = channel.queueDeclare().getQueue();
        // 将交换机和队列绑定
        channel.queueBind(queueName, EXCHANGE_NAME, "");
        System.out.println(" 准备接收消息。。。");
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            // 第一个消费者在控制台进行答应
            System.out.println("控制台打印的消息:" + message);
        };
        channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
    }
}
  • 消费者02-保存到本地硬盘
public class ReceiveLogs02 {

    // 交换机名称
    private static final String EXCHANGE_NAME = "logs";

    public static void main(String[] argv) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        // 创建一个交换机,并设置类型
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
        // 创建一个非持久化队列
        String queueName = channel.queueDeclare().getQueue();
        // 将交换机和队列绑定
        channel.queueBind(queueName, EXCHANGE_NAME, "");
        System.out.println(" 准备接收消息。。。");
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            // 第二个消费者将消息保存到本地
            File file = new File("C:\\rabbitmq_log.txt");
            FileUtils.writeStringToFile(file, message, "UTF-8");
            System.out.println("日志写入成功");
        };
        channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
    }
}

2.7 Routingdirect

在上一个发布订阅中,实现了一对多,但是每一个消息,都会执行我们指定的两个操作,粒度更加细致

对于error级别的信息,需要在控制台打印,并且保存到本地,对于其他信息,仅仅是控制台打印
Routing

2.7.1 操作

  • 发送方
public class EmitLogDirect {

    private static final String EXCHANGE_NAME = "direct_logs";

    public static void main(String[] args) throws Exception {
        final Channel channel = RabbitMqUtils.getChannel();
        // 1.创建交换机,更换了交换机的类型
        channel.exchangeDeclare(EXCHANGE_NAME, "direct");
        // 准备路由的key
        Map<String, String> map = new HashMap<>();
        map.put("info", "普通的info消息");
        map.put("warning", "warning警告消息");
        map.put("error", "error错误消息");
        map.put("debug", "debug调式消息");

        // 循环map,key routkey, value,是发送的消息
        for (Map.Entry<String, String> entry : map.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue();
            // 发送
            channel.basicPublish(EXCHANGE_NAME, key, null, value.getBytes("UTF-8"));
            System.out.println("消息发送成功");
        }
    }
}
  • 消费方
import com.rabbitmq.client.*;

public class ReceiveLogsDirect {

    private static final String EXCHANGE_NAME = "direct_logs";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.22.77");
        factory.setUsername("guest");
        factory.setPassword("guest");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, "direct");
        String queueName = channel.queueDeclare().getQueue();

        channel.queueBind(queueName, EXCHANGE_NAME, "info");
        channel.queueBind(queueName, EXCHANGE_NAME, "debug");
        channel.queueBind(queueName, EXCHANGE_NAME, "warning");
        System.out.println(" [*] 等待消息。退出按 CTRL+C");

        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [x] 收到 '" +
                    delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
        };
        channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
    }
}

2.8 Topics (Topic)

发送到主题交换的消息不能有任意的 routing_key - 它必须是单词列表,由点分隔。

这些词可以是任何东西,但通常它们指定与消息相关的一些特征。一些有效的路由键示例:“ stock.usd.nyse ”、“ nyse.vmw ”、“ quick.orange.rabbit ”。路由键中可以有任意多的单词,最多为 255 个字节

  • *(星号)可以只替换一个单词。

  • # (hash) 可以代替零个或多个单词

  • 绑定的时候出现情况

    • 当一个队列绑定的是#,那么这个队列是不是接收了所有的消息,这个时候跟fanout有点像
    • 当一个队列绑定的既没有#,也没有*,那就是一个direct
      Topics
  • quick.orange.rabbit - Q1,Q2

  • lazy.orange.ele - Q1,Q2

  • quick.orange.fix - Q1

  • lazy.brown.fox - Q2

  • lazy.pink.rabbit - Q2 满足了两次,消息一条

  • quick.brown.fox - 不满足

  • quick.orange.male.rabbit - 不满足

  • lazy.orange.male.rabbit - Q2

2.8.1 操作

  • 发送方
public class EmitLogTopic {

    private static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] args) throws Exception {
        final Channel channel = RabbitMqUtils.getChannel();
        // 1.创建交换机,更换了交换机的类型
        channel.exchangeDeclare(EXCHANGE_NAME, "topic");
        // 准备路由的key
        Map<String, String> map = new HashMap<>();
        map.put("quick.orange.rabbit", "Q1,Q2接收");
        map.put("lazy.orange.ele", "Q1,Q2接收");
        map.put("quick.orange.fix", "Q1接收");
        map.put("lazy.pink.rabbit", "Q2接收");
        map.put("quick.brown.fox", "不满足");
        map.put("quick.orange.male.rabbit", "不满足");
        map.put("lazy.orange.male.rabbit", "Q2接收");

        // 循环map,key routkey, value,是发送的消息
        for (Map.Entry<String, String> entry : map.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue();
            // 发送
            channel.basicPublish(EXCHANGE_NAME, key, null, value.getBytes("UTF-8"));
            System.out.println("消息发送成功");
        }
    }
}
  • 消费方
public class ReceiveLogsTopict01 {
    // 交换机名称
    private static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] argv) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        // 创建一个交换机,并设置类型
        channel.exchangeDeclare(EXCHANGE_NAME, "topic");
        // 创建一个非持久化队列
        String queueName = channel.queueDeclare().getQueue();
        // 将交换机和队列绑定
        channel.queueBind(queueName, EXCHANGE_NAME, "*.orange.*");
        System.out.println(" 准备接收消息。。。");
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            message = "绑定的key:" + delivery.getEnvelope().getRoutingKey() + "-绑定的值:" + message + "-规则(*.orange.*)";
            // 第一个消费者在控制台进行答应
            System.out.println("控制台打印的消息:" + message);
        };
        channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
    }
}
public class ReceiveLogsTopict02 {
    // 交换机名称
    private static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] argv) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        // 创建一个交换机,并设置类型
        channel.exchangeDeclare(EXCHANGE_NAME, "topic");
        // 创建一个非持久化队列
        String queueName = channel.queueDeclare().getQueue();
        // 将交换机和队列绑定
        channel.queueBind(queueName, EXCHANGE_NAME, "*.*.rabbit");
        channel.queueBind(queueName, EXCHANGE_NAME, "lazy.#");
        System.out.println(" 准备接收消息。。。");
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            message = "绑定的key:" + delivery.getEnvelope().getRoutingKey() + "-绑定的值:" + message + "-规则:(*.*.rabbit,lazy.#)";
            // 第一个消费者在控制台进行答应
            System.out.println("控制台打印的消息:" + message);
        };
        channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
    }
}

2.9 Publisher Confirms

不仅仅是防止发送方到mq的消息丢失,还可以通过异步的方式来提高效率

1.为了消息不丢失,持久化,持久化是同步的,可以通过confirm的异步确认提高效率
Publisher Confirms

// 1.启用发布取人
channel.confirmSelect();

2.9.1 三种方式

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

import java.time.Duration;
import java.util.UUID;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;

public class PublisherConfirms {

    static final int MESSAGE_COUNT = 1000;

    public static void main(String[] args) throws Exception {
        //创建一个连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.22.77");
        factory.setUsername("guest");
        factory.setPassword("guest");
        //channel 实现了自动 close 接口 自动关闭 不需要显示关闭
        try (//创建连接
             Connection connection = factory.newConnection();
             //获取信道
             Channel channel = connection.createChannel()) {
            // 开启发布确认
            channel.confirmSelect();
            // Published 1,000 messages individually in 620 ms
            // publishMessagesIndividually(channel);
            // Published 1,000 messages in batch in 80 ms
            // publishMessagesInBatch(channel);
            // Published 1,000 messages and handled confirms asynchronously in 58 ms
            // handlePublishConfirmsAsynchronously(channel);
        }
    }

    // 单个确认(同步)
    static void publishMessagesIndividually(Channel channel) throws Exception {}

    // 批量确认(同步)
    static void publishMessagesInBatch(Channel channel) throws Exception {}

    // 异步确认
    static void handlePublishConfirmsAsynchronously(Channel channel) throws Exception {}

1.单个确认(同步)
// 单个确认(同步)
static void publishMessagesIndividually(Channel channel) throws Exception {
    // 1.获取UUID作为队列的名称
    String queue = UUID.randomUUID().toString();
    // 2.创建队列
    channel.queueDeclare(queue,false,false,true,null);
    // 3.处理之前的时间
    long start = System.nanoTime();
    // 循环发送1000次
    for (int i = 0; i < MESSAGE_COUNT; i++) {
        String body = String.valueOf(i);
        channel.basicPublish("", queue, null, body.getBytes());
        // 确认
        channel.waitForConfirms();
        // 超过5000 认为超时了
        //channel.waitForConfirmsOrDie(5_000);
    }
    long end = System.nanoTime();
    System.out.format("Published %,d messages individually in %,d ms%n", MESSAGE_COUNT, Duration.ofNanos(end - start).toMillis());
}
2.批量确认(同步)
// 批量确认(同步)
static void publishMessagesInBatch(Channel channel) throws Exception {
    String queue = UUID.randomUUID().toString();
    channel.queueDeclare(queue, false, false, true, null);
    // 到100个确认
    int batchSize = 100;
    // 计数
    int outstandingMessageCount = 0;
    // 开始时间
    long start = System.nanoTime();

    for (int i = 0; i < MESSAGE_COUNT; i++) {
        // 需要发送的消息
        String body = String.valueOf(i);
        // 发送
        channel.basicPublish("", queue, null, body.getBytes());
        // 每发送一条,计数器+1
        outstandingMessageCount++;
        // 如果计数器跟设置的批处理数据的值是一样的,执行确认操作 值是100
        if (outstandingMessageCount == batchSize) {
            //channel.waitForConfirmsOrDie(5_000);
            channel.waitForConfirms();
            // 计数器清零
            outstandingMessageCount = 0;
        }
    }

    // 如果数据没有到100条,最后也要确认
    if (outstandingMessageCount > 0) {
        channel.waitForConfirms();
    }
    long end = System.nanoTime();
    System.out.format("Published %,d messages in batch in %,d ms%n", MESSAGE_COUNT, Duration.ofNanos(end - start).toMillis());
}
3.异步确认
// 异步确认
static void handlePublishConfirmsAsynchronously(Channel channel) throws Exception {
    String queue = UUID.randomUUID().toString();
    channel.queueDeclare(queue, false, false, true, null);

    // 队列
    ConcurrentNavigableMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();

    // sequenceNumber:表示的消息的编号,multiple:是否批量
    ConfirmCallback cleanOutstandingConfirms = (sequenceNumber, multiple) -> {

        // 判断是否批量
        if (multiple) {
            ConcurrentNavigableMap<Long, String> confirmed = outstandingConfirms.headMap(
                    sequenceNumber, true
            );
            // 清空map中确认的消息
            confirmed.clear();
        } else {
            // 单个,删除map中确认的消息
            outstandingConfirms.remove(sequenceNumber);
        }

        // 成功确认的回调
        System.out.println("成功回调!!!" + sequenceNumber);
    };
    // 第一个参数:确认之后的回调, 第二个参数:没有确认的回调
    channel.addConfirmListener(cleanOutstandingConfirms,(sequenceNumber, multiple) -> {
        // 没有确认的回调
        // 获取没有确认的编号
        String body = outstandingConfirms.get(sequenceNumber);
        // 输出异常
        System.err.format(
                "Message with body %s has been nack-ed. Sequence number: %d, multiple: %b%n",
                body, sequenceNumber, multiple
        );

        // 如果成功发送之后,应该继续之前的删除成功的消息
        cleanOutstandingConfirms.handle(sequenceNumber, multiple);
    });

    long start = System.nanoTime();
    for (int i = 0; i < MESSAGE_COUNT; i++) {
        String body = String.valueOf(i);

        // 将所有的消息放在队列
        outstandingConfirms.put(channel.getNextPublishSeqNo(), body);

        channel.basicPublish("", queue, null, body.getBytes());
    }

    long end = System.nanoTime();
    System.out.format("Published %,d messages and handled confirms asynchronously in %,d ms%n", MESSAGE_COUNT, Duration.ofNanos(end - start).toMillis());
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值