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:自动删除