在学习Java基础的过程中,使用线程池,其中一个参数就是阻塞队列,因为有核心线程数和最大线程数的限制,当超过了最大最大线程数时,当有新的线程进来,就需要有一个阻塞队列,这里的队列就相当于缓存的作用
.
依次类比,消息中间件就是对消息的缓存,在生成消息
和消费消息
中间创建一个缓冲区,一个只管放,一个只管取
。用专业的名称来讲,一个叫做生产者
,一个叫做消费者
优势
应用解耦
:如下图所示,订单系统是整个消息的入口,库存系统出现问题,会导致订单系统也出现问题,那么整个系统可能也会随之崩溃,这就是耦合度太高
使用MQ,库存系统在MQ中取数据,出错也不会影响订单系统以及其他系统。
异步提速:典型的下订单的例子,如图所示:如果等后面的所以系统都完成,需要920ms
而使用MQ,下完订单就返回,剩下的慢慢处理,用户体验不就上来了
削峰填谷:如果同时有大量的请求过来,服务器可能无法撑住,但是如果有MQ作为中间件,所以的流量全部打到MQ上,服务器再慢慢的从MQ中取数据,这样就避免了高峰。
劣势
既然MQ有这些缺点,那应该在什么样的情况下选择MQ
呢?
生产者不需要从消费者处获得反馈
。引入消息队列之前的直接调用,其接口的返回值应该为空,这才让明明下层的动作还没做,上层却当成动作做完了继续往后走,即所谓异步成为了可能。RabbitMQ是由erlang语言开发,基于AMQP(Advanced Message Queue 高级消息队列协议)协议实现的消息队列,它是一种应用程序之间的通信方法,消息队列在分布式系统开发中应用非常广泛。RabbitMQ官方地址:https://www.rabbitmq.com
基础架构如图所示:
基本概念:
Broker
:接收和分发消息的应用,RabbitMQ Server就是 Message BrokerVirtual host
:出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网Connection
:publisher/consumer 和 broker 之间的 TCP 连接Channel
:如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP Connection的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接
,如果应用程序支持多线程,通常每个thread创建单独的 channel 进行通讯
,AMQP method 包含了channel id 帮助客户端和message broker 识别 channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的 Connection 极大减少了操作系统建立 TCP connection 的开销Exchange
:message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到queue 中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast)Queue
:消息最终被送到这里等待 consumer 取走Binding
:exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key。Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据现在的RabbitMQ共有七种模式,分别为:
Hello World
模式
Work queues
工作队列模式
Publish/Subscribe
发布订阅模式
Routing
路由模式
Topics
主题模式
RPC
远程调用模式
Publisher Confirms
发布确认模式
在Maven中,导入一下坐标
<dependencies>
<dependency>
<groupId>com.rabbitmqgroupId>
<artifactId>amqp-clientartifactId>
<version>5.6.0version>
dependency>
dependencies>
从最简单的模式开始,永远的Hello World
// 公共配置
// 1. 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
// 2. 设置参数
factory.setHost("xxx.xx.xxx.xxx"); //主机地址
factory.setPort(5672); // 端口号
factory.setVirtualHost("/shang"); //设置虚拟机,类似于在linux系统下 / 后面有多个子目录一样
factory.setUsername("lcp"); // 用户名
factory.setPassword("lcp"); // 用户名密码
// 3. 创建连接 Connection
Connection connection = factory.newConnection();
// 4. 创建Channel
Channel channel = connection.createChannel();
我们看图说话,一个生产者将消息放入队列中,消费者从队列中取得消息,可以看到是一对一
的关系。
注意点:
connection
:于RabbitMQ服务器建立连接channel
:和服务器建立通道queue
:队列,队列需要自己创建,或者进行指定,队列使用名字进行区分。如果创建队列时,该队列已存在,则直接返回。// 生产者
// 5. 创建队列Queue
/*
queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete,
Map arguments)
1. queue 队列名称
2. durable 是否持久化,当mq重启后,是否还在
3. exclusive
- 是否独占 只能有一个消费者监听这个端口
- 当connection关闭时,是否删除队列
4. autoDelete 是否自动删除,当没有消费者时
5. arguments 参数
*/
channel.queueDeclare("hello", true, false, false, null);
// 6. 发送消息 -> queue
/*
String exchange, String routingKey, boolean mandatory, boolean immediate, BasicProperties props, byte[] body
1. exchange 交换机名称。简单模式下交换机会会使用默认的 "
2. routingKey 路由名称
3. props :配置信息
4. body : 发送消息
*/
channel.basicPublish("", "hello", null, "Hello".getBytes());
// 7. 释放队列
channel.close();
connection.close();
// 消费者
// 4. 创建Channel
Channel channel = connection.createChannel();
// 5. 创建队列Queue
channel.queueDeclare("hello", true, false, false, null);
// 6. 接收消息,异步方法
/*
String queue, boolean autoAck, Consumer callback
callback 回调对象
*/
Consumer consumer = new DefaultConsumer(channel) {
// 回调方法
/*
String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body
1. consumerTag 消息标识
2. envelope 获取一些信息 :路由, 交换机
3. properties 配置信息
4. 真实数据
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("consumerTag:" + consumerTag);
System.out.println("Exchange:" + envelope.getExchange());
System.out.println("RoutingKey:" + envelope.getRoutingKey());
System.out.println("properties:" + properties);
System.out.println("body: " + new String(body));
}
};
channel.basicConsume("hello", true, consumer);
总体步骤而言:
与hello不同的是 一个生产者 -> 多个消费者
,在消息处理过慢时,可以使用多个消费者进行消费,且这些消费者之间是竞争关系
关注点:
connection
:于RabbitMQ服务器建立连接channel
:和服务器建立通道queue
:队列,队列需要自己创建,或者进行指定,队列使用名字进行区分。如果创建队列时,该队列已存在,则直接返回。// 生产者
// 5. 创建队列Queue
/*
queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete,
Map arguments)
1. queue 队列名称
2. durable 是否持久化,当mq重启后,是否还在
3. exclusive
- 是否独占 只能有一个消费者监听这个端口
- 当connection关闭时,是否删除队列
4. autoDelete 是否自动删除,当没有消费者时
5. arguments 参数
*/
channel.queueDeclare("workQueues", true, false, false, null);
// 6. 发送消息 -> queue
/*
String exchange, String routingKey, boolean mandatory, boolean immediate, BasicProperties props, byte[] body
1. exchange 交换机名称。简单模式下交换机会会使用默认的 "
2. routingKey 路由名称
3. props :配置信息
4. body : 发送消息
*/
for (int i = 0; i < 10; i++) {
channel.basicPublish("", "workQueues", null, ("Hello" + i).getBytes());
}
// 7. 释放队列
// channel.close();
// connection.close();
}
// 消费者 只需绑定一个队列即可
// 5. 创建队列Queue
channel.queueDeclare("workQueues", true, false, false, null);
// 6. 接收消息
/*
String queue, boolean autoAck, Consumer callback
callback 回调对象
*/
Consumer consumer = new DefaultConsumer(channel) {
// 回调方法
/*
String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body
1. consumerTag 消息标识
2. envelope 获取一些信息 :路由, 交换机
3. properties 配置信息
4. 真实数据
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// System.out.println("consumerTag:" + consumerTag);
// System.out.println("Exchange:" + envelope.getExchange());
// System.out.println("RoutingKey:" + envelope.getRoutingKey());
// System.out.println("properties:" + properties);
System.out.println("body: " + new String(body));
}
};
channel.basicConsume("workQueues", true, consumer);
总体步骤而言:
在这个模式中,出现了交换机
的概念,生产者将消费发送到交换机,交换机再分别将消息转发到与之绑定的队列
关注点:
connection
:于RabbitMQ服务器建立连接channel
:和服务器建立通道,并且绑定交换机queue
:队列,队列需要自己创建,或者进行指定,队列使用名字进行区分。如果创建队列时,该队列已存在,则直接返回。exchange
:创建交换机,让交换机和队列进行绑定,模式为fanout
// 生产者
// 5. 创建交换机
/*
String exchange, 路由名称
BuiltinExchangeType type, 路由类型
DIRECT("direct"), 定向
FANOUT("fanout"), 扇形(广播)
TOPIC("topic"), 通配符
HEADERS("headers"); 参数
boolean durable, 是否持久化
boolean autoDelete, 是否删除
boolean internal, 内部使用,一般用false
Map arguments 参数
*/
String exchangeName = "testFanout";
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT,
false, false, null);
// 6. 创建队列
String queueFanout1 = "testQueueFanout1";
String queueFanout2 = "testQueueFanout2";
channel.queueDeclare(queueFanout1, true, false, false, null);
channel.queueDeclare(queueFanout2, true, false, false, null);
/*
String queue, String exchange, String routingKey
routingKey 路由键,绑定规则
如果路由规则为Fanout,则为空字符串
*/
// 7. 绑定队列和交换机
channel.queueBind(queueFanout1, exchangeName, "");
channel.queueBind(queueFanout2, exchangeName, "");
// 8. 发送消息
String body = "这是最好的时代";
channel.basicPublish(exchangeName, "", null, body.getBytes());
// 9. 关闭
// channel.close();
// connection.close();
// 消费者 只关心队列
// 6. 接收消息
/*
String queue, boolean autoAck, Consumer callback
callback 回调对象
*/
Consumer consumer = new DefaultConsumer(channel) {
// 回调方法
/*
String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body
1. consumerTag 消息标识
2. envelope 获取一些信息 :路由, 交换机
3. properties 配置信息
4. 真实数据
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("consumerTag:" + consumerTag);
System.out.println("Exchange:" + envelope.getExchange());
System.out.println("RoutingKey:" + envelope.getRoutingKey());
System.out.println("properties:" + properties);
System.out.println("body: " + new String(body));
}
};
channel.basicConsume("testQueueFanout1", true, consumer);
总体步骤:
整个过程中,消费者只关心队列,不关心这个队列之前的所以设置
在订阅/发布的模式上,增加了routingKey
:路由键,发布者发布消息时,不但需要指定具体的路由,并且需要指定具体路由键,那么这个消息只会发送到与这个路由和路由键绑定的队列中
去
举个例子:
A和B两个队列都绑定卡了同一个路由,但是A处理的消息是订单,B处理的消息是库存,那么根据消息的不同分类,将消息放到对应的队列中,这个就是路由
关注点:
connection
:于RabbitMQ服务器建立连接channel
:和服务器建立通道,并且绑定交换机queue
:队列,队列需要自己创建,或者进行指定,队列使用名字进行区分。如果创建队列时,该队列已存在,则直接返回。exchange
:创建交换机,让交换机和队列进行绑定,模式为BuiltinExchangeType.DIRECT
routingKey
:路由键,交换机和队列绑定的钥匙,生产者发送消息需要指定路由键// 生产者
// 5. 创建交换机,指定交换机类型为DIRECT
String exchangeName = "testDirect";
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT,
false, false, null);
// 6. 创建队列
String queueFanout1 = "testQueueDirect1";
String queueFanout2 = "testQueueDirect2";
channel.queueDeclare(queueFanout1, true, false, false, null);
channel.queueDeclare(queueFanout2, true, false, false, null);
/*
String queue, String exchange, String routingKey
routingKey 路由键,绑定规则
如果路由规则为Fanout,则为空字符串
*/
// 7. 绑定队列和交换机
// 队列1绑定error
channel.queueBind(queueFanout1, exchangeName, "error");
// 队列2绑定error info debug
channel.queueBind(queueFanout2, exchangeName, "error");
channel.queueBind(queueFanout2, exchangeName, "info");
channel.queueBind(queueFanout2, exchangeName, "debug");
// 8. 发送消息
String body = "这是最好的时代";
channel.basicPublish(exchangeName, "info", null, body.getBytes());
// 9. 关闭
// channel.close();
// connection.close();
// 消费者
Consumer consumer = new DefaultConsumer(channel) {
// 回调方法
/*
String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body
1. consumerTag 消息标识
2. envelope 获取一些信息 :路由, 交换机
3. properties 配置信息
4. 真实数据
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("consumerTag:" + consumerTag);
System.out.println("Exchange:" + envelope.getExchange());
System.out.println("RoutingKey:" + envelope.getRoutingKey());
System.out.println("properties:" + properties);
System.out.println("body: " + new String(body));
}
};
channel.basicConsume("testQueueDirect2", true, consumer);
总体步骤:
Topic 主题模式可以实现 Pub/Sub 发布与订阅模式和 Routing 路由模式的功能,只是 Topic 在配置routing key 的时候可以使用通配符,显得更加灵活。
关注点:
connection
:于RabbitMQ服务器建立连接channel
:和服务器建立通道,并且绑定交换机queue
:队列,队列需要自己创建,或者进行指定,队列使用名字进行区分。如果创建队列时,该队列已存在,则直接返回。exchange
:创建交换机,让交换机和队列进行绑定,模式为BuiltinExchangeType.TOPIC
routingKey
:路由键,交换机和队列绑定的钥匙,生产者发送消息需要发送于路由键匹配的字符 String exchangeName = "testTopic";
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC,
false, false, null);
// 6. 创建队列
String queueFanout1 = "testQueueTopic1";
String queueFanout2 = "testQueueTopic2";
channel.queueDeclare(queueFanout1, true, false, false, null);
channel.queueDeclare(queueFanout2, true, false, false, null);
/*
String queue, String exchange, String routingKey
routingKey 路由键,绑定规则
如果路由规则为Fanout,则为空字符串
*/
// 7. 绑定队列和交换机
// * 代表零个或一个
// # 代表零个或多个
// 根据字符串匹配进行路由分发
channel.queueBind(queueFanout1, exchangeName, "#.error");
channel.queueBind(queueFanout1, exchangeName, "order.*");
channel.queueBind(queueFanout2, exchangeName, "#.#");
// 8. 发送消息
String body = "这是最好的时代";
channel.basicPublish(exchangeName, "goods", null, body.getBytes());
// 9. 关闭
// channel.close();
// connection.close();
Consumer consumer = new DefaultConsumer(channel) {
// 回调方法
/*
String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body
1. consumerTag 消息标识
2. envelope 获取一些信息 :路由, 交换机
3. properties 配置信息
4. 真实数据
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("consumerTag:" + consumerTag);
System.out.println("Exchange:" + envelope.getExchange());
System.out.println("RoutingKey:" + envelope.getRoutingKey());
System.out.println("properties:" + properties);
System.out.println("body: " + new String(body));
}
};
channel.basicConsume("testQueueTopic1", true, consumer);
总体步骤: