springcloud微服务技术栈
4、异步通信
1、初识MQ
-
先导概念:微服务间的调用有两种:同步调用和异步调用。
-
同步调用:Feign会是一种同步调用,在消费者服务中远程顺序调用多个提供者客户端服务,消费者必须要等一个服务执行结束后再等待下一个服务执行。
- 优点:时效性强
- 缺点:系统吞吐量差、代码间耦合度高(服务与服务之间的调用)、级联调用失败的问题(即一个服务挂掉了,系统就会一直等下去)。
-
异步调用:常见的异步调用主要是事件驱动模式,假设用户使用支付服务,支付服务推送一个事件消息给事件代理器(Broker),然后它就立刻返回通知用户支付成功了不用等待其他服务处理结果。而其他服务通过订阅事件代理器来接收到事件代理器中的通知,一旦有通知自己执行的消息就执行相关代码即可。
- 优点:服务解耦、系统吞吐量提升、级联问题解决(故障隔离)、流量削峰(同一时间的大量请求可能会使服务器挂掉,通过事件处理器对这些大量的请求进行挤压,分成一批又一批的请求交给服务器处理)
- 缺点:依赖于Broker的可靠性、安全性、吞吐能力;架构复杂,业务没有明显流程线,不好追踪管理。
-
异步同步使用场景:一般来说同步用的多;异步主要用于对并发要求高的场合,且不关心业务的处理结果。
-
-
什么是MQ:MQ,中文是消息队列,字面上看就是存放时间的队列。也就是事件驱动中的Broker。
不同MQ间的对比:
Rabbit ActiveMQ Rocket Kafka 公司/社区 Rabbit Apache 阿里 Apache 开发语言 Erlang Java Java Scala&Java 协议支持 AMQP、XMPP、SMTP、STOMP OpenWire、STOMP、REST、XMPP、AMQP 自定义协议 自定义协议 可用性 高 一般 高 高 单机吞吐量 一般 差 消息延迟 微秒级 毫秒级 毫秒级 毫秒以内 消息可靠性 高 一般 高 一般 -
使用的多:Rabbit、Rocket、Kafka、MQ
2、RabbitMQ快速入门
-
RabbitMQ:基于Erlang语言,支持系统的高可用性。
-
安装与部署:
-
1)windows下,安装暂时略过,pdf中有。
-
2)Linux下单机部署
-
1)在线拉取:docker pull rabbitmq:3-management(带视图管理)
-
2)运行(15672是控制台端口,5672是连接端口)
docker run \ -e RABBITMQ_DEFAULT_USER=myname1 \ #mq视图管理登录用户名 -e RABBITMQ_DEFAULT_PASS=123456 \ #用户密码 --name mq \ --hostname mq1 \ #配置主机名,做集群时用 -p 15672:15672 \ #rabbitmq:3-management的端口 -p 5672:5672 \ -d \ rabbitmq:3-management
-
-
3)Linux下集群部署
-
在RabbitMQ的官方文档中,讲述了两种集群的配置方式:
- 普通模式:普通模式集群不进行数据同步,每个MQ都有自己的队列、数据信息(其它元数据信息如交换机等会同步)。例如我们有2个MQ:mq1,和mq2,如果你的消息在mq1,而你连接到了mq2,那么mq2会去mq1拉取消息,然后返回给你。如果mq1宕机,消息就会丢失。
- 镜像模式:与普通模式不同,队列会在各个mq的镜像节点之间同步,因此你连接到任何一个镜像节点,均可获取到消息。而且如果一个节点宕机,并不会导致数据丢失。不过,这种方式增加了数据同步的带宽消耗。
-
-
-
RabbitMQ大体结构和概念
- channel:操作MQ的工具
- exchange:路有消息到队列中
- queue:缓存消息队列
- virtualhost:虚拟主机,是对queue和exchange等资源进行隔离。
-
RabbitMQ同样将服务分为两类:消息提供者和消息消费者
-
简单例子:
提供者: 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(); } 消费者: public static void main(String[] args) 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.订阅消息 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 + "】"); } }); //注意:控制台先打印下面这一句,再执行上面的handleDelivery方法。 System.out.println("等待接收消息。。。。"); }
3、SpringAMQP
-
什么是AMQP:
-
SpringAMQP:是对AMQP协议的一种实现,它使用RabbitMQ去实现便捷的消息发送与接收,以及自动化声明队列。
下面介绍SpringAMQP常用模型和使用方式
-
1、简单发送-消费模型
SpringAMQP发送消息使用步骤:
-
1)导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
-
2)配置
spring: rabbitmq: host: 192.168.56.10 # rabbitMQ的ip地址 port: 5672 # 端口 username: myname1 password: 123456 virtual-host: /
-
3)代码使用
发送 @Test public void testSendMessage2SimpleQueue() { String queueName = "simple.queue"; String message = "hello, spring amqp!"; rabbitTemplate.convertAndSend(queueName, message); }
-
-
SpringAMQP接受消息使用步骤:
-
1)、2)步与上面一致
-
3)代码使用(注册为组件,交由springboot完成)
@Component public class SpringRabbitListener { @RabbitListener(queues = "simple.queue") public void listenSimpleQueue(String msg) { System.out.println("消费者接收到simple.queue的消息:【" + msg + "】"); }
-
-
2、SpringAMQP工作(Work)模型:假设消息提供者同一时间发出大量小子,一个消息消费者处理不及导致消息队列被塞满,后续的的消息都会被丢失掉,所以为了解决这个问题出现了两个消费者共同去处理这个消息队列。
-
消息预读机制:在上面的这个例子中,假如说这两个消费者一个处理快一个处理慢,我们想让快的多处理一些消息,慢的少处理一些,但是在rabbitmq中这两个实际上会差不多一样的消息数量。这种情况出现的原因就是消息预读机制,消费者先拿取消息再进行处理。
解决办法:
-
3、SpringAMQP发送订阅模型:之前的简单发送消费模式和工作模型都没法解决一开始那个例子的问题:即同一个支付消息要被几个服务共同读取才行。而发送订阅模型就刚好解决了这个问题。
-
SpringAMQP提供了三种交换机:
-
1)FanoutExchange
-
使用方法:
-
1)消息消费者创建exchange(交换机)绑定队列
@Configuration public class FanoutConfig { // my.fanout @Bean public FanoutExchange fanoutExchange(){ return new FanoutExchange("my.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)消息消费者创建对应队列的监听器
@Component public class 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)消息发送者发送到交换机
@Test public void testSendFanoutExchange() { // 交换机名称 String exchangeName = "my.fanout"; // 消息 String message = "hello, every one!"; // 发送消息:交换机,RoutingKey(暂时未空),消息 rabbitTemplate.convertAndSend(exchangeName, "", message); }
-
-
-
2)DirectExchange:会将接收到的消息根据规则路由到指定Queue,因此称为路由模式。
-
1)与上面的例子不同,这次利用@RabbitListener注解实现声明Exchange、Queue、RoutingKey
@Component public class SpringRabbitListener { @RabbitListener(bindings = @QueueBinding( value = @Queue(name = "direct.queue1"), exchange = @Exchange(name = "my.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 = "my.direct", type = ExchangeTypes.DIRECT), key = {"red", "yellow"} )) public void listenDirectQueue2(String msg){ System.out.println("消费者接收到direct.queue2的消息:【" + msg + "】"); } }
-
2)消息发送者
@Test public void testSendDirectExchange() { // 交换机名称 String exchangeName = "my.direct"; // 消息 String message = "hello, red!"; // 发送消息 rabbitTemplate.convertAndSend(exchangeName, "red", message); }
-
-
3)TopicExchange:使用通配符匹配多个RoutingKey
-
使用方法与DirectExchange类似
-
1)声明
@Component public class SpringRabbitListener { @RabbitListener(bindings = @QueueBinding( value = @Queue(name = "my.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 = "my.topic", type = ExchangeTypes.TOPIC), key = "#.news" )) public void listenTopicQueue2(String msg){ System.out.println("消费者接收到topic.queue2的消息:【" + msg + "】"); } }
-
2)消息发送
@Test public void testSendTopicExchange() { // 交换机名称 String exchangeName = "my.topic"; // 消息 String message = "今天天气不错,我的心情好极了!"; // 发送消息 rabbitTemplate.convertAndSend(exchangeName, "china.weather", message); }
-
-
-
-
消息转换器:消息发送者发送的消息可以是是字符串,或者对象,但是对象会默认使用JDK的对象流进行序列化后发送(不推荐)。我们可以使用自定义的方式创建一个MessageConventer类型的Bean来完成对象使用json序列化。
String exchangeName = “my.topic”;
// 消息
String message = “今天天气不错,我的心情好极了!”;
// 发送消息
rabbitTemplate.convertAndSend(exchangeName, “china.weather”, message);
}
```
- 消息转换器:消息发送者发送的消息可以是是字符串,或者对象,但是对象会默认使用JDK的对象流进行序列化后发送(不推荐)。我们可以使用自定义的方式创建一个MessageConventer类型的Bean来完成对象使用json序列化。