一、同步异步通讯
1.同步通讯和异步通讯
同步通讯:当A服务调用B服务时,一直等待B服务执行完毕,A才继续往下走
优点:时效性强,可以立即得到结果。
缺点:
①耦合度高,加入新的需求就要修改原来代码
②性能下降,调用者需要等待服务提供者的响应,如果调用链过长则响应时间是每次调用时间之和。
③资源浪费, 每个调用者在等待服务响应的过程中,不能释放资源,高并发会浪费系统资源
④级联失败,如果提供者出现问题,所有的调用都会出现问题。
异步通讯: 当A服务调用B服务时,不需要等待B服务执行完毕,A就可以往下走(事件驱动模式)
优点:
①服务解耦
②性能提升,吞吐量提高。
③服务没有强依赖,不担心级联失败问题
④流量削峰
缺点:
①依赖Broker的可靠性、安全性、吞吐能力
②架构复杂,业务没有明显的流程线,不好追踪管理
2.什么是MQ(MessageQueue)
存放消息的队列,事件驱动架构的Broker
二、RabbitMQ快速入门
1.RabbitMQ概述
RabbitMQ是开源消息通信中间件
生产者-》交换机-》信道-》队列-》消费者
①Publisher:消息的发送者,把消息发送到exchange交换机
②exchange:交换机,负责路由,把消息投递到queue队列
③queue:队列,负责缓存消息
②consumer:消息的消费者,从队列获取消息,处理消息。
总结
RabbitMQ中的几个概念:
channel:操作MQ的工具
exchange:路由消息到队列中
queue:缓存消息
virtual host:虚拟主机,是对queue、exchange等资源的逻辑分组
2.运行RabbitMQ命令
docker run \
-e RABBITMQ_DEFAULT_USER=itcast \
-e RABBITMQ_DEFAULT_PASS=123321 \
--name mq \
--hostname mq1 \
-p 15672:15672 \
-p 5672:5672 \
-d \
rabbitmq:3-management
①端口15672是MQ管理平台的端口
②端口5672是消息通信的端口
3.常见消息模型
①基本消息队列(BasicQueue)
②工作消息队列(WorkQueue)
③发布订阅(Publish,Subscribe),根据交换机分为三种类型
Fanout Exchange:广播
Direct Exchange:路由
Topic Exchange:主题
4.HelloWorld案例
Publisher:消息发布者,将消息发送到队列queue
Queue:消息队列,负责接受并缓存消息
Consumer:订阅队列,处理队列中的消息
发送流程
- 建立connection
- 创建channel
- 利用channel声明队列
- 利用channel向队列发送消息
接收流程
- 建立connection
- 创建channel
- 利用channel声明队列
- 定义consumer的消费行为handleDelivery()
- 利用channel将消费者与队列绑定
三、SpringAMOP
1.什么是SpringAMOP
SpringAMOP是基于AMOP协议定义的一套API规范,提供模板接收和发送消息。包括两部分,spring-amqp是基础抽象,spring-rabbit是底层的默认实现。
2.案例:利用SpringAMQP实现HelloWorld中的基础消息队列功能
发送
- 引入amqp的starter依赖
- 配置RabbitMQ地址
- 利用RabbitTemplate的convertAndSend方法
①引入AMQP依赖:子模块都需要amop,所以在把依赖写在父工程
<!--AMQP依赖,包含RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
②在publisher服务中编写配置文件application.yml,连接mq信息
spring:
rabbitmq:
host: 192.168.150.101 # 主机名
port: 5672 # 端口
virtual-host: / # 虚拟主机
username: itcast # 用户名
password: 123321 # 密码
③测试类
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSimpleQueue() {
String queueName = "simple.queue";
String message = "hello, spring amqp!";
rabbitTemplate.convertAndSend(queueName, message);
}
}
接收
- 引入amqp的starter依赖
- 配置RabbitMQ地址
- 定义类,添加@Component注解
- 类中声明方法,添加@RabbitListener注解,方法参数就时消息
①②跟前面一样,编写③消费逻辑类
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueueMessage(String msg) throws InterruptedException {
System.out.println("spring 消费者接收到消息 :【" + msg + "】");
}
}
3.WorkQueue工作队列
Work queue工作队列,可以提高消息处理速度,避免队列消息堆积
案例:模拟WorkQueue,实现一个队列绑定多个消费者
基本思路如下:
- 在publisher服务中定义测试方法,每秒产生50条消息,发送到simple.queue
- 在consumer服务中定义两个消息监听者,都监听simple.queue队列
- 消费者1每秒处理50条消息,消费者2每秒处理10条消息
①修改publisher,1秒发送50条消息
@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);
}
}
②两个消费者监听simple.queue。消费者1每秒处理50条,消费者2每秒处理10条。
消费者1处理效率高。
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueueMessage(String msg) throws InterruptedException {
System.out.println("spring 消费者1接收到消息:【" + msg + "】");
Thread.sleep(20);
}
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueueMessage2(String msg) throws InterruptedException {
System.err.println("spring 消费者2接收到消息:【" + msg + "】");
Thread.sleep(100);
}
此时的两个消费者的消息数是相同的【平均分配】,但两个消费者处理消息的效率不一样。
prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息,能者多劳。
总结
Work模型的使用:
- 多个消费者绑定到一个队列,同一条消息只会被一个消费者处理
- 通过设置prefetch来控制消费者预取的消息数量
4.发布Publish,订阅Subscribe
发布订阅模式跟之前案例区别是允许将同一个消息发送给多个消费者。实现方式是加入了exchange交换机。
①常见的交换机exchange类型
Fanout广播
Direct路由
Topic话题
注意:exchange负责消息路由(转发),而不是存储,路由失败则消息丢失
发布订阅-Fanout Exchange
Fanout Exchange会将接收到的消息路由到每一个跟其绑定的queue
案例:利用SpringAMQP演示FanoutExchange的使用
实现思路:
①在consumer服务中,利用代码声明队列,交换机,并将两者绑定。
②在consumer服务中,编写两个消费者方法,分别监听fanout.queue1和fanout.queue2
③在publisher中编写测试方法,向itcast.fanout发送消息
步骤1:在consumer服务声明Exchange、Queue、Binding
@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);
}
}
步骤2:在consumer服务声明两个消费者
@Component
public class SpringListener {
@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服务发送消息到FanoutExchange
@RunWith(SpringRunner.class) // 开启注解
@SpringBootTest
public class PublisherApplicationTests {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSendFanoutExchange() {
// 交换机名称
String exchangeName = "itcast.fanout";
// 消息
String message = "hello, every one!";
// 发送消息
rabbitTemplate.convertAndSend(exchangeName, "", message);
}
}
总结:
交换机的作用是什么?
①接收publisher发送的消息
②将消息按照规则路由到绑定的队列
③不能缓存消息,路由失败,消息丢失
④FanoutExchange将消息路由到每个绑定的队列
声明队列,交换机,绑定关系的Bean是什么?
Queue,FanoutExchange,Binding
发布订阅-DirectExchange
DirectExchange会将接收到的消息根据规则路由到指定的Queue,因此称为路由模式
①每一个Queue都与Exchange设置一个BindingKey
②发布者发送消息时,指定消息的RoutingKey
③Exchange将消息路由到BindingKey与消息RoutingKey一致的队列
案例:利用SpringAMQP演示DirectExchange的使用
实现思路:
①利用@RabbitListener声明Exchange,Queue,RoutingKey
②在consumer服务中,编写两个消费者方法,分别监听direct.queue1和direct.queue2
③在publisher中编写测试方法,向itcast. direct发送消息
步骤1:在consumer服务声明Exchange、Queue
@Component
public class SpringListener {
@RabbitListener(bindings=@QueueBinding(
value = @Queue(name = "direct.queue1"),
exchange = @Exchange(name = "itcast.direct",type = ExchangeTypes.DIRECT),
key = {"red","blue"}
))
public void listenFanoutQueue1(String msg) {
System.out.println("消费者接收到fanout.queue1的消息:【" + msg + "】");
}
@RabbitListener(bindings=@QueueBinding(
value = @Queue(name = "direct.queue2"),
exchange = @Exchange(name = "itcast.direct",type = ExchangeTypes.DIRECT),
key = {"red","yellow"}
))
public void listenFanoutQueue2(String msg) {
System.out.println("消费者接收到fanout.queue2的消息:【" + msg + "】");
}
}
步骤2:在publisher服务发送消息到DirectExchange
@RunWith(SpringRunner.class) // 开启注解
@SpringBootTest
public class PublisherApplicationTests {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSendFanoutExchange() {
// 交换机名称
String exchangeName = "itcast.direct";
// 消息
String message = "hello, every one!!!";
// 发送消息
rabbitTemplate.convertAndSend(exchangeName, "yellow", message);
}
}
发布订阅-TopicExchange
TopicExchange与DirectExchange类似,区别在于routingKey必须是多个单词的列表,并且以 . 分割。
Queue与Exchange指定BindingKey时可以使用通配符:
#:代指0个或多个单词
*:代指一个单词
利用SpringAMQP演示TopicExchange的使用,匹配使用
@Component
public class SpringListener {
@RabbitListener(bindings=@QueueBinding(
value = @Queue(name = "direct.queue1"),
exchange = @Exchange(name = "itcast.topic",type = ExchangeTypes.TOPIC),
key = "china.#"
))
public void listenFanoutQueue1(String msg) {
System.out.println("消费者接收到fanout.queue1的消息:【" + msg + "】");
}
@RabbitListener(bindings=@QueueBinding(
value = @Queue(name = "direct.queue2"),
exchange = @Exchange(name = "itcast.topic",type = ExchangeTypes.TOPIC),
key = "#.news"
))
public void listenFanoutQueue2(String msg) {
System.out.println("消费者接收到fanout.queue2的消息:【" + msg + "】");
}
}
描述下Direct交换机与Topic交换机的差异?
- Topic交换机接收的消息RoutingKey必须是多个单词,以 . 分割
- Topic交换机与队列绑定时的bindingKey可以指定通配符
- #:代表0个或多个词
- *:代表1个词