1 MQ(message queue)基本概念
①生产者不需要从消费者处获得反馈。引入消息队列之前的直接调用,其接口的返回值应该为空,这才让明
明下层的动作还没做,上层却当成动作做完 了继续往后走,即所谓异步成为了可能。
②容许短暂的不一致性。
③确实是用了有效果。即解耦、提速、削峰这些方面的收益,超过加入MQ,管理MQ这些成本。
1.1概述
MQ,消息队列,存储消息的中间件
分布式系统通信两种方式:直接远程调用和借助第三防完成间接通信
发送方称为生产者,接收方称为消费者
1.2 优势
- 应用解耦
- 异步提速
- 削峰填谷
1.2 劣势
- 系统可用性降低( 系统引入的外部依赖越多,系统稳定性越差。)
- 系统复杂性提高
- 一致性问题
2 RabbitMQ基本概念
AMQP,即 Advanced Message Queuing Protocol(高级消息队列协议),是一个网络协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。2006年,AMQP 规范发布。类比HTTP。
RabbitMQ 提供了 6 种工作模式:简单模式、work queues、Publish/Subscribe 发布与订阅模式、Routing 路由模式、Topics 主题模式、RPC 远程调用模式(远程调用,不太算 MQ;暂不作介绍)。
3 JMS
- JMS 即 Java 消息服务(JavaMessage Service)应用程序接口,是一个 Java 平台中关于面向消息中间件的API
- JMS 是 JavaEE 规范中的一种,类比JDBC
- 很多消息中间件都实现了JMS规范,例如:ActiveMQ。RabbitMQ 官方没有提供 JMS 的实现包,但是开源社区有
4 快速入门(简单模式)
PS:消息被消费就没了。
生产者(P)代码:
public class Hello_world {
public static void main(String[] args) throws Exception{
/*
1-创建连接工厂
2-设置连接工厂参数
3-用连接工厂创建连接
4-创建channel
5-创建队列
*/
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("192.168.226.128");
factory.setPort(5672);
factory.setVirtualHost("/itcast");
factory.setUsername("heima");
factory.setPassword("heima");
//工厂创建连接
Connection connection=factory.newConnection();
//连接创建频道
Channel channel=connection.createChannel();
//关联队列,如果没有则创建
channel.queueDeclare("hello",true,false,false,null);
String body="hello zc.huang----";
//发送消息到队列区
channel.basicPublish("","hello",null,body.getBytes());
//7.释放资源
channel.close();
connection.close();
}
}
消费者(C)代码:
public class HelloWord {
public static void main(String[] args) throws Exception {
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("192.168.226.128");
factory.setPort(5672);
factory.setVirtualHost("/itcast");
factory.setUsername("heima");
factory.setPassword("heima");
//3. 创建连接 Connection
Connection connection = factory.newConnection();
//4. 创建Channel
Channel channel = connection.createChannel();
//5. 创建队列Queue
/*
queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)
参数:
1. queue:队列名称
2. durable:是否持久化,当mq重启之后,还在
3. exclusive:
* 是否独占。只能有一个消费者监听这队列
* 当Connection关闭时,是否删除队列
*
4. autoDelete:是否自动删除。当没有Consumer时,自动删除掉
5. arguments:参数。
*/
//如果没有一个名字叫hello_world的队列,则会创建该队列,如果有则不会创建
channel.queueDeclare("hello",true,false,false,null);
/*
basicConsume(String queue, boolean autoAck, Consumer callback)
参数:
1. queue:队列名称
2. autoAck:是否自动确认
3. callback:回调对象
*/
// 接收消息
Consumer consumer = new DefaultConsumer(channel){
/*
回调方法,当收到消息后,会自动执行该方法
1. consumerTag:标识
2. envelope:获取一些信息,交换机,路由key...
3. properties:配置信息
4. body:数据
*/
@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);
//关闭资源?不要
}
}
个人理解:
生产者通过工厂创建连接,再由连接创建频道,由频道调用queueDeclare
方法声明队列,队列声明之后就丢到队列区中,等待消费者调用。消费者这边就创建一个消费者,消费者也做和生产者同样的声明工厂指导创建频道,用频道调用basicConsume
方法从而接受消息,但是可能队列区中生产者并没有消费者要调用的队列,所以在消费者的代码中就加上如下代码:
//如果没有一个名字叫hello_world的队列,则会创建该队列,如果有则不会创建
channel.queueDeclare("hello",true,false,false,null);
一个未经证实的想法:
Channel.queueDeclare()
这个方法的第一个参数queue是指队列名称
Channel.basicPublish()
这个方法的第二个参数routingKey路由名称
生产者发送消息时,执行Channel.basicPublish()
方法,这里的路由名称指定需要是 Channel.queueDeclare()
的队列名称,因为声明队列只是声明,要往队列传入信息需要某个标志嵌入信息,这时候路由名称就是作为这个标志,所以两个要一致。
网上摘抄到的信息:
RabbitMQ默认有一个exchange,叫default exchange,它用一个空字符串表示,
它是direct exchange类型,
任何发往这个exchange的消息都会被路由到routing key的名字对应的队列上,
如果没有对应的队列,则消息会被丢弃。这就是为什么代码中channel执行basicPulish方法时,
第二个参数本应该为routing key,却被写上了QUEUE_NAME。
5 Work Queue 工作队列模式
PS:消息被消费就没了。
Work Queues:与入门程序的简单模式相比,多了一个或一些消费端,多个消费端共同消费同一个队列中的消息。
应用场景:对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。
生产者代码实现:
public class Producer_WordkQueue {
public static void main(String[] args) throws Exception{
/*
1-创建连接工厂
2-设置连接工厂参数
3-用连接工厂创建连接
4-创建channel
5-创建队列
*/
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("192.168.226.128");
factory.setPort(5672);
factory.setVirtualHost("/itcast");
factory.setUsername("heima");
factory.setPassword("heima");
Connection connection=factory.newConnection();
Channel channel=connection.createChannel();
//关联对联,如果没有则创建
channel.queueDeclare("work_quenes",true,false,false,null);
//生产多个消息传到队列区
for (int i = 0; i <9 ; i++) {
String body="———第"+i+"条队列信息------";
//发送消息
channel.basicPublish("","work_quenes",null,body.getBytes());
}
//7.释放资源
channel.close();
connection.close();
}
}
创建两个消费者,代码均如下:
public class Comsumer_WorkQuene1 {
public static void main(String[] args) throws Exception {
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("192.168.226.128");
factory.setPort(5672);
factory.setVirtualHost("/itcast");
factory.setUsername("heima");
factory.setPassword("heima");
//3. 创建连接 Connection
Connection connection = factory.newConnection();
//4. 创建Channel
Channel channel = connection.createChannel();
//5. 创建队列Queue
channel.queueDeclare("work_quenes",true,false,false,null);
// 接收消息
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("body:"+new String(body));
}
};
channel.basicConsume("work_quenes",true,consumer);
//关闭资源?不要
}
}
测试:
先运行两个消费者,使其处于等待状态,再开启生产者,把消息队列传到队列区中,
结果:
可以看出两个消费者是竞争关系,依次接受队列信息。
6 Pub/Sub 订阅模式
- 个人理解:生产者不直接把队列存到队列区,而是通过交换机,再由交换机绑定到多个队列而形成多个队列,谁调用什么什么队列信息,就指定队列名称从而调用,这样就实现了类似分层的需求。对比前面的模式,看似就是实现了一个生产者可以生成多个队列,使结构更清晰。
在订阅模型中,多了一个 Exchange 角色,而且过程略有变化:
- P:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X(交换机)
- C:消费者,消息的接收者,会一直等待消息到来
- Queue:消息队列,接收消息、缓存消息
- Exchange:交换机(X)。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决Exchange的类型。Exchange有常见以下3种类型:
Fanout:广播,将消息交给所有绑定到交换机的队列
Direct:定向,把消息交给符合指定routing key 的队列
Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列
Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与 Exchange 绑定,或者没有符合路由规则的队列,那么消息会丢失!
1. 交换机需要与队列进行绑定,绑定之后;一个消息可以被多个消费者都收到。
2. 发布订阅模式与工作队列模式的区别:
1).工作队列模式不用定义交换机,而发布/订阅模式需要定义交换机
2).发布/订阅模式的生产方是面向交换机发送消息,工作队列模式的生产方是面向队列发送消息(底层使用默认交换机)
3).发布/订阅模式需要设置队列和交换机的绑定,工作队列模式不需要设置,实际上工作队列模式会将队列绑 定到默认的交换机
生产者代码:
public class Producer_PubSub {
public static void main(String[] args) throws Exception {
/*
/* 1-创建连接工厂
2-设置连接工厂参数
3-用连接工厂创建连接
4-创建channel
5-创建队列 */
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.226.128");
factory.setPort(5672);
factory.setVirtualHost("/itcast");
factory.setUsername("heima");
factory.setPassword("heima");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//创建交换机
String exchange = "exchange_fanout";
channel.exchangeDeclare(exchange, BuiltinExchangeType.FANOUT, true, false, false, null);
//创建队列
String queue1name = "fanout_queue_1";
String queue2name = "fanout_queue_2";
channel.queueDeclare(queue1name, true, false, false, null);
channel.queueDeclare(queue2name, true, false, false, null);
//队列与交换机绑定
channel.queueBind(queue1name,exchange,"");
channel.queueBind(queue2name,exchange,"");
//发送消息
String body="日志:订阅模式生产者创建队列消息---";
channel.basicPublish(exchange,"",null,body.getBytes());
//9. 释放资源
channel.close();
connection.close();
}
}
消费者代码:
public class Comsumer_PubSub1 {
public static void main(String[] args) throws Exception {
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("192.168.226.128");
factory.setPort(5672);
factory.setVirtualHost("/itcast");
factory.setUsername("heima");
factory.setPassword("heima");
//3. 创建连接 Connection
Connection connection = factory.newConnection();
//4. 创建Channel
Channel channel = connection.createChannel();
// 接收消息
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("body:"+new String(body));
System.out.println("消费者1业务代码------");
}
};
//!!!消费者接受交换机中哪个队列的消息在这里指定!!!
channel.basicConsume("fanout_queue_1",true,consumer);
//关闭资源?不要
}
}
结果:消费者指定了"fanout_queue_1"
队列名,所以就获取到了指定的队列名的信息。
7 Routing 路由模式
队列与交换机的绑定,不能是任意绑定了,而是要指定一个 RoutingKey(路由key)
消息的发送方在向 Exchange 发送消息时,也必须指定消息的 RoutingKey
Exchange 不再把消息交给每一个绑定的队列,而是根据消息的 Routing Key 进行判断,只有队列的Routingkey 与消息的 Routing key 完全一致,才会接收到消息
- P:生产者,向 Exchange 发送消息,发送消息时,会指定一个routing key
- X:Exchange,接收生产者的消息,然后把消息递交给与 routing key 完全匹配的队列
- C1:消费者,其所在队列指定了需要 routing key 为 error 的消息
- C2:消费者,其所在队列指定了需要 routing key 为 info、error、warning 的消息
生产者代码:
public class Producer_Routing {
public static void main(String[] args) throws Exception {
/*
/* 1-创建连接工厂
2-设置连接工厂参数
3-用连接工厂创建连接
4-创建channel
5-创建队列 */
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.226.128");
factory.setPort(5672);
factory.setVirtualHost("/itcast");
factory.setUsername("heima");
factory.setPassword("heima");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//创建交换机
String exchange = "exchange_direct";
channel.exchangeDeclare(exchange, BuiltinExchangeType.DIRECT, true, false, false, null);
//创建队列
String queue1name = "direct_queue_1";
String queue2name = "direct_queue_2";
channel.queueDeclare(queue1name, true, false, false, null);
channel.queueDeclare(queue2name, true, false, false, null);
//队列与交换机绑定
//队列1
channel.queueBind(queue1name, exchange, "error");
//队列2
channel.queueBind(queue2name, exchange, "info");
channel.queueBind(queue2name, exchange, "error");
channel.queueBind(queue2name, exchange, "warning");
//发送消息
String body = "日志:生产者创建队列消息---";
channel.basicPublish(exchange, "error", null, body.getBytes());
//9. 释放资源
channel.close();
connection.close();
}
}
交换机绑定的队列指定不同的RoutingKey
消费者1代码:
public class Comsumer_Routing1 {
public static void main(String[] args) throws Exception {
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("192.168.226.128");
factory.setPort(5672);
factory.setVirtualHost("/itcast");
factory.setUsername("heima");
factory.setPassword("heima");
//3. 创建连接 Connection
Connection connection = factory.newConnection();
//4. 创建Channel
Channel channel = connection.createChannel();
// 接收消息
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("body:"+new String(body));
System.out.println("error------");
}
};
channel.basicConsume("direct_queue_1",true,consumer);
//关闭资源?不要
}
}
消费者2代码:
//跟消费者1的区别仅仅是这些
// 接收消息
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("body:"+new String(body));
System.out.println("info/error/warning------");
}
};
channel.basicConsume("direct_queue_2",true,consumer);
结果:生产者生产队列指定了不同的RoutingKey,消费者在接受消息时,指定的不同的队列名都和指定的RoutingKey有关,实现接受不同队列消息后的不同业务操作。
8 Topics 通配符模式
- Topic 类型与 Direct 相比,都是可以根据 RoutingKey 把消息路由到不同的队列。只不过 Topic类型Exchange 可以让队列在绑定 Routing key 的时候使用通配符!
- Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert 通配符规则:
#匹配一个或多个词, * 匹配不多不少恰好1个词,
例如:
item.# 能够匹配 item.insert.abc 或者 item.insert,
item.* 只能匹配 item.insert
生产者代码:
public class Producer_Topic {
public static void main(String[] args) throws Exception {
/*
/* 1-创建连接工厂
2-设置连接工厂参数
3-用连接工厂创建连接
4-创建channel
5-创建队列 */
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.226.128");
factory.setPort(5672);
factory.setVirtualHost("/itcast");
factory.setUsername("heima");
factory.setPassword("heima");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//创建交换机
String exchange = "exchange_topic";
channel.exchangeDeclare(exchange, BuiltinExchangeType.TOPIC, true, false, false, null);
//创建队列
String queue1name = "topic_queue_1";
String queue2name = "topic_queue_2";
channel.queueDeclare(queue1name, true, false, false, null);
channel.queueDeclare(queue2name, true, false, false, null);
//队列与交换机绑定
//队列1
channel.queueBind(queue1name, exchange, "*.orange.*");
//队列2
channel.queueBind(queue2name, exchange, "*.*.rabbite");
channel.queueBind(queue2name, exchange, "lazy.#");
channel.queueBind(queue2name, exchange, "#.orange.*");
//发送消息
String body = "日志:生产者创建队列消息---";
channel.basicPublish(exchange, "lazy.dfg", null, body.getBytes());
//9. 释放资源
channel.close();
connection.close();
}
}
消费者代码:略。
Topic 主题模式可以实现 Pub/Sub 发布与订阅模式和 Routing 路由模式的功能,只是 Topic 在配置routing key 的时候可以使用通配符,显得更加灵活。
9 五大模式总结
>>>简单模式:利用频道直接声明队列和发送消息,直观上就是直接把消息丢到一个队列区。
>>>工作队列模式:和简单模式的区别就是在一个队列里创建了多个信息
供给多个消费者获取,但是消费者是竞争关系,有顺序的竞争。
**>>>Pub/Sub 订阅模式:**引入了交换机,绑定队列,相当于把队列交给交换机管理,交换机的类型是BuiltinExchangeType.FANOUT
,发送消息的时候RoutingKey是" "(空字符串)
;
>>>Routing 路由模式:和订阅模式的区别是交换机类型是BuiltinExchangeType.DIRECT
,然后在绑定队列的时候还加入了多个Routing Key,把队列和Routingkey关联;在发送消息加入队列名和RoutingKey,也就指定了消息是发到了哪个队列的哪个RoutingKey上。
//队列1
channel.queueBind(queue1name, exchange, "error");
//队列2
channel.queueBind(queue2name, exchange, "info");
channel.queueBind(queue2name, exchange, "error");
channel.queueBind(queue2name, exchange, "warning");
>>>topic 通配符模式:和路由模式类似,区别在于交换机的类型是BuiltinExchangeType.TOPIC
,然后在绑定队列的时候的RoutingKey的写法引入通配符,也是将队列与RoutingKey进行关联;
//队列1
channel.queueBind(queue1name, exchange, "*.orange.*");
//队列2
channel.queueBind(queue2name, exchange, "*.*.rabbite");
channel.queueBind(queue2name, exchange, "lazy.#");
channel.queueBind(queue2name, exchange, "#.orange.*");