应用场景
RabbitMQ,或者说AMQP解决了什么问题,或者说它的应用场景是什么?
对于一个大型的软件系统来说,它会有很多的组件或者说模块或者说子系统或者(subsystem or Component or submodule)。那么这些模块的如何通信?这和传统的IPC有很大的区别。传统的IPC很多都是在单一系统上的,模块耦合性很大,不适合扩展(Scalability);如果使用socket那么不同的模块的确可以部署到不同的机器上,但是还是有很多问题需要解决。比如:
1)信息的发送者和接收者如何维持这个连接,如果一方的连接中断,这期间的数据如何方式丢失?
2)如何降低发送者和接收者的耦合度?
3)如何让Priority高的接收者先接到数据?
4)如何做到load balance?有效均衡接收者的负载?
5)如何有效的将数据发送到相关的接收者?也就是说将接收者subscribe 不同的数据,如何做有效的filter。
6)如何做到可扩展,甚至将这个通信模块发到cluster上?
7)如何保证接收者接收到了完整,正确的数据?
AMDQ协议解决了以上的问题,而RabbitMQ实现了AMQP。
系统架构
成为系统架构可能不太合适,可能叫应用场景的系统架构更合适。
RabbitMQ Server: 也叫broker server,它不是运送食物的卡车,而是一种传输服务。原话是RabbitMQisn’t a food truck, it’s a delivery service. 他的角色就是维护一条从Producer到Consumer的路线,保证数据能够按照指定的方式进行传输。但是这个保证也不是100%的保证,但是对于普通的应用来说这已经足够了。当然对于商业系统来说,可以再做一层数据一致性的guard,就可以彻底保证系统的一致性了。
Client A & B: 也叫Producer,数据的发送方。createmessages and publish (send) them to a broker server (RabbitMQ).一个Message有两个部分:payload(有效载荷)和label(标签)。payload顾名思义就是传输的数据。label是exchange的名字或者说是一个tag,它描述了payload,而且RabbitMQ也是通过这个label来决定把这个Message发给哪个Consumer。AMQP仅仅描述了label,而RabbitMQ决定了如何使用这个label的规则。
Client 1,2,3:也叫Consumer,数据的接收方。Consumersattach to a broker server (RabbitMQ) and subscribe to a queue。把queue比作是一个有名字的邮箱。当有Message到达某个邮箱后,RabbitMQ把它发送给它的某个订阅者即Consumer。当然可能会把同一个Message发送给很多的Consumer。在这个Message中,只有payload,label已经被删掉了。对于Consumer来说,它是不知道谁发送的这个信息的。就是协议本身不支持。但是当然了如果Producer发送的payload包含了Producer的信息就另当别论了。
对于一个数据从Producer到Consumer的正确传递,还有三个概念需要明确:exchanges, queues and bindings。
Exchanges are where producers publish their messages.
Queuesare where the messages end up and are received by consumers
Bindings are how the messages get routed from the exchange to particular queues.
还有几个概念是上述图中没有标明的,那就是Connection(连接),Channel(通道,频道)。
Connection: 就是一个TCP的连接。Producer和Consumer都是通过TCP连接到RabbitMQ Server的。以后我们可以看到,程序的起始处就是建立这个TCP连接。
Channels: 虚拟连接。它建立在上述的TCP连接中。数据流动都是在Channel中进行的。也就是说,一般情况是程序起始建立TCP连接,第二步就是建立这个Channel。
那么,为什么使用Channel,而不是直接使用TCP连接?
对于OS来说,建立和关闭TCP连接是有代价的,频繁的建立关闭TCP连接对于系统的性能有很大的影响,而且TCP的连接数也有限制,这也限制了系统处理高并发的能力。但是,在TCP连接中建立Channel是没有上述代价的。对于Producer或者Consumer来说,可以并发的使用多个Channel进行Publish或者Receive。有实验表明,1s的数据可以Publish10K的数据包。当然对于不同的硬件环境,不同的数据包大小这个数据肯定不一样,但是我只想说明,对于普通的Consumer或者Producer来说,这已经足够了。如果不够用,你考虑的应该是如何细化split你的设计。
系统原理图:
=========================================================
连接MQ:
public class MqUtil {
public static Connection getMqConnection() throws IOException, TimeoutException {
//获取工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置服务器
connectionFactory.setHost("127.0.0.1");
//设置端口
connectionFactory.setPort(5672);
//设置vhost
connectionFactory.setVirtualHost("/vhost_mmr");
//设置用户名
connectionFactory.setUsername("guest");
//设置密码
connectionFactory.setPassword("guest");
Connection connection = connectionFactory.newConnection();
return connection;
}
}
一、轮询分发
生产者:
public class PublishMq {
public static void main(String[] args) throws IOException, TimeoutException {
//获取连接
Connection connection = MqUtil.getMqConnection();
//获取通道
Channel channel = connection.createChannel();
//创建队列声明
channel.queueDeclare("test_xxx_queue", false, false, false, null);
String msg = "生产者生产了";
//生产消息
channel.basicPublish("", "test_xxx_queue", null, msg.getBytes());
System.out.println("发送成功:" + msg);
//关闭通道
channel.close();
//关闭连接
connection.close();
}
}
消费者(可以多个):
public class ConsumerMq {
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
//获取连接
Connection mqConnection = MqUtil.getMqConnection();
//获取通道
Channel channel = mqConnection.createChannel();
//声明队列
channel.queueDeclare("test_xxx_queue", false, false, false, null);
//消费者
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "utf-8");
System.out.println("监听中:" + msg);
}
};
//监听
channel.basicConsume("test_xxx_queue", true, defaultConsumer);
}
}
RabbitMQ不允许您使用不同的参数重新定义现有队列,并将向尝试执行此操作的任何程序返回错误
公平派遣
为了保证每个任务能够公平执行,而不是一个任务忙到死而一个任务闲的不行,要把任务公平的发到每个消费者。发生这种情况是因为RabbitMQ只是在消息进入队列时调度消息。它不会查看消费者未确认消息的数量。它只是盲目地向第n个消费者发送每个第n个消息。
在处理并确认前一个消息之前,不要向工作人员发送新消息。相反,它会将它发送给下一个仍然不忙的工人
int prefetchCount = 1 ;
channel.basicQos(prefetchCount);
消息确认
使用我们当前的代码,一旦RabbitMQ向客户发送消息,它立即将其标记为删除,如果你杀死一个工人,我们将丢失它刚刚处理的消息。我们还将丢失分发给这个特定工作者但尚未处理的所有消息。为了避免消费者执行一半突然死了然后消息丢失的尴尬,要进行消息确认的机制。
为了确保消息永不丢失,RabbitMQ支持 消息确认。消费者发回ack(nowledgement)告诉RabbitMQ已收到,处理了特定消息,RabbitMQ可以自由删除它。
如果消费者死亡(其通道关闭,连接关闭或TCP连接丢失)而不发送确认,RabbitMQ将理解消息未完全处理并将重新排队。如果其他消费者同时在线,则会迅速将其重新发送给其他消费者。这样你就可以确保没有消息丢失,即使工人偶尔会死亡。
boolean autoAck = false ;
channel.basicConsume(TASK_QUEUE_NAME,autoAck,consumer);
二、能者多劳
消费者:
public class SendWorkQueue {
public static void main(String[] args) throws IOException, TimeoutException {
//获取连接
Connection mqConnection = MqUtil.getMqConnection();
//创建通道
Channel channel = mqConnection.createChannel();
//声明队列
channel.queueDeclare("test_work_queue", false, false, false, null);
//消费者没有确认消息之前不会再次发送消息给消费者,队列每次只发一条给消费者
channel.basicQos(1);
//发布消息
for (int i = 0; i < 10; i++) {
String msg = "..." + i;
channel.basicPublish("", "test_work_queue", null, msg.getBytes());
System.out.println("发布成功:" + msg);
}
//关闭
channel.close();
mqConnection.close();
}
}
消费者1:
public class RecvWorkQueue {
public static void main(String[] args) throws IOException, TimeoutException {
//获取连接
Connection mqConnection = MqUtil.getMqConnection();
//创建通道
Channel channel = mqConnection.createChannel();
//声明队列
channel.queueDeclare("test_work_queue", false, false, false, null);
channel.basicQos(1);
//创建消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
@SneakyThrows
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("消费者1,收到消息:" + msg);
Thread.sleep(5000);
//处理完消息给通道一个信号
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
//监听,关闭自动应答
channel.basicConsume("test_work_queue", false, consumer);
}
}
消费者2:
public class RecvWorkQueue2 {
public static void main(String[] args) throws IOException, TimeoutException {
//获取连接
Connection mqConnection = MqUtil.getMqConnection();
//创建通道
Channel channel = mqConnection.createChannel();
//声明队列
channel.queueDeclare("test_work_queue", false, false, false, null);
channel.basicQos(1);
//创建消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
@SneakyThrows
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("消费者2,收到消息:" + msg);
Thread.sleep(1000);
//处理完消息给通道一个信号
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
//监听,关闭自动应答
channel.basicConsume("test_work_queue", false, consumer);
}
}
注意:
//1、消费者没有确认消息之前不会再次发送消息给消费者,队列每次只发一条给消费者
channel.basicQos(1);
//2、处理完消息给通道一个信号
channel.basicAck(envelope.getDeliveryTag(), false);
//3、监听,关闭自动应答
channel.basicConsume("test_work_queue", false, consumer);
三、发布-订阅模式
发布/订阅
- 生产者是发送消息的用户的应用程序。
- 队列是存储消息的缓冲器。
- 消费者是接收消息的用户的应用程序。
RabbitMQ中消息传递模型的核心思想是生产者永远不会将任何消息直接发送到队列。实际上,生产者通常甚至不知道消息是否会被传递到任何队列。、
生产者只能向Exchange发送消息。交换是一件非常简单的事情。一方面,它接收来自生产者的消息,另一方面将它们推送到队列。exchange所必须确切知道如何处理收到的消息。它应该附加到特定队列吗?它应该附加到许多队列吗?或者它应该被丢弃。其规则由交换类型定义 。
Exchange有四个类型,direct, topic, headers and fanout
channel.exchangeDeclare("logs", "fanout");//第一个参数是Exchange的名字,第二参数是类型
fanout是把消息广播到所有的队列中
无名exchange是默认创建的,就是不显示创建exchange的话就会默认创建一个无名的exchange,无名的exchange消息被路由到具有routingKey指定名称的队列(如果存在)。
临时队列
首先,每当我们连接到Rabbit时,需要一个新的空队列时。为此,我们可以使用随机名称创建队列,或者更好 - 让服务器为我们选择随机队列名称。
其次,一旦我们断开消费者,就应该自动删除队列。
String queueName = channel.queueDeclare().getQueue();//随机队列
在Java客户端中,当我们没有向queueDeclare()提供参数时,我们 使用生成的名称创建一个非持久的,独占的自动删除队列
绑定(binding)
exchange和Queue直接的关系叫做绑定(binding)
channel.queueBind(queueName,“logs”,“”);
生产者:
public class SendWorkQueue {
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
Connection mqConnection = MqUtil.getMqConnection();
Channel channel = mqConnection.createChannel();
//声明交换机
channel.exchangeDeclare("test_qqq_exchange", "fanout");//分发
for (int i = 0; i < 10; i++) {
String msg = "我是消息" + i;
channel.basicPublish("test_qqq_exchange", "", null, msg.getBytes());
System.out.println("发布成功:" + msg);
Thread.sleep(5000);
}
channel.close();
mqConnection.close();
}
}
消费者1:
public class RecvWorkQueue {
public static void main(String[] args) throws IOException, TimeoutException {
Connection mqConnection = MqUtil.getMqConnection();
Channel channel = mqConnection.createChannel();
channel.queueDeclare("test_qqq_queue", false, false, false, null);
//队列绑定交换机
channel.queueBind("test_qqq_queue", "test_qqq_exchange", "");
channel.basicQos(1);
DefaultConsumer defaultConsumer = 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("订阅了交换机:" + msg);
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
channel.basicConsume("test_qqq_queue", false, defaultConsumer);
}
}
消费者2:
public class RecvWorkQueue2 {
public static void main(String[] args) throws IOException, TimeoutException {
Connection mqConnection = MqUtil.getMqConnection();
Channel channel = mqConnection.createChannel();
channel.queueDeclare("test_www_queue", false, false, false, null);
//对列绑定交换机
channel.queueBind("test_www_queue", "test_qqq_exchange", "");
channel.basicQos(1);
DefaultConsumer defaultConsumer = 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("订阅了交换机:" + msg);
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
channel.basicConsume("test_www_queue", false, defaultConsumer);
}
}
来源:CSDN
作者:qq_37685457
链接:https://blog.csdn.net/qq_37685457/article/details/104052561