1.同步通讯(打电话)
优点:时效性较强,可以立即得到结果。
缺点:
1.耦合度高:每次加入新的需求,都要修改原来的代码
2.性能下降:调用者需要等待服务提供者响应,如果调用链过长则响应时间等于每次调用的时间之和。
3.资源浪费:调用链中的每个服务在等待响应过程中,不能释放请求占用的资源,高并发场景下会极度浪费系统资源。
4.级联失败:如果服务提供者出现问题,所有调用方都会跟着出问题,迅速导致整个微服务群故障。
2.异步调用(发微信)
异步以事件驱动为主,事件进入Broker之后,把事件消息发布给订阅了Broker的服务
优点:
1.耦合度低
2.吞吐量提升
3.故障隔离
4.流量削峰
缺点:
1.依赖于Broker的可靠性,安全性,吞吐能力
2.架构复杂了,业务没有明显的流程线,不好追踪管理
3.什么是MQ
MQ(MessageQueue),消息队列,存放消息的队列。也就是事件驱动架构中的Broker.
4.RabbitMQ
1.概述
RabbitMQ是基于Erlang语言开发的开源消息通行中间件
2.RabbitMQ部署
1.单机部署
1.在线拉取
docker pull rabbitmq
2.安装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:latest
运行后进入 IP:15672网站,如果无法访问,执行下面命令,打开管理插件
docker exec -it mq_hzj /bin/bash # 进入容器
rabbitmq-plugins enable rabbitmq_management # 开启管理插件
3.RabbitMQ页面中的几个概念
1.channel:操作MQ的工具
2.exchange:路由消息到队列中
3.queue:缓存消息
4.virtual hose:虚拟主机,是对queue,exchange等资源的逻辑分组
3.常见消息模型
1.基本消息队列(BasicQueue)
基于最基础的消息队列模型来实现的,只包含三个角色:
publisher:消息发布者,将信息发送到队列queue
queue:消息队列,负责接收并缓存消息
consumer:订阅队列,处理队列中的消息
发送流程:
1.建立connection
2.创建channel
3.利用channel声明队列
4.利用channel向队列发送消息
package cn.itcast.mq.helloworld;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import org.junit.Test;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class PublisherTest {
@Test
public void testSendMessage() throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
factory.setHost("192.168.1.52");
factory.setPort(5672);
factory.setVirtualHost("/"); // 虚拟主机
factory.setUsername("houzhijie");
factory.setPassword("123");
// 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();
}
}
接受流程:
1.建立connection
2.创建channel
3.利用channel声明队列(防止队列还未声明)
4.定义consumer的消费行为handleDelivery()
5.利用channel将消费者与队列绑定
package cn.itcast.mq.helloworld;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ConsumerTest {
public static void main(String[] args) throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
factory.setHost("192.168.1.52");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("houzhijie");
factory.setPassword("123");
// 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("等待接收消息。。。。");
}
}
5.SpringAMQP
AMQP(Advanced Message Queuing Protocol):是用于在应用程序或之间传递业务消息的开放标准。该协议与语言和平台无关,更符合微服务中独立性的要求。
Spring AMQP:是基于AMQP协议定义的一套API规范,提供了模板来发送和接收消息。包含两部分,其中spring-amqp是基础抽象,spring-rabbit是底层的默认实现。
1.利用SpringAMQP实现HelloWorld中的基础消息队列功能
一次性消息,阅完即毁
1.在父过程中引入spring-amqp的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2.在publisher服务中发送消息
1.在publisher服务中编写application.yml,添加mq连接信息:
spring:
rabbitmq:
host: 192.168.1.52 # rabbitMQ的ip地址
port: 5672 # 端口
username: houzhijie
password: 123
virtual-host: / # 虚拟主机
2.在publisher服务中编写单元测试
mq不会自动新建队列,需要手动创建这个队列
package cn.itcast.mq.spring;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSendMessage2SimpleQueue() {
String queueName = "simple.queue";
String message = "hello world!";
rabbitTemplate.convertAndSend(queueName, message);
}
}
3.在consumer中编写消费逻辑,监听simple.queue
1.在consumer服务中编写application.yml,添加mq连接信息
spring:
rabbitmq:
host: 192.168.1.52 # rabbitMQ的ip地址
port: 5672 # 端口
username: houzhijie
password: 123
virtual-host: / # 虚拟主机
2.在consumer服务中监听,编写消费逻辑
package cn.itcast.mq.listener;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "simple.queue")
public void listenerSimpleQueue(String msg) {
System.out.println("消费者接收到simple.queue的消息: " + msg);
}
}
2.Work Queue工作队列,一个队列绑定多个消费者
一次性消息,阅完即毁
多写几个监听方法就好了
消费预取:消费者会一次从队列中取多条数据,然后慢慢处理,会影响消息的处理速度
修改方式:
logging:
pattern:
dateformat: MM-dd HH:mm:ss:SSS
spring:
rabbitmq:
host: 192.168.1.52 # rabbitMQ的ip地址
port: 5672 # 端口
username: houzhijie
password: 123
virtual-host: / # 虚拟主机
listener:
simple:
prefetch: 1 # 预期消息的条数
3.发布(Publish)订阅(Subscribe)模式
发布订阅模式与之前的案例的区别就是允许将同一消息发送给多个消费者。实现方式是在发布者和队列之间加入了exchange(交换机)。
常见exchange类型包括:
1.Fanout:广播
2.Dirct:路由
3.Topic:话题
exchange负责消息路由,而不是存储,路由失败则消息丢失。
1.Fanout Exchange
Fanout Exchange会将接收到的消息路由到每一个跟其绑定的queue
1.在消费服务中,利用代码声明队列,交换机,并绑定
package cn.itcast.mq.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FanoutConfig {
// 交换机声明
@Bean
public FanoutExchange fanoutExchange() {
return new FanoutExchange("hzj.fanout");
}
// 声明队列1
@Bean
public Queue fanoutQueue1() {
return new Queue("hzj.queue1");
}
// 绑定队列1到交换机
@Bean
public Binding fanoutBinding1(Queue fanoutQueue1, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
}
// 声明队列2
@Bean
public Queue fanoutQueue2() {
return new Queue("hzj.queue2");
}
// 绑定队列2到交换机
@Bean
public Binding fanoutBinding2(Queue fanoutQueue2, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
}
}
2.消费服务中,编写两个消费者方法,分别监听两个队列
@RabbitListener(queues = "hzj.queue1")
public void listenerFanoutQueue1(String msg) {
System.err.println("消费者1 接收到hzj.queue1的消息: " + msg + LocalTime.now());
}
@RabbitListener(queues = "hzj.queue2")
public void listenerFanoutQueue2(String msg) {
System.err.println("消费者2 接收到hzj.queue2的消息: " + msg + LocalTime.now());
}
3.发送消息
@Test
public void testSendFanoutExchange() {
// 交换机名称
String exchangeName = "hzj.fanout";
// 消息
String message = "hello, every one";
// 发送消息
rabbitTemplate.convertAndSend(exchangeName, "", message);
}
2.DirectExchange
Direct Exchange会将接收到的消息根据规则路由到指定的Queue,因此称为路由模式(routes)
每个Queue都与Exchange设置一个BindingKey
发布者发送消息时,指定消息的RoutingKey
Exchange将消息路由到BindingKey与信息RoutingKey一致的队列
BindingKey可以相同,相同时每个队列都会收到消息
1.利用@RabbitListener声明Exchange,Queue,RoutingKey
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue1"),
exchange = @Exchange(name = "hzj.direct", type = ExchangeTypes.DIRECT), // 默认是Direct交换机
key = {"red", "blue"}))
public void listenerDirectQueue1(String msg) {
System.out.println("消费者接收到direct.queue1的消息" + msg);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue2"),
exchange = @Exchange(name = "hzj.direct", type = ExchangeTypes.DIRECT), // 默认是Direct交换机
key = {"red", "yellow"}))
public void listenerDirectQueue2(String msg) {
System.out.println("消费者接收到direct.queue2的消息" + msg);
}
2.发送
@Test
public void testSendDirectExchange() {
// 交换机名称
String exchangeName = "hzj.direct";
// 消息
String message = "hello, every one";
// 发送消息
rabbitTemplate.convertAndSend(exchangeName, "red", message);
}
3.TopicExchange
TopicExchange与DirectExchange类似,区别在与routingKey必须是多个单词的列表,并且用 . 分割
Queue与Exchange指定BindingKey时可以使用通配符:
#:代指0个或多个单词
*:代指一个单词
1.声明交换机和队列
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue1"),
exchange = @Exchange(name = "hzj.topic", type = ExchangeTypes.TOPIC),
key = "china.#"
))
public void listenerTopicQueue1(String msg) {
System.out.println("消费者接收到了topic.queue1的消息" + msg);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue2"),
exchange = @Exchange(name = "hzj.topic", type = ExchangeTypes.TOPIC),
key = "#.news"
))
public void listenerTopicQueue2(String msg) {
System.out.println("消费者接收到了topic.queue2的消息" + msg);
}
2.发送
@Test
public void testSendTopicExchange() {
// 交换机名称
String exchangeName = "hzj.topic";
// 消息
String message = "hzj nb";
// 发送消息
rabbitTemplate.convertAndSend(exchangeName, "china.news", message);
}
4.消息转换器
Spring的对消息对象的处理是由org.springframework.amop.support.converter.MessageConverter来处理的。而默认实现是SimpleMessageConverter,基于JDK的ObjectOutputStream完成序列化。
如果要修改只需要定义一个MessageConverter类型的Bean接口,推荐使用JSON方式序列化
1.在publisher引入依赖
<!-- json依赖 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
2.重新定义MessageConverter
@Bean
public Jackson2JsonMessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
3.接收方重新定义MessageConverter
4.接收消息
@RabbitListener(queues = "object.queue")
public void listenerObjectQueue(Map<String, Object> msg) {
System.out.println("接收到object.queue的消息" + msg);
}