持续学习&持续更新中…
守破离
微服务间通讯有同步和异步两种方式:
同步通讯:就像打电话,需要实时响应。
异步通讯:就像发邮件,不需要马上回复。
两种方式各有优劣,打电话可以立即得到响应,但是你却不能跟多个人同时通话。
我们之前学习的Feign调用就属于同步方式,虽然调用可以实时得到结果,但存在下面的问题:
总结:
同步调用的优点:
同步调用的问题:
异步调用则可以避免上述问题:
我们以购买商品为例,用户支付后需要调用订单服务完成订单状态修改,也需要调用物流服务,从仓库分配响应的库存并准备发货。
在事件模式中,支付服务是事件发布者(publisher),在支付完成后只需要发布一个支付成功的事件(event),事件中带上订单id。
订单服务和物流服务是事件订阅者(consumer),订阅支付成功的事件,监听到事件后完成自己业务即可。
为了解除事件发布者与订阅者之间的耦合,两者并不是直接通信,而是有一个中间人(Broker)。发布者发布事件到Broker,不关心谁来订阅事件。订阅者从Broker订阅事件,不关心谁发来的消息。
Broker 是一个像数据总线一样的东西,所有的服务要接收数据和发送数据都发到这个总线上,这个总线就像协议一样,让服务间的通讯变得标准和可控。
异步调用常见实现就是事件驱动模式:
事件驱动优势:
总结:
异步调用的好处:
耦合度低,每个服务都可以灵活插拔,可替换
吞吐量高:无需等待订阅者处理完成,响应更快速,就可以处理更多的用户请求
故障隔离:服务没有直接调用,不存在级联失败问题
流量削峰:不管发布事件的流量波动多大,都由Broker接收,订阅者可以按照自己的速度去处理事件
异步调用的缺点:
大多数情况下我们对并发并没有很高的要求,相反而言我们对实效性要求很高,需要用到服务的返回结果,因此大多数我们使用同步。
异步调用只适合那种不需要知道返回结果的、只需通知一下的、对并发和吞吐量要求较高的、需要服务解耦的服务调用
注意:
我们在Centos7虚拟机中使用Docker来安装。
下载镜像:
方式一:在线拉取
docker pull rabbitmq:3-management
方式二:从本地加载:在课前资料已经提供了镜像包,上传到虚拟机中后,使用命令加载镜像即可:
docker load -i mq.tar
安装MQ:
执行下面的命令来运行MQ容器:
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
访问MQ或者登录MQ的管理平台都需要用到环境变量配置的用户名和密码
集群部署必须配置--hostname
-p 15672:15672
RabbitMQ的管理页面端口
-p 5672:5672
消息通信的端口
查看管理页面:
官方的HelloWorld是基于最基础的消息队列模型来实现的,只包括三个角色:
实现步骤:
代码:
Publisher:
public class PublisherTest {
@Test
public void testSendMessage() throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
factory.setHost("192.168.152.134");
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();
}
}
Consumer:
public class ConsumerTest {
public static void main(String[] args) throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
factory.setHost("192.168.152.134");
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.订阅消息
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("等待接收消息。。。。");
}
}
注意:由于Publisher和Consumer并不一定谁先启动,因此它俩都需要创建(声明)队列【MQ内部会优化,并不会创建两个相同的队列】
总结:
基本消息队列的消息发送流程:
基本消息队列的消息接收流程:
因为publisher和consumer服务都需要amqp依赖,因此这里把依赖直接放到父工程mq-demo中:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
dependency>
在publisher服务中编写application.yml,添加mq连接信息:
spring:
rabbitmq:
host: 192.168.152.134
port: 5672
username: itcast
password: 123321
virtual-host: /
在publisher服务中新建一个测试类,编写测试方法:
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAMQPTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSendSimpleQueue() {
String queueName = "simple.queue";
String message = "Hello, SpringAMQP! I am LP!";
rabbitTemplate.convertAndSend(queueName, message);
}
}
在consumer服务中编写application.yml,添加mq连接信息:
在consumer服务中新建一个类,编写消费逻辑:
@Component
public class SpringAMQPListener {
@RabbitListener(queues = "simple.queue")
public void receiveSimpleQueueMessage(String message) {
System.out.println("接收到了simple.queue的消息:【" + message + "】");
}
}
注意:
publisher:
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAMQPTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSendWorkQueue() throws Exception {
String queueName = "simple.queue";
String message = "Hello, I am ";
for (int i = 1; i <= 50; i++) {
rabbitTemplate.convertAndSend(queueName, message + i);
// 1秒钟等于1000毫秒
// 模拟1秒发送50条消息
TimeUnit.MILLISECONDS.sleep(20);
}
}
}
consumer:
@Component
public class SpringAMQPListener {
@RabbitListener(queues = "simple.queue")
public void receiveWorkQueueMessage1(String message) throws Exception {
System.out.println("WorkQueueMessage1接收到了消息:【" + message + "】" + LocalTime.now());
// TimeUnit.MILLISECONDS.sleep(25); // 模拟一秒钟处理40个消息
TimeUnit.MILLISECONDS.sleep(20); // 模拟一秒钟处理50个消息
}
@RabbitListener(queues = "simple.queue")
public void receiveWorkQueueMessage2(String message) throws Exception {
System.err.println("WorkQueueMessage2-----------接收到了消息:【" + message + "】" + LocalTime.now());
// TimeUnit.MILLISECONDS.sleep(50); // 模拟一秒钟处理20个消息
// TimeUnit.MILLISECONDS.sleep(500); // 模拟一秒钟处理2个消息
TimeUnit.MILLISECONDS.sleep(100); // 模拟一秒钟处理10个消息
}
}
有一个问题:
什么是消费预取机制:
配置预取机制:
总结:交换机的作用是什么?
consumer:
@Configuration
public class FanoutExchangeConfig {
// 声明Fanout交换机
@Bean
public FanoutExchange fanoutExchange() {
return new FanoutExchange("itcast.fanout");
}
// 声明第1个队列
@Bean
public Queue fanoutQueue1() {
return new Queue("fanout.queue1");
}
//绑定队列1和交换机
@Bean
public Binding bindingQueue1(FanoutExchange fanoutExchange, Queue fanoutQueue1) {
return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
}
// 声明第2个队列
@Bean
public Queue fanoutQueue2() {
return new Queue("fanout.queue2");
}
//绑定队列2和交换机
@Bean
public Binding bindingQueue2(FanoutExchange fanoutExchange, Queue fanoutQueue2) {
return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
}
}
@Component
public class SpringAMQPListener {
@RabbitListener(queues = "fanout.queue1")
public void listenFanoutQueue1(String message) {
System.err.println("fanoutQueue1-----------接收到了消息:【" + message + "】");
}
@RabbitListener(queues = "fanout.queue2")
public void listenFanoutQueue2(String message) {
System.err.println("fanoutQueue2接收到了消息:【" + message + "】");
}
}
Direct Exchange 会将接收到的消息根据规则路由到指定的Queue,因此称为路由模式(routes)。
consumer:
@Component
public class SpringAMQPListener {
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(name = "direct.queue1"),
exchange = @Exchange(name = "itcast.direct", type = "direct"),
key = {"blue", "red"}
)
)
public void listenDirectExchangeQueue1(String message) {
System.out.println("listenDirectExchangeQueue1:message:【" + message + "】");
}
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(name = "direct.queue2"),
exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),
key = {"yellow", "red"}
)
)
public void listenDirectExchangeQueue2(String message) {
System.err.println("listenDirectExchangeQueue2——>message:【" + message + "】");
}
}
publisher:
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAMQPTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testDirectExchangeQueue() {
String exchangeName = "itcast.direct";
// String message = "Hello, blue!";
// 发送消息,参数依次为:交换机名称,RoutingKey,消息
// rabbitTemplate.convertAndSend(exchangeName, "blue", message);
// String message = "Hello, yellow!";
// rabbitTemplate.convertAndSend(exchangeName, "yellow", message);
String message = "Hello, red!";
rabbitTemplate.convertAndSend(exchangeName, "red", message);
}
}
.
分割。#
:代指0个或多个单词*
:代指一个单词consumer:
@Component
public class SpringAMQPListener {
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(name = "topic.queue1"),
exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),
key = "china.#"
)
)
public void listenTopicExchangeQueue1(String message) {
System.err.println("listenTopicExchangeQueue1——>message:【" + message + "】");
}
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(name = "topic.queue2"),
exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),
key = "#.news"
)
)
public void listenTopicExchangeQueue2(String message) {
System.out.println("listenTopicExchangeQueue2==>message:【" + message + "】");
}
}
publisher:
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAMQPTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testTopicExchangeQueue() {
String exchangeName = "itcast.topic";
// String message = "新闻:传智教育【教育行业IPO第一股】上市了!";
// rabbitTemplate.convertAndSend(exchangeName, "china.news", message);
String message = "天气:晴天,34摄氏度";
rabbitTemplate.convertAndSend(exchangeName, "china.weather", message);
}
}
PS:老师使用上述方法在consumer中声明队列的目的是在浏览器中查看队列中的消息,自己实现完全可以不这样做,直接使用注解即可。
默认的JDK序列化的方式:这样既不直观,传输的字符又过长,因此,推荐换一种使用JSON序列化的方式。
Spring的对消息对象的处理是由org.springframework.amqp.support.converter.MessageConverter
来处理的。
而默认实现是SimpleMessageConverter
,基于JDK的ObjectOutputStream完成序列化。
如果要修改只需要定义一个 MessageConverter 类型的Bean即可。
推荐用JSON方式序列化,步骤如下:
黑马程序员:SpringCloud微服务技术栈.
本文完,感谢您的关注支持!