目录
初识MQ
同步和异步通讯
同步通讯的优缺点
异步调用方案
异步通信优缺点
常见MQ技术对比
RabbitMQ快速入门
安装RabbitMQ
RabbitMQ整体架构与相关概念
常见消息模型编辑
入门案例
SpringAMQP
基本介绍
SpringAMQP案例——模拟HelloWorld消息模型
SpringAMQP案例——模拟WorkQueue消息模型
SpringAMQP案例——模拟发布订阅消息模型
发布订阅消息模型介绍
案例——FanoutExchange
案例——DirectExchange
案例——TopicExchange
消息转换器
初识MQ
同步和异步通讯
微服务间通讯有同步和异步两种方式:同步通讯就像打电话,需要实时响应,异步通讯就像发邮件,不需要马上回复。两种方式各有优劣,打电话可以立即得到响应,但是你却不能跟多个人同时通话。发送邮件可以同时与多个人收发邮件,但是往往响应会有延迟。
同步通讯的优缺点
优点:
时效性较强,可以立即得到结果。
缺点:
1. 耦合度高:每次加入新的需求,都要修改原来的代码;
2. 性能下降:调用者需要等待服务提供者响应,如果调用链过长则响应时间等于每次调用的时间之和;
3. 资源浪费:调用链中的每个服务在等待响应过程中,不能释放请求占用的资源,高并发场景下会极度浪费系统资源;
4. 级联失败:如果服务提供者出现问题,所有调用方都会跟着出问题如同多米诺骨牌一样,迅速导致整个微服务群故障。
异步调用方案
我们以购买商品为例,用户支付后需要调用订单服务完成订单状态修改,调用物流服务,从仓库分配响应的库存并准备发货。
在事件模式中,支付服务是事件发布者,在支付完成后只需要发布一个支付成功的事件,事件中带上订单id。
订单服务和物流服务是事件订阅者,订阅支付成功的事件,监听到事件后完成自己业务即可。
为了解除事件发布者与订阅者之间的耦合,两者并不是直接通信,而是有一个中间Broker。发布者发布事件到Broker,不关心谁来订阅事件。订阅者从Broker订阅事件,不关心谁发来的消息。
异步通信优缺点
优点:
1. 吞吐量提升:无需等待订阅者处理完成,响应更快速;
2. 故障隔离:服务没有直接调用,不存在级联失败问题;
3. 调用间没有阻塞,不会造成无效的资源占用;
4. 耦合度极低,每个服务都可以灵活插拔,可替换;
5. 流量削峰:不管发布事件的流量波动多大,都由Broker接收,订阅者可以按照自己的速度去处理事件。
缺点:
1. 架构复杂了,业务没有明显的流程线,不好管理;
2. 需要依赖于Broker的可靠、安全、性能。
常见MQ技术对比
RabbitMQ快速入门
安装RabbitMQ
步骤1 在线拉取镜像
docker pull rabbitmq:3-management
步骤2 执行下面的命令来运行MQ容器,在命令行中设置用户名和密码
docker run \ -e RABBITMQ_DEFAULT_USER=root \ -e RABBITMQ_DEFAULT_PASS=123456 \ --name mq \ --hostname mq1 \ -p 15672:15672 \ -p 5672:5672 \ -d \ rabbitmq:3-management
步骤3 进入RabbitMQ管理平台
安装好后,安装好后通过IP+端口访问管理界面。管理界面端口是15672,tcp连接的端口是5672。在浏览器中访问192.168.237.128:1567进入RabbitMQ管理平台,其中192.168.237.128为虚拟机ip地址。
RabbitMQ整体架构与相关概念
RabbitMQ架构图
RabbitMQ中的几个概念
channel:操作MQ的工具
exchange:路由消息到队列中
queue:缓存消息
virtualhost:虚拟主机,是对queue、exchange等资源的逻辑分组
常见消息模型
入门案例
发布者发送消息代码:
public class PublisherTest { @Test public void testSendMessage() throws IOException, TimeoutException { // 1.建立连接 ConnectionFactory factory = new ConnectionFactory(); // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码 factory.setHost("192.168.150.101"); factory.setPort(5672); factory.setVirtualHost("/"); factory.setUsername("itcast"); factory.setPassword("123321"); // 1.2.建立连接 Connection connection = factory.newConnection(); // 2.创建通道Channel Channel channel = connection.createChannel(); // 3.创建队列 String queueName = "simple.queue"; channel.queueDeclare(queueName, false, false, false, null); // 4.发送消息 String message = "hello, rabbitmq!"; channel.basicPublish("", queueName, null, message.getBytes()); System.out.println("发送消息成功:【" + message + "】"); // 5.关闭通道和连接 channel.close(); connection.close(); } }
运行之后可以在RabbitMQ管理平台看到队列里已经有一个消息
点击该消息,通过Get Message可以查看接收到的消息内容
消费者建立连接代码:
public class ConsumerTest { @Test public static void main(String[] args) throws IOException, TimeoutException { // 1.建立连接 ConnectionFactory factory = new ConnectionFactory(); // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码 factory.setHost("192.168.237.128"); factory.setPort(5672); factory.setVirtualHost("/"); factory.setUsername("root"); factory.setPassword("123456"); // 1.2.建立连接 Connection connection = factory.newConnection(); // 2.创建通道Channel Channel channel = connection.createChannel(); // 3.创建队列 String queueName = "simple.queue"; channel.queueDeclare(queueName, false, false, false, null); // 4.订阅消息 channel.basicConsume(queueName, true, new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { // 5.处理消息 String message = new String(body); System.out.println("接收到消息:【" + message + "】"); } }); System.out.println("等待接收消息。。。。"); } }
运行之后可以在RabbitMQ管理平台发现已经建立起一个连接,并且能够在idea控制台观察到从消息队列里获取的消息。
小结:
发布者和生产者的代码中,都有创建队列这一部分,是否会产生冲突?
由于无法确定发布者和生产者运行的前后顺序,为避免寻找不到所需绑定的队列,因此都需要创建队列,如果该队列已经存在,也不会产生冲突。
基本消息队列的消息发送流程
1.建connection
2.创建channel
3.利用channel声明队列
4.利用channel向队列发送消息
基本消息队列的消息接收流程
1.建connection
2.创建channel
3.利用channel声明队列
4.定义consumer的消费行为handleDelivery05.利用channel将消费者与队列绑定
SpringAMQP
基本介绍
AMQP(Advanced Message Queuing Protocol)是用于在应用程序或之间传递业务消息的开放标准。该协议与语言和平台无关,更符合微服务中独立性的要求。
Spring AMQP是基于AMQP协议定义的一套API规范,提供了模板来发送和接收消息。包含两部分,其中spring-amqp是基础抽象spring-rabbit是底层的默认实现。
SpringAMQP案例——模拟HelloWorld消息模型
模拟消息发送:
步骤1. 在工程中引入AMQP依赖
org.springframework.boot spring-boot-starter-amqp
步骤2. 配置MQ地址:在publisher服务的application.yml中添加配置
spring: rabbitmq: host: 192.168.237.128 # rabbitMQ的ip地址 port: 5672 # 端口 username: root password: 123456 virtual-host: /
步骤3. 在publisher服务中设置测试类,利用convertAndSend方法实现信息发送
@RunWith(SpringRunner.class) @SpringBootTest public class SpringAmqpTest { @Autowired private RabbitTemplate rabbitTemplate; @Test public void testSendMessage2SimpleQueue() { String queueName = "simple.queue"; String message = "hello,spring amqp"; rabbitTemplate.convertAndSend(queueName,message); } }
运行之后可以看到消息队列中存在一条消息,消息内容为我们发送的内容
模拟消息接收:
步骤1. 在工程中引入AMQP依赖
org.springframework.boot spring-boot-starter-amqp
步骤2. 配置MQ地址:在consumer服务的application.yml中添加配置
spring: rabbitmq: host: 192.168.237.128 # rabbitMQ的ip地址 port: 5672 # 端口 username: root password: 123456 virtual-host: /
步骤2. 在consumer服务中新建一个类,编写消费逻辑
@Component public class SpringRabbitListener { @RabbitListener(queues = "simple.queue") public void listenSimpleQueue(String msg){ System.out.println("消费者接收到simple.queue的消息:【" + msg + "】"); } }
运行之后,通过idea控制台可以看到已经成功接收到消息
注意:消息一旦消费就会从队列删除,RabbitMQ没有消息回溯功能。
SpringAMQP案例——模拟WorkQueue消息模型
根据WorkQueue消息模型,有一个发布者和两个消费者,因此我们模拟消费者发送大量消息的情况。
/** * workQueue * 向队列中不停发送消息,模拟消息堆积。 */ @Test public void testWorkQueue() throws InterruptedException { // 队列名称 String queueName = "simple.queue"; // 消息 String message = "hello, message_"; for (int i = 0; i < 50; i++) { // 发送消息 rabbitTemplate.convertAndSend(queueName, message + i); Thread.sleep(20); } }
再设置两个消费者,通过设置sleep休眠,明显区分两个消费者的性能。
@RabbitListener(queues = "simple.queue") public void listenSimpleQueue1(String msg) throws InterruptedException { System.out.println("消费者1接收到的消息:【" + msg + "】" + LocalTime.now()); Thread.sleep(20); } @RabbitListener(queues = "simple.queue") public void listenSimpleQueue2(String msg) throws InterruptedException { System.err.println("消费者2接收到的消息:【" + msg + "】" + LocalTime.now()); Thread.sleep(200); }
先运行发布者,发布50条消息,再运行消费者,控制台信息如下:
通过观察控制台信息,我们可以发现,消费者1只接收单数的消息,而消费者2只接收双数的消息。当消费者1很快完成了自己的25条消息,消费者2却在缓慢的处理自己的25条消息。
这是由WorkQueue消息模型中的消息预取机制所导致的。在该机制下,消费者无论当前是否能够处理消息,都会提前从队列中取出消息。因此,所有的消息会被平均分配给两个消费者。结果是,无论哪个消费者的接收速度较快,整体的消息接收速度都取决于接收速度较慢的消费者。
为了解决这个问题,我们可以通过设置消费者每次获取消息的数量来进行限制。只允许每个消费者一次获取一条消息,并且要求在当前消息处理完毕之后才能获取下一条消息。通过这种方式,我们可以实现能者多劳的效果。
经过设置后,我们再次运行程序,可以发现结果不再像之前一样,消费者1能够根据自己的性能接收更多消息。
SpringAMQP案例——模拟发布订阅消息模型
发布订阅消息模型介绍
可以看到,在订阅模型中,多了一个exchange角色,而且过程略有变化:
Publisher:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X(交换机)
-
Exchange:交换机,图中的X。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange有以下3种类型:
-
Fanout:广播,将消息交给所有绑定到交换机的队列
-
Direct:定向,把消息交给符合指定routing key 的队列
-
Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列
-
-
Consumer:消费者,与以前一样,订阅队列,没有变化
-
Queue:消息队列也与以前一样,接收消息、缓存消息。
案例——FanoutExchange
步骤1. 在consumer中创建一个类,用于声明队列和交换机,并将队列与交换机进行绑定
@Configuration public class FanoutConfig { // 声明交换机 itcast.fanout @Bean public FanoutExchange fanoutExchange() { return new FanoutExchange(("itcast.fanout")); } // 声明fanout.queue1 @Bean public Queue fanoutQueue1() { return new Queue("fanout.queue1"); } // 绑定队列1到交换机 @Bean public Binding fanoutBinding1(Queue fanoutQueue1, FanoutExchange fanoutExchange) { return BindingBuilder .bind(fanoutQueue1) .to(fanoutExchange); } // 声明fanout.queue2 @Bean public Queue fanoutQueue2() { return new Queue("fanout.queue2"); } // 绑定队列2到交换机 @Bean public Binding fanoutBinding2(Queue fanoutQueue2, FanoutExchange fanoutExchange) { return BindingBuilder .bind(fanoutQueue2) .to(fanoutExchange); } }
重新运行后,在RabbitMQ管理平台中可以看到已经生成该交换机,并且与两个队列绑定。
步骤2:在consumer服务的SpringRabbitListener中添加两个方法,作为消费者,对两个队列进行监听
@RabbitListener(queues = "fanout.queue1") public void listenFanoutQueue1(String msg){ System.out.println("消费者接收到fanout.queue1的消息:【" + msg + "】"); } @RabbitListener(queues = "fanout.queue2") public void listenFanoutQueue2(String msg){ System.out.println("消费者接收到fanout.queue2的消息:【" + msg + "】"); }
步骤3. 在publisher服务的SpringAmqpTest类中添加测试方法,将消息发送到交换机
@Test public void testSendFanoutExchange() { //交换机名称 String exchangeName = "itcast.fanout"; //消息 String message = "hello, every one!"; //发送消息 rabbitTemplate.convertAndSend(exchangeName,"", message); }
运行该测试方法,可以在控制台中看到消费者成功接收到消息。
总结
交换机的作用是什么?
1. 接收publisher发送的消息
2. 将消息按照规则路由到与之绑定的队列
3. 不能缓存消息,路由失败,消息丢失
4. FanoutExchange的会将消息路由到每个绑定的队列
绑定的队列声明队列、交换机、绑定关系的Bean是什么?
Queue、FanoutExchange、Binding
案例——DirectExchange
在Direct模型下:
1. 队列与交换机的绑定,不能是任意绑定了,而是要指定一个 RoutingKey(路由key)
2. 消息的发送方在向 Exchange发送消息时,也必须指定消息的 RoutingKey
3. Exchange不再把消息交给每一个绑定的队列,而是根据消息的 outing key 进行判断,只有队列的Routingkey 与消息的 Routing key 完全一致,才会接收到消息
基于@Bean的方式声明队列和交换机比较麻烦,Spring还提供了基于注解方式来声明。
步骤1. 在consumer的SpringRabbitListener中添加两个消费者,同时基于注解来声明队列和交换机
@RabbitListener( bindings = @QueueBinding( value = @Queue(name = "direct.queue1"), exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT), key = {"red", "blue"} )) public void listenDirectQueue1(String msg) { System.out.println("消费者接收到direct.queue1的消息:【" + msg + "】"); } @RabbitListener( bindings = @QueueBinding( value = @Queue(name = "direct.queue2"), exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT), key = {"red", "yellow"} )) public void listenDirectQueue2(String msg) { System.err.println("消费者接收到direct.queue2的消息:【" + msg + "】"); }
步骤2. 在publisher服务的SpringAmqpTest类中添加测试方法
@Test public void testSendDirectExchange() { //交换机名称 String exchangeName = "itcast.direct"; //消息 String message = "hello, blue!"; //发送消息 rabbitTemplate.convertAndSend(exchangeName,"yellow", message); }
当key为red时,控制台输出如下:
当key为blue时,控制台输出如下:
当key为yellow时,控制台输出如下:
总结
描述下Direct交换机与Fanout交换机的差异?
1. Fanout交换机将消息路由给每一个与之绑定的队列
2. Direct交换机根据RoutingKey判断路由给哪个队列
3. 如果多个队列具有相同的RoutingKey,则与Fanout功能类似
基于@RabbitListener注解声明队列和交换机有哪些常见注解?
@Queue、@Exchange
案例——TopicExchange
步骤1. 在consumer的SpringRabbitListener中添加两个消费者,同时基于注解来声明队列和交换机
@RabbitListener(bindings = @QueueBinding( value = @Queue(name = "topic.queue1"), exchange = @Exchange(name = "itcast.topic" ,type = ExchangeTypes.TOPIC), key = "china.#" )) public void listenTopicQueue1(String msg) { System.out.println("消费者接收到Topic.queue1的消息:【" + msg + "】"); } @RabbitListener(bindings = @QueueBinding( value = @Queue(name = "topic.queue2"), exchange = @Exchange(name = "itcast.topic" ,type = ExchangeTypes.TOPIC), key = "#.news" )) public void listenTopicQueue2(String msg) { System.err.println("消费者接收到Topic.queue2的消息:【" + msg + "】"); }
步骤2. 在publisher服务的SpringAmqpTest类中添加测试方法
@Test public void testSendTopicExchange() { //交换机名称 String exchangeName = "itcast.topic"; //消息 String message = "世界新闻"; //发送消息 rabbitTemplate.convertAndSend(exchangeName,"world.news", message); }
当key为china.news时,控制台输出如下:
当key为china.weather时,控制台输出如下:
当key为workd.news时,控制台输出如下:
消息转换器
Spring会把发送的消息序列化为字节发送给MQ,接收消息的时候,还会把字节反序列化为Java对象。只不过,默认情况下Spring采用的序列化方式是JDK序列化,而JDK序列化存在一些问题,如数据体积过大、有安全漏洞、可读性差等。
我们希望消息体的体积更小、可读性更高,因此可以使用JSON方式来做序列化和反序列化。
步骤1. 在publisher和consumer两个服务中都引入依赖:
com.fasterxml.jackson.dataformat jackson-dataformat-xml2.9.10 步骤2. 配置消息转换器,在启动类中添加一个Bean即可:
@Bean public MessageConverter jsonMessageConverter(){ return new Jackson2JsonMessageConverter(); }
猜你喜欢
- 17天前(兰州旅游文化产业发展有限公司)甘肃省兰州市2023年乡村旅游暨A级旅游景区管理工作培训班开班
- 17天前(从“见世面”到“内在需要”:在海南,追问旅行的意义)从“见世面”到“内在需要”:在海南,追问旅行的意义
- 17天前(安徽民宿发展报告)首届安徽省乡村民宿创意设计大赛启动
- 17天前(万豪酒店 珠海)万豪酒店品牌启航珠海金湾,续写大湾区拓展新篇
- 17天前(哥伦比亚号邮轮)爱达邮轮与哥仑比亚船舶管理集团达成合作
- 17天前(新西兰“空降”上海:新西兰旅游局邀请你来“玩真的”!)新西兰“空降”上海:新西兰旅游局邀请你来“玩真的”!
- 17天前(澳涞坞是什么)从最美山庄到世界舞台:澳涞山庄见证世界十佳旅居城市评选
- 17天前(澳涞山庄见证北欧零碳到中国实践,世界十佳环境保护城市榜单发布)澳涞山庄见证北欧零碳到中国实践,世界十佳环境保护城市榜单发布
- 17天前(上海迪士尼 夏天)酷爽夏日,奇妙相伴!来上海迪士尼度假区清凉入夏
- 17天前(曹妃甸美仑华府哪个楼层好)曹妃甸新城教育经济新引擎启动—美仑国际酒店盛大开业
网友评论
- 搜索
- 最新文章
- (2020广州车展哈弗)你的猛龙 独一无二 哈弗猛龙广州车展闪耀登场
- (哈弗新能源suv2019款)智能科技颠覆出行体验 哈弗重塑新能源越野SUV价值认知
- (2021款全新哈弗h5自动四驱报价)新哈弗H5再赴保障之旅,无惧冰雪护航哈弗全民电四驱挑战赛
- (海南航空现况怎样)用一场直播找到市场扩张新渠道,海南航空做对了什么?
- (visa jcb 日本)优惠面面俱到 JCB信用卡邀您畅玩日本冰雪季
- (第三届“堡里有年味·回村过大年”民俗花灯会活动)第三届“堡里有年味·回村过大年”民俗花灯会活动
- (展示非遗魅力 长安启源助力铜梁龙舞出征)展示非遗魅力 长安启源助力铜梁龙舞出征
- (阿斯塔纳航空公司)阿斯塔纳航空机队飞机数量增至50架
- (北京香港航班动态查询)香港快运航空北京大兴新航线今日首航
- (我在港航“呵护”飞机 每一次安全着陆就是最好的荣誉)我在港航“呵护”飞机 每一次安全着陆就是最好的荣誉
- 热门文章