使用rabbitmq

4 篇文章 0 订阅
1 篇文章 0 订阅

RabbitMQ

1. 什么是MQ(图片没加载有时间加上)

AMQP,即Advanced Message Queuing Protocol,高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。消息中间件主要用于组件之间的解耦,消息的发送者无需知道消息使用者的存在,反之亦然。 AMQP的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。 RabbitMQ是一个开源的AMQP实现,服务器端用Erlang语言编写,支持多种客户端,如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP等,支持AJAX。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。

2. RabbitMQ的应用场景

1.异步处理

同步和异步(会造成等待);向数据库发送请求(不会造成等待)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yKhTjFG8-1573723272449)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps1.jpg)]

如:传统做法:串行:用户注册150ms——50ms数据库——50ms发送邮箱——50ms发送短信

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b8qMsHDg-1573723272451)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps2.jpg)]

使用rabbitmq:用户注册55ms——50ms数据库—5ms消息队列—发送邮箱——发送短信

这样用户发送注册成功之后不需要再等待发送邮箱和短信的时间了,只要写入队列就可以了,也就是说只要是用户不需要等待的东西都可以考虑使用rabbitmq

2.系统解耦

用户下单后,订单系统需要通知库存系统,传统的做法就是订单系统调用库存系统的接口.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AJOncycf-1573723272453)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps3.jpg)]

缺点:当库存系统出现故障时,订单就会失败,订单系统和库存系统高耦合

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T5P2YzcA-1573723272454)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps4.jpg)]

当:订单系统——消息队列——库存系统

订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。

库存系统:订阅下单的消息,获取下单消息,进行库操作。 就算库存系统出现故障,消息队列也能保证消息的可靠投递,不会导致消息丢失。

3.流量削峰

流量削峰一般在秒杀活动中应用广泛

场景:秒杀活动,一般会因为流量过大,导致应用挂掉,为了解决这个问题,一般在应用前端加入消息队列。 作用: 1.可以控制活动人数,超过此一定阀值的订单直接丢弃(我为什么秒杀一次都没有成功过呢^^) 2.可以缓解短时间的高流量压垮应用(应用程序按自己的最大处理能力获取订单)

1.用户的请求,服务器收到之后,首先写入消息队列,加入消息队列长度超过最大值,则直接抛弃用户请求或跳转到错误页面. 2.秒杀业务根据消息队列中的请求信息,再做后续处理. 高负载请求/任务的缓冲处理

1.1. 学习5种队列

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5xgkcMdu-1573723272455)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps5.jpg)]

1.2. 安装文档

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KgEZZOay-1573723272457)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps6.jpg)]

2. 搭建RabbitMQ环境

2.1. 下载

下载地址:http://www.rabbitmq.com/download.html

2.2. windows下安装

2.2.1. 安装Erlang

下载:http://www.erlang.org/download/otp_win64_17.3.exe

安装:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-urre2myu-1573723272459)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps7.jpg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mz8zp6bD-1573723272459)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps8.jpg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NWLCaB9O-1573723272460)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps9.jpg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wpx12ZCo-1573723272461)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps10.jpg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TQhQZ9vx-1573723272461)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps11.jpg)]

安装完成。

2.2.2. 安装RabbitMQ

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l9kTMK7u-1573723272462)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps12.jpg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cm0t0iMV-1573723272463)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps13.jpg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m3r0Ns3C-1573723272464)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps14.jpg)]

安装完成。

开始菜单里出现如下选项:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GKNs2Sbv-1573723272464)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps15.jpg)]

启动、停止、重新安装等。

1.1.1. 启用管理工具

1、 双击[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XWRRjl95-1573723272465)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps16.jpg)]

2、 进入C:\Program Files (x86)\RabbitMQ Server\rabbitmq_server-3.4.1\sbin输入命令:

rabbitmq-plugins enable rabbitmq_management

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hOuhO1aB-1573723272466)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps17.jpg)]

这样就启动了管理工具,可以试一下命令:

​ 停止:net stop RabbitMQ

​ 启动:net start RabbitMQ

3、 在浏览器中输入地址查看:http://127.0.0.1:15672/

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eUOQSvHM-1573723272467)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps18.jpg)]

4、 使用默认账号登录:guest/ guest

1.1. Linux下安装

1.1.1. 安装Erlang

1.1.2. 添加yum支持

cd /usr/local/src/

mkdir rabbitmq

cd rabbitmq

wget http://packages.erlang-solutions.com/erlang-solutions-1.0-1.noarch.rpm

rpm -Uvh erlang-solutions-1.0-1.noarch.rpm

rpm --import http://packages.erlang-solutions.com/rpm/erlang_solutions.asc

使用yum安装:

sudo yum install erlang

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ylx3pLkR-1573723272467)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps19.jpg)]

1.1.3. 安装RabbitMQ

上传rabbitmq-server-3.4.1-1.noarch.rpm文件到/usr/local/src/rabbitmq/

安装:

rpm -ivh rabbitmq-server-3.4.1-1.noarch.rpm

1.1.4. 启动、停止

service rabbitmq-server start

service rabbitmq-server stop

service rabbitmq-server restart

1.1.5. 设置开机启动

chkconfig rabbitmq-server on

1.1.6. 设置配置文件

cd /etc/rabbitmq

cp /usr/share/doc/rabbitmq-server-3.4.1/rabbitmq.config.example /etc/rabbitmq/

mv rabbitmq.config.example rabbitmq.config

1.1.7. 开启用户远程访问

vi /etc/rabbitmq/rabbitmq.config

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eJYEBJzV-1573723272468)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps20.jpg)]

注意要去掉后面的逗号。

1.1.8. 开启web界面管理工具

rabbitmq-plugins enable rabbitmq_management

service rabbitmq-server restart

1.1.9. 防火墙开放15672端口

/sbin/iptables -I INPUT -p tcp --dport 15672 -j ACCEPT

/etc/rc.d/init.d/iptables save

1.2. 安装的注意事项

1、 推荐使用默认的安装路径

2、 系统用户名必须是英文

Win10改名字非常麻烦,具体方法百度
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wSmRf1ok-1573723272469)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps21.jpg)]

3、 计算机名必须是英文

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HGecvbBM-1573723272469)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps22.jpg)]

4、 系统的用户必须是管理员

如果安装失败应该如何解决:

1、 重装系统 – 不推荐

2、 将RabbitMQ安装到linux虚拟机中

a) 推荐

3、 使用别人安装好的RabbitMQ服务

a) 只要给你开通一个账户即可。

b) 使用公用的RabbitMQ服务,在192.168.50.22

c) 推荐

常见错误:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BWLHhb32-1573723272470)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps23.jpg)]

1.3. 安装完成后操作

1、系统服务中有RabbitMQ服务,停止、启动、重启

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x7flLpn7-1573723272471)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps24.jpg)]

2、打开命令行工具
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r5av5kVD-1573723272472)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps25.jpg)]
如果找不到命令行工具:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-51ZdSxN1-1573723272472)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps26.jpg)]

输入命令rabbitmq-plugins enable rabbitmq_management启用管理插件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YbmmU2hR-1573723272473)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps27.jpg)]

查看管理页面
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dxnVtRi5-1573723272474)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps28.jpg)]

通过默认账户 guest/guest 登录
如果能够登录,说明安装成功。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CvySl6cC-1573723272475)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps29.jpg)]

1. 添加用户

1.1. 添加admin用户

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wVCAJzQ7-1573723272475)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps30.jpg)]

1.2. 用户角色

1、 超级管理员(administrator)

可登陆管理控制台,可查看所有的信息,并且可以对用户,策略(policy)进行操作。

2、 监控者(monitoring)

可登陆管理控制台,同时可以查看rabbitmq节点的相关信息(进程数,内存使用情况,磁盘使用情况等)

3、 策略制定者(policymaker)

可登陆管理控制台, 同时可以对policy进行管理。但无法查看节点的相关信息(上图红框标识的部分)。

4、 普通管理者(management)

仅可登陆管理控制台,无法看到节点信息,也无法对策略进行管理。

5、 其他

无法登陆管理控制台,通常就是普通的生产者和消费者。

1.3. 创建Virtual Hosts

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZgHUKpwW-1573723272476)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps31.jpg)]

选中Admin用户,设置权限:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3EfDgu1P-1573723272477)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps32.jpg)]

看到权限已加:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0jyZp7gz-1573723272478)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps33.jpg)]

1.4. 管理界面中的功能

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hU9FoY74-1573723272478)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps34.jpg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CbWpK8ZA-1573723272479)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps35.jpg)]

1. 五种队列

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bv3U2mqF-1573723272480)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps36.jpg)]

1.1. 导入my-rabbitmq

1.1.1. 图示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zPhlOOmk-1573723272480)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps37.jpg)]

P:消息的生产者

C:消息的消费者

红色:队列

生产者将消息发送到队列,消费者从队列中获取消息。

1.1.2. 导入RabbitMQ的客户端依赖

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

1.1.3. 获取MQ的连接

package com.rabbitmq.rabbitmq;

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

public class ConnectionUtil {

    public static Connection getConnection() throws Exception {
        //定义连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置服务地址
        factory.setHost("localhost");
        //端口
        factory.setPort(5672);
        //设置账号信息,用户名、密码、vhost
        factory.setVirtualHost("testhost");
        factory.setUsername("admin");
        factory.setPassword("admin");
        // 通过工程获取连接
        Connection connection = factory.newConnection();
        return connection;
    }
}

1.1.4. 生产者发送消息到队列

package com.rabbitmq.rabbitmq.rabbitmq1;

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

//生产者发送消息到队列
public class Send {
    //队列
    private final static String QUEUE_NAME = "q_test_01";

    public static void main(String[] argv) throws Exception {
        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        // 从连接中创建通道
        Channel channel = connection.createChannel();

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

        // 消息内容
        String message = "Hello World!";
        channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
        System.out.println(" [x] Sent '" + message + "'");
        //关闭通道和连接
        channel.close();
        connection.close();
    }
}

1.1.5. 管理工具中查看消息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Srsk4b67-1573723272481)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps38.jpg)]

点击上面的队列名称,查询具体的队列中的信息:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c6Z4Tyyg-1573723272482)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps39.jpg)]

1.1.6. 消费者从队列中获取消息

package com.rabbitmq.rabbitmq.rabbitmq1.rabbitmq11;

//import com.zpc.rabbitmq.util.ConnectionUtil;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.rabbitmq.ConnectionUtil;

//消费者从队列中获取消息
public class Recv {

    private final static String QUEUE_NAME = "q_test_01";

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

        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        // 从连接中创建通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);

        // 监听队列
        channel.basicConsume(QUEUE_NAME, true, consumer);

        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [x] Received '" + message + "'");
        }
    }
}

1.2. Work模式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YKflyy9n-1573723272483)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps40.jpg)]

1.2.1. 图示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xsWc9L5R-1573723272483)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps41.jpg)]

一个生产者、2个消费者。

一个消息只能被一个消费者获取。

1.2.2. 消费者1

package com.rabbitmq.rabbitmq.rabbitwork;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.rabbitmq.ConnectionUtil;


//消费者1
public class Recv {

    private final static String QUEUE_NAME = "test_queue_work";

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

        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

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

        // 同一时刻服务器只会发一条消息给消费者
        channel.basicQos(1);

        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列,false表示手动返回完成状态,true表示自动
        channel.basicConsume(QUEUE_NAME, false, consumer);

        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [y] Received '" + message + "'");
            //休眠
            Thread.sleep(10);
            // 返回确认状态,注释掉表示使用自动确认模式
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }
    }
}

1.2.3. 消费者2

package com.rabbitmq.rabbitmq.rabbitwork;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.rabbitmq.ConnectionUtil;

//import com.zpc.rabbitmq.util.ConnectionUtil;
//消费者2
public class Recv2 {

    private final static String QUEUE_NAME = "test_queue_work";

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

        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

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

        // 同一时刻服务器只会发一条消息给消费者
        channel.basicQos(1);

        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列,false表示手动返回完成状态,true表示自动
        channel.basicConsume(QUEUE_NAME, false, consumer);
   int i=0;
        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [x] Received '" + message + "'");
            // 休眠1秒
            Thread.sleep(1000);
            //下面这行注释掉表示使用自动确认模式
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            i++;
            System.out.println(i);
        }

    }
}


1.2.4. 生产者

向队列中发送100条消息。

package com.rabbitmq.rabbitmq.rabbitwork;


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

//import com.zpc.rabbitmq.util.ConnectionUtil;

//生产者
public class Send {

    private final static String QUEUE_NAME = "test_queue_work";

    public static void main(String[] argv) throws Exception {
        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

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

        for (int i = 0; i < 100; i++) {
            // 消息内容
            String message = "" + i;
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            System.out.println(" [x] Sent '" + message + "'");

            Thread.sleep(i * 10);
        }

        channel.close();
        connection.close();
    }
}

1.1.1. 测试

测试结果:

1、 消费者1和消费者2获取到的消息内容是不同的,同一个消息只能被一个消费者获取。

2、 消费者1和消费者2获取到的消息的数量是相同的,一个是消费奇数号消息,一个是偶数。

其实,这样是不合理的,因为消费者1线程停顿的时间短。应该是消费者1要比消费者2获取到的消息多才对。

RabbitMQ 默认将消息顺序发送给下一个消费者,这样,每个消费者会得到相同数量的消息。即轮询(round-robin)分发消息。

怎样才能做到按照每个消费者的能力分配消息呢?联合使用 Qos 和 Acknowledge 就可以做到。

basicQos 方法设置了当前信道最大预获取(prefetch)消息数量为1。消息从队列异步推送给消费者,消费者的 ack 也是异步发送给队列,从队列的视角去看,总是会有一批消息已推送但尚未获得 ack 确认,Qos 的 prefetchCount 参数就是用来限制这批未确认消息数量的。设为1时,队列只有在收到消费者发回的上一条消息 ack 确认后,才会向该消费者发送下一条消息。prefetchCount 的默认值为0,即没有限制,队列会将所有消息尽快发给消费者。

2个概念

轮询分发 :使用任务队列的优点之一就是可以轻易的并行工作。如果我们积压了好多工作,我们可以通过增加工作者(消费者)来解决这一问题,使得系统的伸缩性更加容易。在默认情况下,RabbitMQ将逐个发送消息到在序列中的下一个消费者(而不考虑每个任务的时长等等,且是提前一次性分配,并非一个一个分配)。平均每个消费者获得相同数量的消息。这种方式分发消息机制称为Round-Robin(轮询)。

公平分发 :虽然上面的分配法方式也还行,但是有个问题就是:比如:现在有2个消费者,所有的奇数的消息都是繁忙的,而偶数则是轻松的。按照轮询的方式,奇数的任务交给了第一个消费者,所以一直在忙个不停。偶数的任务交给另一个消费者,则立即完成任务,然后闲得不行。而RabbitMQ则是不了解这些的。这是因为当消息进入队列,RabbitMQ就会分派消息。它不看消费者为应答的数目,只是盲目的将消息发给轮询指定的消费者。

为了解决这个问题,我们使用basicQos( prefetchCount = 1)方法,来限制RabbitMQ只发不超过1条的消息给同一个消费者。当消息处理完毕后,有了反馈,才会进行第二次发送。

还有一点需要注意,使用公平分发,必须关闭自动应答,改为手动应答。

1.1. Work模式的“能者多劳”

打开上述代码的注释:

// 同一时刻服务器只会发一条消息给消费者

channel.basicQos(1);

//开启这行 表示使用手动确认模式

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

同时改为手动确认:

// 监听队列,false表示手动返回完成状态,true表示自动

channel.basicConsume(QUEUE_NAME, false, consumer);

测试:

消费者1比消费者2获取的消息更多。

1.2. 消息的确认模式

消费者从队列中获取消息,服务端如何知道消息已经被消费呢?

模式1:自动确认

​ 只要消息从队列中获取,无论消费者获取到消息后是否成功消息,都认为是消息已经成功消费。

模式2:手动确认

​ 消费者从队列中获取消息后,服务器会将该消息标记为不可用状态,等待消费者的反馈,如果消费者一直没有反馈,那么该消息将一直处于不可用状态。

手动模式:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2hDEokKF-1573723272484)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps42.jpg)]

自动模式:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XKuovfUl-1573723272485)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps43.jpg)]

1.3. 订阅模式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-95wM0l7W-1573723272486)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps44.jpg)]

1.3.1. 图示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F9Uwpme7-1573723272486)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps45.jpg)]

解读:

1、1个生产者,多个消费者

2、每一个消费者都有自己的一个队列

3、生产者没有将消息直接发送到队列,而是发送到了交换机

4、每个队列都要绑定到交换机

5、生产者发送的消息,经过交换机,到达队列,实现,一个消息被多个消费者获取的目的

注意:一个消费者队列可以有多个消费者实例,只有其中一个消费者实例会消费

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Err46YOE-1573723272487)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps46.jpg)]

1.3.2. 消息的生产者

向交换机中发送消息。

package com.rabbitmq.rabbitmq.rabbitX;


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

//消息的生产者(看作是后台系统)
//        向交换机中发送消息。
//注意:消息发送到没有队列绑定的交换机时,消息将丢失,因为,交换机没有存储消息的能力,消息只能存在在队列中。
public class Send {

    private final static String EXCHANGE_NAME = "test_exchange_fanout";

    public static void main(String[] argv) throws Exception {
        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明exchange
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");

        // 消息内容
        String message = "Hello World!";
        channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
        System.out.println(" [x] Sent '" + message + "'");

        channel.close();
        connection.close();
    }
}

注意:消息发送到没有队列绑定的交换机时,消息将丢失,因为,交换机没有存储消息的能力,消息只能存在在队列中。

1.3.3. 消费者1

package com.rabbitmq.rabbitmq.rabbitX;


import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.rabbitmq.ConnectionUtil;


//消费者1(看作是前台系统)
public class Recv {

    private final static String QUEUE_NAME = "test_queue_direct_1";

    private final static String EXCHANGE_NAME = "test_exchange_fanout";

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

        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

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

        // 绑定队列到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
        // 同一时刻服务器只会发一条消息给消费者
        channel.basicQos(1);

        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列,手动返回完成
        channel.basicConsume(QUEUE_NAME, false, consumer);

        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [Recv] Received '" + message + "'");
            Thread.sleep(10);

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

1.3.4. 消费者2

package com.rabbitmq.rabbitmq.rabbitX;


import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.rabbitmq.ConnectionUtil;

//消费者2(看作是搜索系统)
public class Recv2 {

    private final static String QUEUE_NAME = "test_queue_work2";

    private final static String EXCHANGE_NAME = "test_exchange_fanout";

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

        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

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

        // 绑定队列到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");

        // 同一时刻服务器只会发一条消息给消费者
        channel.basicQos(1);

        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列,手动返回完成
        channel.basicConsume(QUEUE_NAME, false, consumer);

        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [Recv2] Received '" + message + "'");
            Thread.sleep(10);

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

1.1.1. 测试

测试结果:

同一个消息被多个消费者获取。一个消费者队列可以有多个消费者实例,只有其中一个消费者实例会消费到消息。

在管理工具中查看队列和交换机的绑定关系:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cQOkTqdO-1573723272488)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps47.jpg)]

1.1. 路由模式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uGFUDUIX-1573723272489)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps48.jpg)]

1.1.1. 图示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gysHarcD-1573723272489)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps49.jpg)]

1.1.2. 生产者

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MC4hFsIa-1573723272490)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps50.jpg)]

package com.rabbitmq.rabbitmq.rabbitxkey;


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

//消息的生产者(看作是后台系统)
//        向交换机中发送消息。
//注意:消息发送到没有队列绑定的交换机时,消息将丢失,因为,交换机没有存储消息的能力,消息只能存在在队列中。
public class Send {

    private final static String EXCHANGE_NAME = "test_exchange_direct";

    public static void main(String[] argv) throws Exception {
        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明exchange
        channel.exchangeDeclare(EXCHANGE_NAME, "direct");

        // 消息内容
        String message = "Hello World!";
        channel.basicPublish(EXCHANGE_NAME, "delete", null, message.getBytes());
        System.out.println(" [x] Sent '" + message + "'");
        String messageupdate = "Hello update!";
        channel.basicPublish(EXCHANGE_NAME, "update", null, messageupdate.getBytes());
        System.out.println(" [x] Sent '" + messageupdate + "'");

        channel.close();
        connection.close();
    }
}

1.1.3. 消费者1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RjmzycEq-1573723272491)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps51.jpg)]

package com.rabbitmq.rabbitmq.rabbitxkey;


import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.rabbitmq.ConnectionUtil;


//消费者1
public class Recv {

    private final static String QUEUE_NAME = "test_queue_direct_3";

    private final static String EXCHANGE_NAME = "test_exchange_direct2";

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

        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

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

        // 绑定队列到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "delete");
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "update");

        // 同一时刻服务器只会发一条消息给消费者
        channel.basicQos(1);

        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列,手动返回完成
        channel.basicConsume(QUEUE_NAME, false, consumer);

        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [Recv] Received '" + message + "'");
            Thread.sleep(10);
        }
    }
}

1.1.4. 消费2

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7R1oact2-1573723272492)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps52.jpg)]

package com.rabbitmq.rabbitmq.rabbitxkey;


import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.rabbitmq.ConnectionUtil;

//消费者2
public class Recv2 {

    private final static String QUEUE_NAME = "test_queue_direct_4";

    private final static String EXCHANGE_NAME = "test_exchange_direct2";

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

        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

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

        // 绑定队列到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "insert");
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "update");
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "delete");

        // 同一时刻服务器只会发一条消息给消费者
        channel.basicQos(1);

        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列,手动返回完成
        channel.basicConsume(QUEUE_NAME, false, consumer);

        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [Recv2] Received '" + message + "'");
            Thread.sleep(10);

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

1.2. 主题模式(通配符模式)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rwUE7UFo-1573723272492)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps53.jpg)]

topic Exchange:将路由键和某模式进行匹配,此时队列需要绑定要一个模式上。符号:#匹配0个或多个词,符号*匹配一个不多不少也给词,词中间用.点表示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zfwLUbEP-1573723272493)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps54.jpg)]

1.2.1. 图示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zGbUsDRV-1573723272494)(file:///C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\wps55.jpg)]

同一个消息被多个消费者获取。一个消费者队列可以有多个消费者实例,只有其中一个消费者实例会消费到消息。

1.2.2. 生产者

package com.rabbitmq.rabbitmq.rabbittopic;//package com.rabbitmq.rabbitmq.rabbittopic;


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


public class Send {

    private final static String EXCHANGE_NAME = "test_exchange_topic";

    public static void main(String[] argv) throws Exception {
        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明exchange
        channel.exchangeDeclare(EXCHANGE_NAME, "topic");

        // 消息内容
        String message = "Hello World!!";
        channel.basicPublish(EXCHANGE_NAME, "routekey.1", null, message.getBytes());
        System.out.println(" [x] Sent '" + message + "'");

        channel.close();
        connection.close();
    }
}

1.2.3. 消费者1

package com.rabbitmq.rabbitmq.rabbittopic;//package com.rabbitmq.rabbitmq.rabbittopic;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.rabbitmq.ConnectionUtil;

public class Recv {

    private final static String QUEUE_NAME = "test_queue_topic_work_1";

    private final static String EXCHANGE_NAME = "test_exchange_topic";

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

        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

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

        // 绑定队列到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "routekey.*");

        // 同一时刻服务器只会发一条消息给消费者
        channel.basicQos(1);

        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列,手动返回完成
        channel.basicConsume(QUEUE_NAME, false, consumer);

        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [Recv_x] Received '" + message + "'");
            Thread.sleep(10);

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

1.2.4. 消费者2

package com.rabbitmq.rabbitmq.rabbittopic;//package com.rabbitmq.rabbitmq.rabbittopic;


import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.rabbitmq.ConnectionUtil;

public class Recv2 {

    private final static String QUEUE_NAME = "test_queue_topic_work_2";

    private final static String EXCHANGE_NAME = "test_exchange_topic";

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

        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

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

        // 绑定队列到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "*.*");

        // 同一时刻服务器只会发一条消息给消费者
        channel.basicQos(1);

        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列,手动返回完成
        channel.basicConsume(QUEUE_NAME, false, consumer);

        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [Recv2_x] Received '" + message + "'");
            Thread.sleep(10);

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

2. Springboot集成RabbitMQ

2.1. 简单队列

1、配置pom文件,主要是添加spring-boot-starter-amqp的支持

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

2、配置application.properties文件

配置rabbitmq的安装地址、端口以及账户信息

server:
  port: 8082
spring:
  application:
    name: spirng-boot-rabbitmq
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest

3、配置队列

package com.zpc.rabbitmq;

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

@Configuration
public class RabbitConfig {
//    durable:持久化,本来声明在内存,持久化会保存到本地,重启mq不会消失
//    Exclusive:排外,如果是,两个消费者不能同时消费,一般就只有一个消费者
//    autoDelete:自动删除,最后一个消费者断开的时候自动删除队列
    @Bean
    public Queue queue() {
        return new Queue("q_hello",true);
    }//持久化
}

4、发送者

package com.zpc.rabbitmq;

import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.Date;

@Component
public class HelloSender {
    @Autowired
    private AmqpTemplate rabbitTemplate;

    public void send() {
        String date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());//24小时制
        String context = "hello " + date;
        System.out.println("Sender : " + context);
        //简单对列的情况下routingKey即为Q名
        this.rabbitTemplate.convertAndSend("q_hello", context);
    }

}

5、接收者

package com.zpc.rabbitmq;

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

@Component
@RabbitListener(queues = "q_hello")
public class HelloReceiver {

    @RabbitHandler
    public void process(String hello) {
        System.out.println("Receiver1  : " + hello);
    }

}

6、测试

package com.zpc.rabbitmq;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

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

    @Autowired
    private HelloSender helloSender;

    @Test
    public void hello() throws Exception {
        helloSender.send();
    }
}

1.1. **多对多使用(**Work模式)

注册两个Receiver:

package com.zpc.rabbitmq;

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

@Component
@RabbitListener(queues = "q_hello")
public class HelloReceiver2 {

    @RabbitHandler
    public void process(String hello) throws InterruptedException {
        Thread.sleep(1000);
        System.out.println("Receiver2  : " + hello);
    }

}

在测试类里面加入

@Test
public void oneToMany() throws Exception {
    for (int i=0;i<100;i++){
        helloSender.send(i);
        Thread.sleep(200);
    }
}

在生产者里面加入

public void send(int i) {
    String date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());//24小时制
    String context = "hello " + i + " " + date;
    System.out.println("Sender : " + context);
    //简单对列的情况下routingKey即为Q名
    this.rabbitTemplate.convertAndSend("q_hello", context);
}

1.2. Topic Exchange(主题模式)

topic 是RabbitMQ中最灵活的一种方式,可以根据routing_key自由的绑定不同的队列

首先对topic规则配置,这里使用两个队列(消费者)来演示。

(1)配置队列,绑定交换机

package com.zpc.rabbitmq.topic;

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

@Configuration
public class TopicRabbitConfig {

    final static String message = "q_topic_message";
    final static String messages = "q_topic_messages";


    @Bean
    public Queue queueMessage() {
        return new Queue(TopicRabbitConfig.message);
    }

    @Bean
    public Queue queueMessages() {
        return new Queue(TopicRabbitConfig.messages);
    }

    /**
     * 声明一个Topic类型的交换机
     * @return
     */
    @Bean
    TopicExchange exchange() {
        return new TopicExchange("mybootexchange");
    }

    /**
     * 绑定Q到交换机,并且指定routingKey
     * @param queueMessage
     * @param exchange
     * @return
     */
    @Bean
    Binding bindingExchangeMessage(Queue queueMessage, TopicExchange exchange) {
        return BindingBuilder.bind(queueMessage).to(exchange).with("topic.message");
    }

    @Bean
    Binding bindingExchangeMessages(Queue queueMessages, TopicExchange exchange) {
        return BindingBuilder.bind(queueMessages).to(exchange).with("topic.#");
    }
}

(2)创建2个消费者

q_topic_message 和q_topic_messages

package com.zpc.rabbitmq.topic;

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

@Component
@RabbitListener(queues = "q_topic_message")
public class Receiver1 {

    @RabbitHandler
    public void process(String hello) {
        System.out.println("Receiver1  : " + hello);
    }

}

package com.zpc.rabbitmq.topic;

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

@Component
@RabbitListener(queues = "q_topic_messages")
public class Receiver2 {

    @RabbitHandler
    public void process(String hello) {
        System.out.println("Receiver2 : " + hello);
    }
}

(3)消息发送者(生产者)

package com.zpc.rabbitmq.topic;

import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class MsgSender {

    @Autowired
    private AmqpTemplate rabbitTemplate;

    public void send1() {
        String context = "hi, i am message 1";
        System.out.println("Sender : " + context);
        this.rabbitTemplate.convertAndSend("mybootexchange", "topic.message", context);
    }

    /**
     * 这个消息只有消费者2才能接受到
     */
    public void send2() {
        String context = "hi, i am messages 2";
        System.out.println("Sender : " + context);
        this.rabbitTemplate.convertAndSend("mybootexchange", "topic.messages", context);
    }
}

send1方法会匹配到topic.#和topic.message,两个Receiver都可以收到消息,发送send2只有topic.#可以匹配所有只有Receiver2监听到消息。

4)测试

package com.zpc.rabbitmq.topic;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

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

    @Autowired
    private MsgSender msgSender;

    @Test
    public void send1() throws Exception {
        msgSender.send1();
    }

    @Test
    public void send2() throws Exception {
        msgSender.send2();
    }
}

1.1. Fanout Exchange(订阅模式)

Fanout 就是我们熟悉的广播模式或者订阅模式,给Fanout交换机发送消息,绑定了这个交换机的所有队列都收到这个消息。

(1)配置队列,绑定交换机

package com.zpc.rabbitmq.fanout;

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

@Configuration
public class FanoutRabbitConfig {

    @Bean
    public Queue aMessage() {
        return new Queue("q_fanout_A");
    }

    @Bean
    public Queue bMessage() {
        return new Queue("q_fanout_B");
    }

    @Bean
    public Queue cMessage() {
        return new Queue("q_fanout_C");
    }

    @Bean
    FanoutExchange fanoutExchange() {
        return new FanoutExchange("mybootfanoutExchange");
    }

    @Bean
    Binding bindingExchangeA(Queue aMessage, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(aMessage).to(fanoutExchange);
    }

    @Bean
    Binding bindingExchangeB(Queue bMessage, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(bMessage).to(fanoutExchange);
    }

    @Bean
    Binding bindingExchangeC(Queue cMessage, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(cMessage).to(fanoutExchange);
    }

}

2)创建3个消费者

package com.zpc.rabbitmq.fanout;

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

@Component
@RabbitListener(queues = "q_fanout_A")
public class ReceiverA {

    @RabbitHandler
    public void process(String hello) {
        System.out.println("AReceiver  : " + hello + "/n");
    }

}
package com.zpc.rabbitmq.fanout;

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

@Component
@RabbitListener(queues = "q_fanout_B")
public class ReceiverB {

    @RabbitHandler
    public void process(String hello) {
        System.out.println("BReceiver  : " + hello + "/n");
    }
}
package com.zpc.rabbitmq.fanout;

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

@Component
@RabbitListener(queues = "q_fanout_C")
public class ReceiverC {

    @RabbitHandler
    public void process(String hello) {
        System.out.println("CReceiver  : " + hello + "/n");
    }

}

(3)生产者

package com.zpc.rabbitmq.fanout;

import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class MsgSenderFanout {

    @Autowired
    private AmqpTemplate rabbitTemplate;

    public void send() {
        String context = "hi, fanout msg ";
        System.out.println("Sender : " + context);
        this.rabbitTemplate.convertAndSend("mybootfanoutExchange","", context);
    }
}

4)测试

package com.zpc.rabbitmq.fanout;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

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

    @Autowired
    private MsgSenderFanout msgSender;

    @Test
    public void send1() throws Exception {
        msgSender.send();
    }
}

结果如下,三个消费者都收到消息:

AReceiver : hi, fanout msg

CReceiver : hi, fanout msg

BReceiver : hi, fanout msg

消息回调和消息公平分发:

消息回调:其实就是消息确认(生产者推送消息成功,消费这接收消息成功)。

有两个回调函数,一个叫 ConfirmCallback ,一个叫 RetrunCallback;

那么以上这两种回调函数都是在什么情况会触发呢?

先从总体的情况分析,推送消息存在四种情况:

①消息推送到server,但是在server里找不到交换机
②消息推送到server,找到交换机了,但是没找到队列
③消息推送到sever,交换机和队列啥都没找到
④消息推送成功

在rabbitmq-provider项目的application.yml文件上,加上消息确认的配置项后:

server:
  port: 8021
spring:
  #给项目来个名字
  application:
    name: rabbitmq-provider
  #配置rabbitMq 服务器
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: root
    password: root
    #消息确认配置项
    #确认消息已发送到交换机(Exchange)
    publisher-confirms: true
    #确认消息已发送到队列(Queue)
    publisher-returns: true

有两种方式:1.是在生产者实现 ConfirmCallback 和 RetrunCallback。 2.在配置文件来配置

这里使用的是第一种:

package com.lyp.rabbitmq;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.UUID;

//编写消息的生产者
@Component
public class MsgProducer implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 构造方法注入rabbitTemplate
     */
    @Autowired
    public MsgProducer(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
        rabbitTemplate.setConfirmCallback(this); //rabbitTemplate如果为单例的话,那回调就是最后设置的内容
    }

    public void sendMsg(String content) {
        CorrelationData correlationId = new CorrelationData(UUID.randomUUID().toString());
        //把消息放入ROUTINGKEY_A对应的队列当中去,对应的是队列A
        rabbitTemplate.convertAndSend(RabbitConfig.EXCHANGE_A, RabbitConfig.ROUTINGKEY_A, content, correlationId);
    }

    /**
     * 回调
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        System.out.println("confirm相关数据:" + correlationData + "\nack确认情况:" + ack + "\ncause原因:" + cause);
        logger.info(" 回调id:" + correlationData);
        if (ack) {
            logger.info("消息成功消费");
        } else {
            logger.info("消息消费失败:" + cause);
        }
    }

    @Override
    public void returnedMessage(Message message, int i, String s, String s1, String s2) {
        System.out.println("ReturnCallback:     " + "消息:" + message);
        System.out.println("ReturnCallback:     " + "回应码:" + i);
        System.out.println("ReturnCallback:     " + "回应信息:" + s);
        System.out.println("ReturnCallback:     " + "交换机:" + s1);
        System.out.println("ReturnCallback:     " + "路由键:" + s2);
    }
}

在RabbitConfig中如下:什么也没有配置,交换机也没有配置

注意:查看当前MQ服务器里面是否有spring-boot-routingKey_A交换机,如果有请删除掉

原因:因为生产者发送交换机调用RabbitConfig.EXCHANGE_A的常量

package com.lyp.rabbitmq;

//编写RabbitConfig类,类里面设置很多个EXCHANGE,QUEUE,ROUTINGKEY,是为了接下来的不同使用场景。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitConfig {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    public static final String EXCHANGE_A = "my-mq-exchange_A";
    public static final String EXCHANGE_B = "my-mq-exchange_B";
    public static final String EXCHANGE_C = "my-mq-exchange_C";


    public static final String QUEUE_A = "QUEUE_A";
    public static final String QUEUE_B = "QUEUE_B";
    public static final String QUEUE_C = "QUEUE_C";

    public static final String ROUTINGKEY_A = "spring-boot-routingKey_A";
    public static final String ROUTINGKEY_B = "spring-boot-routingKey_B";
    public static final String ROUTINGKEY_C = "spring-boot-routingKey_C";

}

①消息推送到server,但是在server里找不到交换机
写个测试接口,把消息推送到名为‘spring-boot-routingKey_A’的交换机上(这个交换机是没有创建没有配置的)

confirm相关数据:CorrelationData [id=7516cbe7-c55e-4d56-8e0e-19501f8ca8d0]
ack确认情况:false
cause原因:channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'my-mq-exchange_A' in vhost '/', class-id=60, method-id=40)

原因里面有说,没有找到交换机’my-mq-exchange_A’

所以:这种情况触发的是 ConfirmCallback 回调函数。

②消息推送到server,找到交换机了,但是没找到队列
这种情况就是需要新增一个交换机,但是不给这个交换机绑定队列,我来简单地在RabitConfig里面新增一个直连交换机,名叫‘my-mq-exchange_A’,但没给它做任何绑定配置操作:

//定义交换机,直连交换机,精确匹配
@Bean
public DirectExchange defaultExchange() {
    return new DirectExchange(EXCHANGE_A);
}

然后写个测试接口,把消息推送到名为‘‘my-mq-exchange_A’的交换机上(这个交换机是没有任何队列配置的):

我们查看控制台输出情况:

ReturnCallback:     消息:(Body: ...消息内容...)
ReturnCallback:     回应码:312
ReturnCallback:     回应信息:NO_ROUTE
ReturnCallback:     交换机:lonelyDirectExchange
ReturnCallback:     路由键:TestDirectRouting

ConfirmCallback:     相关数据:null
ConfirmCallback:     确认情况:true
ConfirmCallback:     原因:null

可以看到这种情况,两个函数都被调用了;
这种情况下,消息是推送成功到服务器了的,所以ConfirmCallback对消息确认情况是true;
而在RetrunCallback回调函数的打印参数里面可以看到,消息是推送到了交换机成功了,但是在路由分发给队列的时候,找不到队列,所以报了错误 NO_ROUTE 。
所以:②这种情况触发的是 ConfirmCallback和RetrunCallback两个回调函数。

③消息推送到sever,交换机和队列啥都没找到
这种情况其实一看就觉得跟①很像,没错 ,③和①情况回调是一致的,所以不做结果说明了。
所以: ③这种情况触发的是 ConfirmCallback 回调函数。

④消息推送成功
那么测试下,按照正常调用之前消息推送的接口就行,就调用下 /sendFanoutMessage接口,可以看到控制台输出:

ConfirmCallback:     相关数据:null
ConfirmCallback:     确认情况:true
ConfirmCallback:     原因:null	

所以: ④这种情况触发的是 ConfirmCallback 回调函数。

消息的确认机制

接下来我们继续, 消费者接收到消息的消息确认机制。

和生产者的消息确认机制不同,因为消息接收本来就是在监听消息,符合条件的消息就会消费下来。
所以,消息接收的确认机制主要存在三种模式:

①自动确认, 这也是默认的消息确认情况。 AcknowledgeMode.NONE
RabbitMQ成功将消息发出(即将消息成功写入TCP Socket)中立即认为本次投递已经被正确处理,不管消费者端是否成功处理本次投递。
所以这种情况如果消费端消费逻辑抛出异常,也就是消费端没有处理成功这条消息,那么就相当于丢失了消息。
一般这种情况我们都是使用try catch捕捉异常后,打印日志用于追踪数据,这样找出对应数据再做后续处理。

② 不确认, 这个不做介绍
③ 手动确认 , 这个比较关键,也是我们配置接收消息确认机制时,多数选择的模式。
消费者收到消息后,手动调用basic.ack/basic.nack/basic.reject后,RabbitMQ收到这些消息后,才认为本次投递成功。
basic.ack用于肯定确认
basic.nack用于否定确认(注意:这是AMQP 0-9-1的RabbitMQ扩展)
basic.reject用于否定确认,但与basic.nack相比有一个限制:一次只能拒绝单条消息
消费者端以上的3个方法都表示消息已经被正确投递,但是basic.ack表示消息已经被正确处理,但是basic.nack,basic.reject表示没有被正确处理,但是RabbitMQ中仍然需要删除这条消息。

说了这么多,我们来看看公平分发是如何实现的:

配置文件:改成手动模式并且消费者最大正在处理的消息数量为1

server:
  port: 8083
spring:
  application:
    name: spirng-boot-rabbitmq
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: admin
    password: admin
    #消息确认配置项

    #确认消息已发送到交换机(Exchange)
    publisher-confirms: true
    #确认消息已发送到队列(Queue)
    publisher-returns: true
#    # 全局开启ACK
    listener:
      simple:
        acknowledge-mode: MANUAL # 来配置ack模式,分别为NONE、MANUAL、AUTO。I:NONE:默认为NONE,自动ack模式,II:MANUAL:即为手动ack模式:AUTO:自动确认ack 如果此时消费者抛出异常,不同的异常会有不同的处理方式。
###     在单个请求处理的消息个数
        prefetch: 1

在RabbitConfig中:

package com.lyp.rabbitmq;

//编写RabbitConfig类,类里面设置很多个EXCHANGE,QUEUE,ROUTINGKEY,是为了接下来的不同使用场景。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitConfig {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    public static final String EXCHANGE_A = "my-mq-exchange_A";
    public static final String EXCHANGE_B = "my-mq-exchange_B";
    public static final String EXCHANGE_C = "my-mq-exchange_C";


    public static final String QUEUE_A = "QUEUE_A";
    public static final String QUEUE_B = "QUEUE_B";
    public static final String QUEUE_C = "QUEUE_C";

    public static final String ROUTINGKEY_A = "spring-boot-routingKey_A";
    public static final String ROUTINGKEY_B = "spring-boot-routingKey_B";
    public static final String ROUTINGKEY_C = "spring-boot-routingKey_C";
    
    //定义交换机,直连交换机,精确匹配
    @Bean
    public DirectExchange defaultExchange() {
        return new DirectExchange(EXCHANGE_A);
    }

    /**
     * 创建队列
     * 获取队列A
     *
     * @return
     */
    @Bean
    public Queue queueA() {
        return new Queue(QUEUE_A, true, false, false); //队列持久
    }

    /**
     * 建立关系,交换机和+队列  绑定起来
     * ROUTINGKEY_A路由的key
     *
     * @return
     */
    @Bean
    public Binding binding() {
        return BindingBuilder.bind(queueA()).to(defaultExchange()).with(RabbitConfig.ROUTINGKEY_A);
    }

}

生产者类

package com.lyp.rabbitmq;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.UUID;

//编写消息的生产者
@Component
public class MsgProducer implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 构造方法注入rabbitTemplate
     */
    @Autowired
    public MsgProducer(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
        rabbitTemplate.setConfirmCallback(this); //rabbitTemplate如果为单例的话,那回调就是最后设置的内容
    }

    public void sendMsg(String content) {
        CorrelationData correlationId = new CorrelationData(UUID.randomUUID().toString());
        //把消息放入ROUTINGKEY_A对应的队列当中去,对应的是队列A
        rabbitTemplate.convertAndSend(RabbitConfig.EXCHANGE_A, RabbitConfig.ROUTINGKEY_A, content, correlationId);
    }

    /**
     * 回调
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        System.out.println("confirm相关数据:" + correlationData + "\nack确认情况:" + ack + "\ncause原因:" + cause);
        logger.info(" 回调id:" + correlationData);
        if (ack) {
            logger.info("消息成功消费");
        } else {
            logger.info("消息消费失败:" + cause);
        }
    }

    @Override
    public void returnedMessage(Message message, int i, String s, String s1, String s2) {
        System.out.println("ReturnCallback:     " + "消息:" + message);
        System.out.println("ReturnCallback:     " + "回应码:" + i);
        System.out.println("ReturnCallback:     " + "回应信息:" + s);
        System.out.println("ReturnCallback:     " + "交换机:" + s1);
        System.out.println("ReturnCallback:     " + "路由键:" + s2);
    }
}

消费者1:

package com.lyp.rabbitmq.Msg;

import com.lyp.rabbitmq.RabbitConfig;
import com.rabbitmq.client.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
@RabbitListener(queues = RabbitConfig.QUEUE_A)
public class MsgReceiverC_one {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @RabbitHandler
    public void process(String msg, Channel channel, Message message) throws InterruptedException, IOException {

        Thread.sleep(1000);
        logger.info("消费者1接收处理队列A当中的消息: " + msg);
        try {
            //告诉服务器收到这条消息 已经被我消费了 可以再队列删除;否则消息服务器以为这条消息没处理掉  后续还会再发
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
            logger.info("已经消费掉receiver success" + msg);
        } catch (Exception e) {
//            e.printStackTrace();
//            重新放入队列
//            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
            //丢弃这条消息
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
            logger.info("未消费信息receiver fail");
        }


    }

}

消费者2

package com.lyp.rabbitmq.Msg;

import com.lyp.rabbitmq.RabbitConfig;
import com.rabbitmq.client.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
@RabbitListener(queues = RabbitConfig.QUEUE_A)
public class MsgReceiverC_two {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @RabbitHandler
    public void process(String msg, Channel channel, Message message) throws InterruptedException, IOException {

        Thread.sleep(20);
        logger.info("消费者2接收处理队列A当中的消息: " + msg);

        try {
            //告诉服务器收到这条消息 已经被我消费了 可以再队列删除;否则消息服务器以为这条消息没处理掉  后续还会再发
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
            logger.info("已经消费receiver success" + msg);
        } catch (Exception e) {
//            e.printStackTrace();
//            重新放入队列
//            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
            //丢弃这条消息
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
            logger.info("未消费receiver fail");
        }
    }


}

测试:

package com.lyp.rabbitmq.controller;

import com.lyp.rabbitmq.MsgProducer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RammitController {

    @Autowired
    private MsgProducer messageSender;

    @RequestMapping("/boot/send")
    public String send(String name) {
        System.out.println(name);
        for (int i = 0; i < 1; i++) {
            messageSender.sendMsg("x1");
        }
        return "hello word";
    }

}

公平分发模式完成

关于死信队列

在大多数的MQ中间件中,都有死信队列的概念。死信队列同其他的队列一样都是普通的队列。在RabbitMQ中并 没有特定的“死信队列”类型,而是通过配置,将其实现。
当我们在创建一个业务的交换机和队列的时候,可以配置参数,指明另一个队列为当前队列的死信队列,在RabbitMQ中,死信队列(严格的说应该是死信交换机)被称为DLX Exchange。当消息“死掉”后,会被自动路由到DLX Exchange的queue中。

什么样的消息会进入死信队列?
1.消息的TTL过期。
2.消费者对broker应答Nack,并且消息禁止重回队列。
3.Queue队列长度已达上限。

场景描述:

当用户下单后,状态为待支付,假如在规定的过期时间内尚未支付金额,那么就应该设置订单状态为取消。在不用MQ的情况下,我们可以设置一个定时器,每秒轮询数据库查找超出过期时间且未支付的订单,然后修改状态,但是这种方式会占用很多资源,所以在这里我们可以利用RabbitMQ的死信队列。

下面是简单案例:

引用依赖

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

配置文件yml

server:
  port: 8083
spring:
  application:
    name: spirng-boot-rabbitmq
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: admin
    password: admin
    #消息确认配置项

    #确认消息已发送到交换机(Exchange)
    publisher-confirms: true
    #确认消息已发送到队列(Queue)
    publisher-returns: true
#    # 全局开启ACK
    listener:
      simple:
        acknowledge-mode: MANUAL # 来配置ack模式,分别为NONE、MANUAL、AUTO。I:NONE:默认为NONE,自动ack模式,II:MANUAL:即为手动ack模式:AUTO:自动确认ack 如果此时消费者抛出异常,不同的异常会有不同的处理方式。
##        #当前监听容器数
        concurrency: 1
##        #最大数
        max-concurrency: 10
###     在单个请求处理的消息个数
        prefetch: 1
        retry:
          # 允许消息消费失败的重试
          enabled: true
          #  # 消息最多消费次数3次
          max-attempts: 3
          #  # 消息多次消费的间隔1秒
          initial-interval: 1000
          #  #  设置为false,会丢弃消息或者重新发布到死信队列
        default-requeue-rejected: true

消息的生产者:消息的过期时间,这里使用的第二种方式

第一种是声明队列的时候,在队列的属性中设置,缺点:这样该队列中的消息都会有相同的有效期;
第二种是发送消息时给消息设置属性,可以为每条消息都设置不同的TTL。

package com.lyp.rabbitmq2.rabbitmqeadLetter2;

import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.UUID;

@Component
public class OrderController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void sendMessage(String orderNo){
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());// 生成一个消息的唯一id,可不选
        // 声明消息处理器  设置消息的编码以及消息的过期时间  时间毫秒值 为字符串
        MessagePostProcessor messagePostProcessor = message -> {
            MessageProperties messageProperties = message.getMessageProperties();
            // 设置编码
            messageProperties.setContentEncoding("utf-8");
            // 设置过期时间 一分钟
            int expiration = 60000;
            messageProperties.setExpiration(String.valueOf(expiration));
            return message;
        };
        // 向ORDER_DL_EXCHANGE 发送消息  形成死信   在OrderQueueReceiver类处理死信交换机转发给转发队列的信息
        rabbitTemplate.convertAndSend(RabbitmqConfig2.ORDER_DL_EXCHANGE, RabbitmqConfig2.DL_KEY, orderNo, messagePostProcessor, correlationData);
        System.out.println(new Date() +  "发送消息,订单号为" + orderNo);
    }
}

​ 创建Queue以及Exchange创建和绑定

package com.lyp.rabbitmq2.rabbitmqeadLetter2;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;

/**
 * @Author: ZJH
 * @Date: 2019/3/7 15:35
 */
@Configuration
public class RabbitmqConfig2 {


    /**
     * 订单死信队列交换机标识符  属性值不能改,写死
     */
    private static final String ORDER_DEAD_LETTER_QUEUE_KEY = "x-dead-letter-exchange";
    /**
     * 订单死信队列交换机绑定键 标识符  属性值不能改,写死
     */
    private static final String ORDER_DEAD_LETTER_ROUTING_KEY = "x-dead-letter-routing-key";
    //交换机
    public static final String ORDER_DL_EXCHANGE="ORDER_DL_EXCHANGE";
    //队列
    public static final String ORDER_DL_QUEUE="ORDER_DL_QUEUE";
    //死信队列
    public static final String ORDER_REDIRECT_QUEUE="ORDER_REDIRECT_QUEUE";
    //路由
    public static final String DL_KEY="DL_KEY";
    //死信的路由
    public static final String RED_KEY="RED_KEY";
        //队列的最大长度,属性值不能修改,写死
    private static final String XMAXLENGTH="x-max-length";
    //队列的最大字符,属性值不能修改,写死
    private static final String XMAXLENGTHBYTES="x-max-length-bytes";
    //----------------------------订单死信定义------------------------------
    // 订单过期流程: 消息(创建的订单号)---》发送到订单死信队列,不消费(设置过期时间)---》(超过设定的过期时间)根据ORDER_DEAD_LETTER_QUEUE_KEY路由死信交换机 ---》重新消费,根据ORDER_DEAD_LETTER_ROUTING_KEY转发到转发队列(取出消息订单号查找订单,假如仍然未支付就取消订单)---》end

    /**
     * orderDeadLetterExchange(direct类型交换机)
     */
    @Bean
    public Exchange orderDeadLetterExchange() {
        return ExchangeBuilder.directExchange(RabbitmqConfig2.ORDER_DL_EXCHANGE).durable(true).build();
    }

    /**
     * 声明一个订单死信队列.
     * x-dead-letter-exchange   对应  死信交换机
     * x-dead-letter-routing-key  对应 死信队列
     */
    @Bean
    public Queue orderDeadLetterQueue() {
        // 参数
        Map<String, Object> args = new HashMap<>(3);
        // 出现dead letter之后将dead letter重新发送到指定exchange
        args.put(ORDER_DEAD_LETTER_QUEUE_KEY, RabbitmqConfig2.ORDER_DL_EXCHANGE);
        // 出现dead letter之后将dead letter重新按照指定的routing-key发送
        args.put(ORDER_DEAD_LETTER_ROUTING_KEY, RabbitmqConfig2.RED_KEY);
        //队列的最大长度
        args.put(XMAXLENGTH, 10);
        // name队列名字  durable是否持久化,true保证消息的不丢失, exclusive是否排他队列,如果一个队列被声明为排他队列,该队列仅对首次申明它的连接可见,并在连接断开时自动删除, autoDelete如果该队列没有任何订阅的消费者的话,该队列是否会被自动删除, arguments参数map
        return new Queue(RabbitmqConfig2.ORDER_DL_QUEUE,true,false,false, args);
    }

    /**
     * 定义订单死信队列转发队列.
     */
    @Bean
    public Queue orderRedirectQueue() {
        return new Queue(RabbitmqConfig2.ORDER_REDIRECT_QUEUE,true,false,false);
    }

    /**
     * 死信路由通过 DL_KEY 绑定键绑定到订单死信队列上.
     */
    @Bean
    public Binding orderDeadLetterBinding() {
        return new Binding(RabbitmqConfig2.ORDER_DL_QUEUE, Binding.DestinationType.QUEUE, RabbitmqConfig2.ORDER_DL_EXCHANGE, RabbitmqConfig2.DL_KEY, null);

    }

    /**
     * 死信路由通过 KEY_R 绑定键绑定到订单转发队列上.
     */
    @Bean
    public Binding orderRedirectBinding() {
        return new Binding(RabbitmqConfig2.ORDER_REDIRECT_QUEUE, Binding.DestinationType.QUEUE, RabbitmqConfig2.ORDER_DL_EXCHANGE, RabbitmqConfig2.RED_KEY, null);
    }
}

消息的消费者:

package com.lyp.rabbitmq2.rabbitmqeadLetter2;

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

import java.io.IOException;
import java.util.Date;

@Component
//@RabbitListener(queues = RabbitmqConfig2.ORDER_REDIRECT_QUEUE)
public class MesageReceiver {


    /**
     * 监听转发队列  走逻辑判断,尚未支付且超过过期时间的订单号设置为失效订单
     *
     * @param message 信息包装类
     * @param channel 通道
     */
    @RabbitListener(queues = RabbitmqConfig2.ORDER_REDIRECT_QUEUE)
    public void redirect(Message message, Channel channel) throws IOException {
        // 从队列中取出订单号
        byte[] body = message.getBody();
        String orderNo = new String(body, "UTF-8");
        System.out.println(new Date() + "消费消息,订单号为" + orderNo);
        // 确认消息有没有被收到,false表示手动确认  在处理完消息时,返回应答状态
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }
}

测试:启动一次,可以查看下图,等待1分钟,再次启动查看测试

package com.lyp.rabbitmq2.rabbitmqeadLetter2;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

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


    @Autowired
    private OrderController orderController;


    @Test
    public void testSendDeadLetterQueue() throws InterruptedException {
        for (int i = 0; i <15 ; i++) {
            orderController.sendMessage("666");
        }

    }



}

测试结果:一共10条记录

Wed Nov 13 15:53:32 CST 2019消费消息,订单号为666
Wed Nov 13 15:53:32 CST 2019消费消息,订单号为666
Wed Nov 13 15:53:32 CST 2019消费消息,订单号为666
省略…

发送消息时可以看到rabbitmq管理界面的ORDER_DL_QUEUE队列有一条待消费的消息,然后在60秒过期后变成死信队列发送至ORDER_DL_EXCHANGE交换器,然后交换器根据路由转发到ORDER_REDIRECT_QUEUE队列,并被监听消费掉。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0qmSUuUa-1573723272496)(C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\Queue.jpg)]

延迟时间过之后:测试消息只有在死信队列里面才会被消费

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RDBRwHa4-1573723272496)(C:\Users\Hasee\AppData\Local\Temp\ksohtml18452\Queue2.png)]

遇到的问题:

消息的消费者MesageReceiver使用@RabbitListener注解为什么加在类上不能使用报错,加在方法上就没用问题

事务:
spring:
  application:
    name: spirng-boot-rabbitmq
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: admin
    password: admin
    #消息确认配置项

    #确认消息已发送到交换机(Exchange)
    publisher-confirms: true
    #确认消息已发送到队列(Queue)
    publisher-returns: true
#    # 全局开启ACK
    listener:
      direct:
        acknowledge-mode: manual
      simple:
        acknowledge-mode: MANUAL

spring.rabbitmq.publisher-confirms一定要配置为false,否则会与事务处理相冲突,启动时会报异常。

Configuration配置

    /**
     * 配置启用rabbitmq事务
     *
     * @param connectionFactory
     * @return
     */
    @Bean
    public RabbitTransactionManager rabbitTransactionManager(CachingConnectionFactory connectionFactory) {
        return new RabbitTransactionManager(connectionFactory);
    }

本类用于配置RabbitMQ的Exchange和Queue,同时声明了事务管理器(这个很重要)。

消息的发送者:

package com.lyp.rabbitmq.transaction;

import com.lyp.rabbitmq.RabbitConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.PostConstruct;
import java.util.UUID;


/**
 * RabbitMQ消息发送类
 */
@Component
public class RabbitSender implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {

    private static final Logger logger = LoggerFactory.getLogger("gateway_mq");

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostConstruct
    private void init() {
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnCallback(this);
        rabbitTemplate.setChannelTransacted(true);
    }

    @Transactional
    public void sendIngateQueue(String msg) {
        CorrelationData correlationId = new CorrelationData(UUID.randomUUID().toString());
        logger.info("进闸支付消息已发送 {}", msg);
        rabbitTemplate.convertAndSend(RabbitConfig.EXCHANGE_A, RabbitConfig.ROUTINGKEY_A, msg, correlationId);

    }

    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            logger.info("消息已确认 cause:{} - {}", cause, correlationData.toString());
        } else {
            logger.info("消息未确认 cause:{} - {}", cause, correlationData.toString());
        }
    }

    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        logger.info("消息被退回 {}", message.toString());
    }

}

本类用于统一向RabbitMQ发送消息,在发送消息的sendIngateQueue()方法上,加了@Transactional注解,表示这个方法将启用事务(此时的事务即是RabbitMQ事务,因为前面定义了RabbitTransactionManager )。
由于启用了事务,所以需要在系统初始化时,调用rabbitTemplate.setChannelTransacted(true),以激活rabbitTemplate对象事务处理功能。
编写消息的消费者:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Component;

import com.alibaba.dubbo.config.annotation.Reference;
import com.rabbitmq.client.Channel;
import com.xxxx.gateway.model.TradePayModelRes;

@Component
@RabbitListener(queues = "${mq.ingate.queue}")
public class IngateConsumer {

	private static Logger logger = LoggerFactory.getLogger("gateway_mq");
	
	@RabbitHandler
	public void process(TradePayModelRes tradePayModelRes, Channel channel, Message message) {
		logger.info("收到进闸支付消息 {}",tradePayModelRes.toString());
		try {
				//do samothing
				channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
				logger.info("确认消费进闸支付消息 {}",tradePayModelRes.getOutTradeNo());
		} catch (Exception e) {
				logger.info("进闸支付入库异常 {} - {}",tradePayModelRes.getOutTradeNo(),e);
		}
	}
}

至此,配置完成。
发送消息时,调用rabbitSender.sendIngateQueue()方法,即可启用事务发送消息机制,以保证消息不丢失。

2. 总结

使用MQ实现商品数据的同步优势:

1、 降低系统间耦合度

2、 便于管理数据的同步(数据一致性)

3、流量过大做一个缓冲的作用(服务器减轻压力)

拓展:

创建交换机的参数:

public DirectExchange(String name, boolean durable, boolean autoDelete)//name:交换机的名称
//durable:是否持久化
//autoDelete:自动删除
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值