✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
RabbitMQ提供了6种消息模型,但是第6种其实是RPC,并不是MQ,因此我们只学习前5种
在线手册:https://www.rabbitmq.com/getstarted.htm
5种消息模型,大体分为两类:
P2P(point to point)模式包含三个角色:
消息队列(queue),生产者(sender),消费者(receiver)
每个消息发送到一个特定的队列中,消费者从中获得消息
队列中保留这些消息,直到他们被消费或超时
特点:
publish(Pub)/subscribe(Sub):
pub/sub模式包含三个角色:交换机(exchange),发布者(publisher),订阅者
(subcriber)(没有队列是因为,队列只需要在消费者绑定路由即可)
多个发布者将消息发送交换机,系统将这些消息传递给多个订阅者
特点:
下面引用官网的一段介绍:
Introduction
RabbitMQ is a message broker: it accepts and forwards messages. You can think about it as
a post office: when you put the mail that you want posting in a post box, you can be sure
that Mr. or Ms. Mailperson will eventually deliver the mail to your recipient. In this analogy,
RabbitMQ is a post box, a post office and a postman.
译文:RabbitMQ是一个消息代理:它接收和转发消息。你可以把它想象成一个邮局:当你把你想要
寄的邮件放到一个邮箱里,你可以确定邮递员先生或女士最终会把邮件送到你的收件人那里。在
这个类比中,RabbitMQ是一个邮箱、一个邮局和一个邮递员。
RabbitMQ本身只是接收,存储和转发消息,并不会对信息进行处理!
类似邮局,处理信件的应该是收件人而不是邮局!
生产者代码:
package simplemode;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import util.ConnectionUtil;
/**
* @author WeiHong
* @date 2021 - 09 - 06 16:39
* 简单模式
* 消息生产者
*/
public class Sender {
public static void main(String[] args) throws Exception {
String msg ="学习rabbitMQ,ACK手动确认!";
//1.获取连接
Connection connection = ConnectionUtil.getConnection();
//2.在连接中创建通道
Channel channel = connection.createChannel();
//3.创建消息队列(1、2、3、4、5)
/**
* 参数1:队列的名称
* 参数2:队列中的数据是否持久化
* 参数3:是否排外(是否支持扩展,当前队列只能自己用,不能给别人用)
* 参数4:是否自动删除(当队列连接数为0时,队列会销毁,不管队列是否存有保存数据)
* 参数5:队列参数(没有参数为null)
*/
channel.queueDeclare("queue1",false,false,false,null);
//4.向指定的队列发送消息
/**
* 参数1:交换机名称,当前为简单模式,也就是p2p模式,没有交换机,所以名称为“”;
* 参数2:目标队列的名称
* 参数3:设置消息的属性(没有属性则为null)
* 参数4:消息的内容(只接收字节数组)
*/
channel.basicPublish("","queue1",null,msg.getBytes());
System.out.println("发送:"+msg);
//5.释放资源
channel.close();
connection.close();
}
}
启动生产者,即可前往管理端查看队列中的信息,会有一条信息没有处理和确认
消费者代码:
package simplemode;
import com.rabbitmq.client.*;
import util.ConnectionUtil;
import java.io.IOException;
/**
* @author WeiHong
* @date 2021 - 09 - 06 17:00
* 消息接收者
*/
public class Recer {
public static void main(String[] args)throws Exception{
// 1.获取连接
Connection connection = ConnectionUtil.getConnection();
// 2.在连接中创建通道(信道)
Channel channel = connection.createChannel();
// 3.从信道中获得信息
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override//交付处理(收件人信息,信封(包裹上的快递标签),协议的配置,消息)
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//body就是从队列中获取的消息
String s= new String(body);
System.out.println("接收="+s);
}
};
// 4.监听队列 true:自动消息确认
channel.basicConsume("queue1",true,consumer);
}
}
启动消费者,前往管理端查看队列中的信息,所有信息都已经处理和确认,显示0
消息确认机制ACK:
// 4.监听队列 false:手动消息确认
channel.basicConsume("queue1", false,consumer);
确认ACK代码如下:
package simplemode;
import com.rabbitmq.client.*;
import util.ConnectionUtil;
import java.io.IOException;
/**
* @BelongsProject: lagou-rabbitmq
* @Author: GuoAn.Sun
* @CreateTime: 2020-08-10 15:08
* @Description: 消息接收者,加入ACK确认机制
*/
public class RecerByACK {
public static void main(String[] args) throws Exception {
// 1.获得连接
Connection connection = ConnectionUtil.getConnection();
// 2.获得通道(信道)
final Channel channel = connection.createChannel();
// 3.从信道中获得消息
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override //交付处理(收件人信息,包裹上的快递标签,协议的配置,消息)
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// body就是从队列中获取的消息
String s = new String(body);
System.out.println("接收 = " + s);
// 手动确认(收件人信息,是否同时确认多个消息)
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
// 4.监听队列 false:手动消息确认
channel.basicConsume("queue1", false,consumer);
}
}
ACK确认模式:
*// 4.监听队列 false:手动消息确认,必须为手动消息确认,ACK确认机制才生效。
*channel.basicConsume(“queue1”,fasle,consumer);
没执行channel.basicAck(envelope.getDeliveryTag(),false);该语句时消息是未被确认的。
之前我们学习的简单模式,一个消费者来处理消息,如果生产者生产消息过快过多,而消费者的能力有限,就会产生消息在队列中堆积(生活中的滞销)
一个烧烤师傅,一次烤50支羊肉串,就一个人吃的话,烤好的肉串会越来越多,怎么处理?
多招揽客人进行消费即可。当我们运行许多消费者程序时,消息队列中的任务会被众多消费者共享,但其中某一个消息只会被一个消费者获取(100支肉串20个人吃,但是其中的某支肉串只能被一个人吃)
生产者代码:
package workmode;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import util.ConnectionUtil;
/**
* @author WeiHong
* @date 2021 - 09 - 06 19:23
* 工作模型
*/
public class Sender {
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("work_queue",false,false,false,null);
for(int i = 1;i<=100;i++) {
String msg = "羊肉串 --> " + i;
channel.basicPublish("", "work_queue", null, msg.getBytes());
System.out.println("新鲜出炉:" + msg);
}
channel.close();
connection.close();
}
}
消费者1代码:
package workmode;
import com.rabbitmq.client.*;
import util.ConnectionUtil;
import java.io.IOException;
/**
* @author WeiHong
* @date 2021 - 09 - 06 21:22
*/
public class Recer1 {
static int i =1;
public static void main(String[] args)throws Exception{
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
//声明队列、双重含义
// 如果队列不存在,就创建;如果队列存在,则获取
channel.queueDeclare("work_queue",false,false,false,null);
channel.basicQos(1);//系统不会给消费者发送超过1个消息以上,只有确认ACK后,才回继续发下一个消息
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//body就是从队列中获取的消息
String s= new String(body);
System.out.println("【顾客1】吃掉 " + s+" ! 总共吃【"+i+++"】串!"+"信封收件人:"+envelope.getDeliveryTag());
//模拟网络延迟
try {
Thread.sleep(200);
}catch (Exception e){
e.printStackTrace();
}
//手动确认(收件人信息,是否确认多个消息)
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
// 4.监听队列 false:手动消息确认
channel.basicConsume("work_queue", false,consumer);
}
}
消费者2代码:
package workmode;
import com.rabbitmq.client.*;
import util.ConnectionUtil;
import java.io.IOException;
/**
* @BelongsProject: lagou-rabbitmq
* @Author: GuoAn.Sun
* @CreateTime: 2020-08-10 15:08
* @Description: 消息接收者,加入ACK确认机制
*/
public class Recer2 {
static int i =1;
public static void main(String[] args) throws Exception {
// 1.获得连接
Connection connection = ConnectionUtil.getConnection();
// 2.获得通道(信道)
final Channel channel = connection.createChannel();
channel.queueDeclare("work_queue",false,false,false,null);
channel.basicQos(1);
// 3.从信道中获得消息
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override //交付处理(收件人信息,包裹上的快递标签,协议的配置,消息)
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String s = new String(body);
System.out.println("【顾客2】吃掉 " + s+" ! 总共吃【"+i+++"】串!");
// 模拟网络延迟
try{
Thread.sleep(900);
}catch (Exception e){
}
// 手动确认(收件人信息,是否同时确认多个消息)
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
// 4.监听队列 false:手动消息确认
channel.basicConsume("work_queue", false,consumer);
}
}
结果:
生产者产生100个羊肉串
消费者1消费了81条消息
消费者2消费了19条消息
channel.basicQos:系统不会给消费者发送超过1个消息以上,只有确认ACK后,才回继续发下一个消息
能者多劳必须要配合手动的ACK机制才生效:
workqueue,多个消费者监听同一个队列
接收到消息后,通过线程池,异步消费
发布-订阅
在上一篇教程中,我们创建了一个工作队列。工作队列背后的假设是,每个任务都被准确地交
付给一个工作者。在这一部分中,我们将做一些完全不同的事情——将消息传递给多个消费者。
此模式称为“发布/订阅”。
为了演示这个模式,我们将构建一个简单的日志记录系统。它将由两个程序组成——第一个将
发送日志消息,第二个将接收和打印它们。
在我们的日志系统中,接收程序的每一个正在运行的副本都将获得消息。这样我们就可以运行
一个接收器并将日志指向磁盘;与此同时,我们可以运行另一个接收器并在屏幕上看到日志。
基本上,发布的日志消息将广播到所有接收方
生活中的案例:就是玩抖音快手,众多粉丝关注一个视频主,视频主发布视频,所有粉丝都可以得到视
频通知
上图中,X就是视频主,红色的队列就是粉丝。binding是绑定的意思(关注)
P生产者发送信息给X路由,X将信息转发给绑定X的队列
X队列将信息通过信道发送给消费者,从而进行消费
整个过程,必须先创建路由
路由在生产者程序中创建
因为路由没有存储消息的能力,当生产者将信息发送给路由后,消费者还没有运行,所以没
有队列,路由并不知道将信息发送给谁
运行程序的顺序:
1.先运行生产者sender;创建路由
2在运行消费者Recer1、Recer2。
生产者代码
package publishSubscribeMode;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import util.ConnectionUtil;
/**
* @author WeiHong
* @date 2021 - 09 - 07 14:11
* 发布订阅模式
*/
public class Sender {
public static void main(String[] args) throws Exception {
// 1.获取连接
Connection connection = ConnectionUtil.getConnection();
// 2.在连接中创建信道
Channel channel = connection.createChannel();
// 3.声明路由(4种路由)
// fanout:不处理路由键(只需要将队列绑定到路由上,发送到路由的消息都会被转发到与该路由绑定的所有队列上)
channel.exchangeDeclare("test_exchange_fanout","fanout");
// 4.发布消息
String msg = "这是发布订阅模式的fanout路由模型的消息";
channel.basicPublish("test_exchange_fanout","",null,msg.getBytes());
System.out.println("发送:"+msg);
// 5.释放资源
channel.close();
connection.close();
}
}
消费者1代码
package publishSubscribeMode;
import com.rabbitmq.client.*;
import util.ConnectionUtil;
import java.io.IOException;
/**
* @author WeiHong
* @date 2021 - 09 - 07 15:29
* 发布订阅模式-消费者1
*/
public class Recer1 {
public static void main(String[] args)throws Exception{
// 1.获取连接
Connection connection = ConnectionUtil.getConnection();
// 2.在链接中创建信道
Channel channel = connection.createChannel();
//2.1声明队列
channel.queueDeclare("test_exchange_fanout_queue1",false,false,false,null);
//2.2绑定路由
channel.queueBind("test_exchange_fanout_queue1","test_exchange_fanout","");
// 3.定义内部类重写方法接收消息\从信道中获得信息
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String s = new String(body);
System.out.println("[消费者1] = "+ s);
}
};
// 4.监听队列 true:自动消息确认
channel.basicConsume("test_exchange_fanout_queue1",true,consumer);
}
}
消费者2代码
package publishSubscribeMode;
import com.rabbitmq.client.*;
import util.ConnectionUtil;
import java.io.IOException;
/**
* @author WeiHong
* @date 2021 - 09 - 07 15:29
* 发布订阅模式-消费者2
*/
public class Recer2 {
public static void main(String[] args)throws Exception{
// 1.获取连接
Connection connection = ConnectionUtil.getConnection();
// 2.在链接中创建信道
Channel channel = connection.createChannel();
//2.1声明队列
channel.queueDeclare("test_exchange_fanout_queue2",false,false,false,null);
//2.2绑定路由
channel.queueBind("test_exchange_fanout_queue2","test_exchange_fanout","");
// 3.定义内部类重写方法接收消息\从信道中获得信息
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String s = new String(body);
System.out.println("[消费者2] = "+ s);
}
};
// 4.监听队列 true:自动消息确认
channel.basicConsume("test_exchange_fanout_queue2",true,consumer);
}
}
生产者代码
package routerMode;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import util.ConnectionUtil;
/**
* @author WeiHong
* @date 2021 - 09 - 09 14:38
* 路由模式-生产者
* 路由会根据类型进行定向分发消息给不同的队列
*/
public class Sender {
public static void main(String[] args) throws Exception {
//1.获取连接
Connection connection = ConnectionUtil.getConnection();
//2.在连接中创建信道
Channel channel = connection.createChannel();
//3.路由声明(路由名,路由类型)
//direct:会根据路由键进行定向分发消息
channel.exchangeDeclare("router_exchange_direct","direct");
//4.向队列发送信息
for(int i = 0; i < 10 ; i++){
String msg = "查询的数据第"+i+"条消息!";
//selecte:此时该参数为路由键不是队列名
channel.basicPublish("router_exchange_direct","selecte",null,msg.getBytes());
System.out.println("生产者发送="+msg);
}
//5.释放连接
channel.close();
connection.close();
}
}
消费者1代码
package routerMode;
import com.rabbitmq.client.*;
import util.ConnectionUtil;
import java.io.IOException;
/**
* @author WeiHong
* @date 2021 - 09 - 09 15:05
*/
public class Recer1 {
public static void main(String[] args) throws Exception {
//1.获取连接
Connection connection = ConnectionUtil.getConnection();
//2.在链接中创建信道
Channel channel = connection.createChannel();
//2.1声明队列
channel.queueDeclare("exchange_direct_queue1",false,false,false,null);
//2.2绑定路由(如果路由键的类型是插入、修改绑定到这个队列1上)
channel.queueBind("exchange_direct_queue1","router_exchange_direct","insert");
channel.queueBind("exchange_direct_queue1","router_exchange_direct","update");
//3.定义内部类获取信息
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String res = new String(body);
System.out.println("【消费者1】="+ res);
}
};
//4.监听队列 true:自动消息确认
channel.basicConsume("exchange_direct_queue1",true,consumer);
}
}
消费者2代码
package routerMode;
import com.rabbitmq.client.*;
import util.ConnectionUtil;
import java.io.IOException;
/**
* @author WeiHong
* @date 2021 - 09 - 09 15:05
*/
public class Recer2 {
public static void main(String[] args) throws Exception {
//1.获取连接
Connection connection = ConnectionUtil.getConnection();
//2.在链接中创建信道
Channel channel = connection.createChannel();
//2.1声明队列
channel.queueDeclare("exchange_direct_queue2",false,false,false,null);
//2.2绑定路由(如果路由键的类型是查询绑定到这个队列2上)
channel.queueBind("exchange_direct_queue2","router_exchange_direct","selecte");
//3.定义内部类获取信息
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String res = new String(body);
System.out.println("【消费者2】="+ res);
}
};
//4.监听队列 true:自动消息确认
channel.basicConsume("exchange_direct_queue2",true,consumer);
}
}
运行顺序
记住运行程序的顺序,先运行一次sender(创建路由器),
有了路由器之后,在创建两个Recer1和Recer2,进行队列绑定
再次运行sender,发出消息
消费者接收信息由路由键确定,当生产者发送信息到路由上时,因为指定了路由键,所以消息会被指派到对应的消息队列里。消费者通过队列绑定路由接收到不同路由键的消息。
//selecte:此时该参数为路由键不是队列名
channel.basicPublish("router_exchange_direct","selecte",null,msg.getBytes());
//2.2绑定路由(如果路由键的类型是查询绑定到这个队列2上)
channel.queueBind("exchange_direct_queue2","router_exchange_direct","selecte");
和路由模式90%是一样的。
唯独的区别就是路由键支持模糊匹配
匹配符号
*:只能匹配一个词(正好一个词,多一个不行,少一个也不行)
#:匹配0个或更多个词
看一下官网案例:
Q1绑定了路由键 .orange. Q2绑定了路由键 ..rabbit 和 lazy.#
下面生产者的消息会被发送给哪个队列?
quick.orange.rabbit # Q1 Q2
lazy.orange.elephant # Q1 Q2
quick.orange.fox # Q1
lazy.brown.fox # Q2
lazy.pink.rabbit # Q2
quick.brown.fox # 无
orange # 无
quick.orange.male.rabbit # 无
生产者代码
package topic;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import util.ConnectionUtil;
/**
* @author WeiHong
* @date 2021 - 09 - 14 14:03
*/
public class Sender {
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明路由(路由名,路由类型)
// topic:模糊匹配的定向分发
channel.exchangeDeclare("test_exchange_topic", "topic");
String msg = "商品降价";
channel.basicPublish("test_exchange_topic", "produce.price.test", null,
msg.getBytes());
System.out.println("[用户系统]:" + msg);
channel.close();
connection.close();
}
}
消费者1代码
package topic;
import com.rabbitmq.client.*;
import util.ConnectionUtil;
import java.io.IOException;
/**
* @author WeiHong
* @date 2021 - 09 - 14 14:10
*/
public class Recer1 {
public static void main(String[] args) throws Exception {
//1.建立连接
Connection connection = ConnectionUtil.getConnection();
//2.在链接中建立信道
Channel channel = connection.createChannel();
//3.声明队列
channel.queueDeclare("test_exchange_topic_queue1",false,false,false,null);
//4.绑定路由
channel.queueBind("test_exchange_topic_queue1","test_exchange_topic","user.*");
//5.定义内部类接收消息
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body);
System.out.println("[消费者1]接收到的信息:"+msg);
}
};
//6.监听队列 true:消息自动确认
channel.basicConsume("test_exchange_topic_queue1", true,consumer);
}
}
消费者2代码
package topic;
import com.rabbitmq.client.*;
import util.ConnectionUtil;
import java.io.IOException;
/**
* @author WeiHong
* @date 2021 - 09 - 14 14:10
*/
public class Recer2 {
public static void main(String[] args) throws Exception {
//1.建立连接
Connection connection = ConnectionUtil.getConnection();
//2.在链接中建立信道
Channel channel = connection.createChannel();
//3.声明队列
channel.queueDeclare("test_exchange_topic_queue2",false,false,false,null);
//4.绑定路由
channel.queueBind("test_exchange_topic_queue2","test_exchange_topic","produce.#");
//5.定义内部类接收消息
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body);
System.out.println("[消费者2]接收到的信息:"+msg);
}
};
//6.监听队列 true:消息自动确认
channel.basicConsume("test_exchange_topic_queue2", true,consumer);
}
}
几种常见路由件说明:
*// fanout:不处理路由键(只需要将队列绑定到路由上,发送到路由的消息都会被转发到与该路由绑定的所有队列上)
*channel.exchangeDeclare(“test_exchange_fanout”,“fanout”);
*//direct:会根据路由键进行定向分发消息
*channel.exchangeDeclare(“router_exchange_direct”,“direct”);
*// topic:模糊匹配的定向分发
*channel.exchangeDeclare(“test_exchange_topic”, “topic”);