1.同步与异步:
微服务间通讯有同步和异步两种方式:同步和异步,同步一般时效性比较强的业务需要用到同步,一个逻辑的执行需要另一个逻辑的结果,这种耦合度很高的必须同步,但是如果时效性不是很强的业务,应该使用异步方式,

比如下面这个例子:

如果同步会有很多弊端:耦合度高,性能下降(吞吐量低)因为各个模块之间需要等待,是一个串行,每个服务运行是需要占用cpu资源的,串行会导致占着茅坑不拉屎造成cpu资源浪费,一个模块出现问题会导致多米诺效应。

2.实现异步的方案:事件驱动模式,其实事件就是消息,比如支付服务完成后给一个中间件发送一个事件,说我支付完成了,然后其他服务就可以从这个中间件拿到事件,然后进行处理,各个服务之间谁也管不着谁,实现异步执行。
在事件模式中,支付服务是事件发布者(publisher),在支付完成后只需要发布一个支付成功的事件(event),事件中带上订单id。
订单服务和物流服务是事件订阅者(Consumer),订阅支付成功的事件,监听到事件后完成自己业务即可。
为了解除事件发布者与订阅者之间的耦合,两者并不是直接通信,而是有一个中间人(Broker)。发布者发布事件到Broker,不关心谁来订阅事件。订阅者从Broker订阅事件,不关心谁发来的消息。

模式的优缺点:

好处:
-
吞吐量提升:无需等待订阅者处理完成,响应更快速
-
故障隔离:服务没有直接调用,不存在级联失败问题
-
调用间没有阻塞,不会造成无效的资源占用
-
耦合度极低,每个服务都可以灵活插拔,可替换
-
流量削峰:不管发布事件的流量波动多大,都由Broker接收,订阅者可以按照自己的速度去处理事件
缺点:
- 架构复杂了,业务没有明显的流程线,不好管理
- 需要依赖于Broker的可靠、安全、性能
而MQ(消息中间件)就是其中的一种技术。
3.MQ,消息队列(MessageQueue),就是存放消息的队列。也就是事件驱动架构中的Broker
几种常见MQ的对比:
| RabbitMQ | ActiveMQ | RocketMQ | Kafka | |
|---|---|---|---|---|
| 公司/社区 | Rabbit | Apache | 阿里 | Apache |
| 开发语言 | Erlang | Java | Java | Scala&Java |
| 协议支持 | AMQP,XMPP,SMTP,STOMP | OpenWire,STOMP,REST,XMPP,AMQP | 自定义协议 | 自定义协议 |
| 可用性 | 高 | 一般 | 高 | 高 |
| 单机吞吐量 | 一般 | 差 | 高 | 非常高 |
| 消息延迟 | 微秒级 | 毫秒级 | 毫秒级 | 毫秒以内 |
| 消息可靠性 | 高 | 一般 | 高 | 一般 |
追求可用性:Kafka、 RocketMQ 、RabbitMQ
追求可靠性:RabbitMQ、RocketMQ
追求吞吐能力:RocketMQ、Kafka
追求消息低延迟:RabbitMQ、Kafka
这篇文章学习的是RabbitMQ,一般最求可靠性并且对吞吐量没那么大的业务,这个就行了
一般来说对可靠性不高的业务使用kafaka,比如日志啥的。
4.RabbitMQ
4.1安装,采取虚拟机docker安装
docker run \
-e RABBITMQ_DEFAULT_USER=zs \
-e RABBITMQ_DEFAULT_PASS=1234 \
--name mq \
--hostname mq1 \
-p 15672:15672 \
-p 5672:5672 \
-d \
rabbitmq:3-management
两个-e在配置用户和密码,第一个-p15672是访问RabbitMQ的图形化UI页面,我们只需要登录ip地址:这个就可以访问,可以在这个UI页面里创建交换机队列用户啥的,也可以不使用图形化页面,第二个-p才是RabbitMQ的服务器地址。
然后直接访问
进入后介绍一下页面,首先我们得了解RabbitMQ的架构

RabbitMQ中的一些角色:
- publisher:生产者
- consumer:消费者
- exchange:交换机,负责消息路由(就是相当于消息驿站吧,负责分配消息往哪里发)
- queue:队列,存储消息
- virtualHost:虚拟主机,隔离不同租户的exchange、queue、消息的隔离(因为不同用户用这个服务肯定得隔离,所以需要指定虚拟主机)





4.2常见消息模型:基本消息队列和工作消息队列没有交换机,只有生产者,消费者和队列,前者只是一对一,后者可以一对多,后面三个消息队列有交换机这几种区别在之后的讲解中会具体讲到。

4.3基本消息队列入门
这里先展示以下最原始的用法,以便我们理解结构,不需要记忆,因为spring已经集合了api:
publisher实现
思路:
- 建立连接
- 创建Channel
- 声明队列
- 发送消息
- 关闭连接和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.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();
}
}
consumer实现
代码思路:
- 建立连接
- 创建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.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 + "】");
}
});
System.out.println("等待接收消息。。。。");
}
}
5.SpringAMQP
AMOP是协议,SpringAMQP是基于RabbitMQ封装的一套模板,并且还利用SpringBoot对其实现了自动装配,使用起来非常方便。
在spring中要使用这个,需要引入依赖
java
<!--AMQP依赖,包含RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
然后生产者和消费者的yam文件中都需要配置信息
java
spring:
rabbitmq:
host: 192.168.150.101 # 主机名
port: 5672 # 端口
virtual-host: / # 虚拟主机
username: itcast # 用户名
password: 123321 # 密码
既然要使用队列,那么就得创建一个队列吧,创建队列主要有三种方法,第一种就是最直接的UI页面,第二种就是在配置类中配置(Queue)比如:
java
@Configuration
public class FanoutConfig {
/**
* 声明队列
*/
@Bean
public Queue fanoutQueue1(){
return new Queue("fanout.queue1");
}
}
第三种就是在消费者上面直接利用注解声明,经常用来后三种使用,一起绑定交换机。这种后边再说。
5.1基本消息队列:案例利用SpringAMOP是西安基本消息队列,发送一个hello,并且被消费者拿到。
生产者只需要注入,然后调用con方法即可,需要传入队列名称和消息。消息的话RabbitMQ支持Object,也就是所有类型,后面还要讲。
java
@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);
}
}
消费者把要干的事写在一个类里面,采用RabbitListener注解表面监听的队列是什么(这种形势要求队列要存在),然后你放进去什么类型,参数就是什么类型。
java
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueueMessage(String msg) throws InterruptedException {
System.out.println("spring 消费者接收到消息:【" + msg + "】");
}
}
5.2工作消息队列
简单来说就是**让多个消费者绑定到一个队列,共同消费队列中的消息,**这种模型和前一种一样都不能共同消费一种消息,它们都是一个消费者消费完就没了。
实现的话其实一样,再写一个方法监听同一个队列即可,但是有这样一个问题:生产者发布的消息他们是如何抢的,如何分配,这里就有一个机制:消息预取机制,就是消息来了排布在queue里面,然后queue会把消息平均分配给这些消息队列,你一个他一个,如果这个机制没有上限,那么就会拖垮吞吐量,比如100条消息,A一秒消费80个,B一秒消费20个,但这样可以使一秒钟就处理完吗?不行,因为这个机制没有上限,开始会给A50个,B50个,然后A需要0.几s,但是B需要3.多s,这样是不行的,并没有考虑到消费者的处理能力。这样显然是有问题的。
在spring中有一个简单的配置,可以解决这个问题。我们修改consumer服务的application.yml文件,添加配置:设置这个预取上限就是1,即只给你们平均分配一个,处理完再过来。
java
spring:
rabbitmq:
listener:
simple:
prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息
5.3发布订阅模式:剩下三种,其实字面意思很好理解,多的交换机就是实现这个的,所谓订阅不就是去订阅一本杂志一样,杂志发东西我就收东西,那比如说我订阅了地理杂志,生产者发布了各种各样的消息,我订阅了地理,我就只会收到地理方面的消息,这就是交换机做了提前过滤,就是这个意思。

三种模式大致区别是:
- Fanout:广播,将消息交给所有绑定到交换机的队列
- Direct:定向,把消息交给符合指定routing key 的队列
- Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列
在spring实现了这三种路由:

值得注意的是交换机是和队列绑定的,不是和消费者绑定的。那如何创建并且绑定队列呢,其实也有三种方法和创建队列是一样的,UI,配置类或者第三种(一会讲)
配置类方法:在配置类里面创建交换机队列,然后进行绑定。
java
@Configuration
public class FanoutConfig {
/**
* 声明交换机
* @return Fanout类型交换机
*/
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("itcast.fanout");
}
/**
* 第1个队列
*/
@Bean
public Queue fanoutQueue1(){
return new Queue("fanout.queue1");
}
/**
* 绑定队列和交换机
*/
@Bean
public Binding bindingQueue1(Queue fanoutQueue1, FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
}
/**
* 第2个队列
*/
@Bean
public Queue fanoutQueue2(){
return new Queue("fanout.queue2");
}
/**
* 绑定队列和交换机
*/
@Bean
public Binding bindingQueue2(Queue fanoutQueue2, FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
}
}
交换机的作用是什么?
- 接收publisher发送的消息
- 将消息按照规则路由到与之绑定的队列
- 不能缓存消息,路由失败,消息丢失
- 不同交换机有不同的发送规则
声明队列、交换机、绑定关系的Bean是什么?
- Queue
- ***Exchange
- Binding
5.3.1Fanout路由:广播路由,很显然就是字面意思,广播就是雨露均沾

实现:首先绑定交换机和队列
然后发布消息:记得先注入工具,然后发送的时候发送给交换机即可,第二个参数先设置为空,后面其他两种路由会讲
java
@Test
public void testFanoutExchange() {
// 队列名称
String exchangeName = "itcast.fanout";
// 消息
String message = "hello, everyone!";
rabbitTemplate.convertAndSend(exchangeName, "", message);
}
然后就消费呗,消费的话就得监听队列而不是交换机了
java
@RabbitListener(queues = "fanout.queue1")
public void listenFanoutQueue1(String msg) {
System.out.println("消费者1接收到Fanout消息:【" + msg + "】");
}
@RabbitListener(queues = "fanout.queue2")
public void listenFanoutQueue2(String msg) {
System.out.println("消费者2接收到Fanout消息:【" + msg + "】");
}
有没有发现使用配置类配置交换机,队列,进行绑定很繁琐,其实第三种方法是最简便的:
直接在消费者的方法上声明
java@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.out.println("消费者接收到direct.queue2的消息:【" + msg + "】"); }还是那个注解,然后里面是一个巨大的参数bindings(其实前面配置类里这个不就是绑定的bean嘛),@QueueBinding()参数很多,我们可以使用ctrl+p提示,首先第一个参数顾名思义就是要绑定的队列,第二个就是要绑定的交换机其中name就是名字,后面那个type就是交换机的类型,刚才讲的Fanout算一种,后面那两种也是类型。然后那个key主要是后两种需要使用的参数,是用来指定这个队列的RoutingKey(路由key)是什么,然后生产者发送的时候是不是原来我们有一个地方是空的,现在只需要传入这个参数,
java@Test public void testFanoutExchange() { // 队列名称 String exchangeName = "itcast.fanout"; // 消息 String message = "hello, everyone!"; rabbitTemplate.convertAndSend(exchangeName, "red", message); }交换机就会自动去找是这个RoutingKey(red)的队列给它发送,注意这种方式会直接新建队列,交换机。
到这里其实种模式我们都可以实现了,无非就是先声明队列或者交换机,然后绑定,然后生产者注入工具类,发送消息,指定交换机,路由key,消息。然后消费者去绑定消息队列,然后消费即可。
其实这就是后两种路由模式的特点:有key
5.3.2Direct路由:定向路由也是见名知意,定向也就是RoutingKey定向。
在Fanout模式中,一条消息,会被所有订阅的队列都消费。但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。
模型特点:
- 队列与交换机的绑定,不能是任意绑定了,而是要指定一个
RoutingKey(路由key) - 消息的发送方在 向 Exchange发送消息时,也必须指定消息的
RoutingKey。 - Exchange不再把消息交给每一个绑定的队列,而是根据消息的
Routing Key进行判断,只有队列的Routingkey与消息的Routing key完全一致,才会接收到消息.
具体实现我也不写了,懒得写了,通过第三种方式直接指定即可,看一个例子:第一个队列的key是red和blue,第二个是red和yellow
java
@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.out.println("消费者接收到direct.queue2的消息:【" + msg + "】");
}
其实我们发现如果让所有队列的key的包含同一个RoutingKey,那完全可以实现广播路由的情景。
5.3.3Topic:话题路由
Topic与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key 的时候使用通配符!
Routingkey 一般都是有一个或多个单词组成,多个单词之间以"."分割,例如: item.insert
通配符规则:
#:匹配一个或多个词
*:匹配不多不少恰好1个词
注意是词,每个词以"."分割
举例:比如A队列只想绑定关于中国的消息,那么就是chain.#,B队列只想绑定篮球的消息,那么就是#.basketball。
写一个消费者例子吧:
java
@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.out.println("消费者接收到topic.queue2的消息:【" + msg + "】");
}
两个队列绑定同一个交换机,路由key不一样,然后比如生产者发布
java
rabbitTemplate.convertAndSend(exchangeName, "china.news", message);
那么匹配到的队列就不一样。
6.消息转换器
之前那个最基本的代码发送消息的参数是字节码对象,也就是说我们可以发送任何类型的消息,Spring会把你发送的消息序列化为字节发送给MQ,接收消息的时候,只要参数类型一样,还会把字节反序列化为对应的Java对象。
但是默认情况下Spring采用的序列化方式是JDK序列化。众所周知,JDK序列化存在下列问题:
- 数据体积过大
- 有安全漏洞(很容易注入)
- 可读性差
显然,JDK序列化方式并不合适。我们希望消息体的体积更小、可读性更高,因此可以使用JSON方式来做序列化和反序列化。而使用json可读性和体积大家都知道很好,安全性也很好。使用JSON序列化器操作如下:
在publisher和consumer两个服务中都要引入依赖(直接写在父工程中即可):
java
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.9.10</version>
</dependency>
配置消息转换器:在配置类中添加一个Bean即可(当然可以在启动类里面配置):
java
@Bean
public MessageConverter jsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}
7.完成黑马点评异步秒杀业务。使用最基本消极队列就行了。
8.RabbitMQ可以实现多种队列,比如延时,死信,优先级队列,之后说