1.1.同步与异步通讯
微服务之间的两种通讯方式
- 同步:需要实时反馈
- 异步:不需要实时反馈
1.1.1.同步通讯
Feign调用就是同步通讯
优点:
- 时效性较强,可以立刻得到结果
缺点:
- 耦合度高
- 性能与吞吐能力下降
- 需要更多的资源消耗
- 级联问题失败
1.1.2.异步通讯
类似:网上购物,下单成功(publisher),发送一个事件(event),事件上带着订单(id),
物流服务作为事件的订阅者(consumer),当订阅支付成功的事件后,监听到事件完成。
为了解除publisher与consumer之间的耦合,而不是两者直接通讯,MQ中间件(broker)就出现了。
事件发布者只需要将事件发布到中间件,不关心谁来订阅。订阅者在中间件订阅事件,不关心谁发布的事件。
MQ就类似一个小型服务器,所有的服务要接收和发送信息都要通过这个服务器,并且所有的服务必须要遵循这个服务器的协议,让服务之间的通讯变得标准可控。
优点:
- 吞吐量提升:无需等待订阅者完成,响应跟迅速
- 耦合度低:每个服务都可以灵活的切换
- 流量切锋:不管发布事件的流量波动多大,都由Broker接收,订阅者可以按照自己的速度去处理事件
缺点:
- 架构复杂,业务没有明显的流程线,不好管理
- 重度依赖中间件的性能
1.2.技术对比
MQ,中文是消息队列(MessageQueue),字面来看就是存放消息的队列。也就是事件驱动架构中的Broker。
比较常见的MQ实现:
-
ActiveMQ
-
RabbitMQ
-
RocketMQ
-
Kafka
几种常见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
2.2.消息类型
3.1.Basic Queue 简单队列模型
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-amqp -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>2.7.4</version>
</dependency>
spring:
rabbitmq:
host: # 主机名
port: # 端口
virtual-host: / # 虚拟主机
username: # 用户名
password: # 密码
publisher:
@Test
public void testSimpleQueue() {
// 队列名称
String queueName = "simple.queue";
// 消息
String message = "hello";
// 发送消息
rabbitTemplate.convertAndSend(queueName, message);
}
consumer:
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueueMessage(String msg) throws InterruptedException {
System.out.println("消费者接收到消息:【" + msg + "】");
}
}
3.2.WorkQueue
Work queues,也被称为(Task queues),任务模型。简单来说就是让多个消费者绑定到一个队列,共同消费队列中的消息。
publisher:
/**
* workQueue
* 向队列中不停发送消息,模拟消息堆积。
*/
@Test
public void testWorkQueue() throws InterruptedException {
// 队列名称
String queueName = "simple.queue";
// 消息
String message = "hello";
for (int i = 0; i < 50; i++) {
// 发送消息
rabbitTemplate.convertAndSend(queueName, message + i);
Thread.sleep(20);
}
}
consumer:
@RabbitListener(queues = "simple.queue")
public void listenWorkQueue1(String msg) throws InterruptedException {
System.out.println("message01: " + message);
Thread.sleep(20);
}
@RabbitListener(queues = "simple.queue")
public void listenWorkQueue2(String msg) throws InterruptedException {
System.err.println("message02.........: " + message);
Thread.sleep(200);
}
结果:
可以看到消费者1很快完成了自己的25条消息。消费者2却在缓慢的处理自己的25条消息。
消息是平均分配给每个消费者,并没有考虑到消费者的处理能力。这样显然是有问题的。
3.2.1.解决方案
spring:
rabbitmq:
listener:
simple:
prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息
结果:
实现了,按劳分配的基本原则。
3.3.Fanout
在Fanout模式下,消息发送流程是这样的:
-
1) 可以有多个队列
-
2) 每个队列都要绑定到Exchange(交换机)
-
3) 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定
-
4) 交换机把消息发送给绑定过的所有队列
-
5) 订阅队列的消费者都能拿到消息
@Configuration
public class FanoutConfig {
/**
* 声明交换机
* @return Fanout类型交换机
*/
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("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:
@Test
public void testFanoutExchange() {
// 队列名称
String exchangeName = "fanout";
// 消息
String message = "hello";
rabbitTemplate.convertAndSend(exchangeName, "", message);
}
consumer:
@RabbitListener(queues = "fanout.queue1")
public void listenFanoutQueue1(String msg) {
System.out.println("message01: " + message);
}
@RabbitListener(queues = "fanout.queue2")
public void listenFanoutQueue2(String msg) {
System.err.println("message02.........: " + message);
}
结果:
3.4.Direct
在Direct模型下:
-
队列与交换机的绑定,不能是任意绑定了,而是要指定一个
RoutingKey
(路由key) -
消息的发送方在 向 Exchange发送消息时,也必须指定消息的
RoutingKey
。 -
Exchange不再把消息交给每一个绑定的队列,而是根据消息的
Routing Key
进行判断,只有队列的Routingkey
与消息的Routing key
完全一致,才会接收到消息
publisher:
@Test
public void testDirectExchange() {
String exchange = "lonely.directExchange";
String message = "hello";
rabbitTemplate.convertAndSend(exchange, "blue", message);
}
consumer:
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue01"),
exchange = @Exchange(name = "lonely.directExchange"),
key = {"blue", "red"}
))
public void listenDirectQueue01(String message) {
System.out.println("message01.........: " + message);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue02"),
exchange = @Exchange(name = "lonely.directExchange"),
key = {"yellow", "red"}
))
public void listenDirectQueue02(String message) {
System.err.println("message02.........: " + message);
}
结果:
3.5.Topic
Topic
类型的Exchange
与Direct
相比,都是可以根据RoutingKey
把消息路由到不同的队列。只不过Topic
类型Exchange
可以让队列在绑定Routing key
的时候使用通配符!
Routingkey
一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
通配符规则:
#
:匹配一个或多个词
*
:匹配不多不少恰好1个词
举例:
#
:能够匹配item.spu.insert
或者 item.spu
item.*
:只能匹配item.spu
publisher:
@Test
public void testTopicExchange() {
String exchange = "lonely.topicExchange";
String message = "hello,lonely";
rabbitTemplate.convertAndSend(exchange, "china.news", message);
System.out.println("---------------------------------------------------------");
rabbitTemplate.convertAndSend(exchange, "china", message);
System.out.println("---------------------------------------------------------");
rabbitTemplate.convertAndSend(exchange, "news", message);
}
consumer:
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue01"),
exchange = @Exchange(name = "lonely.topicExchange", type = ExchangeTypes.TOPIC),
key = "china.#"
))
public void listenTopicQueue01(String message) {
System.out.println("message01.........: " + message);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue02"),
exchange = @Exchange(name = "lonely.topicExchange", type = ExchangeTypes.TOPIC),
key = "#.news"
))
public void listenTopicQueue02(String message) {
System.err.println("message02.........: " + message);
}
结果:
3.6.消息转换器
Spring会把你发送的消息序列化为字节发送给MQ,接收消息的时候,还会把字节反序列化为Java对象。
默认情况下Spring采用的序列化方式是JDK序列化。众所周知,JDK序列化存在下列问题:
-
数据体积过大
-
有安全漏洞
-
可读性差
publisher:
@Test
public void testSendObject(){
Map<String,Object> msg = new HashMap<>();
msg.put("name","lonely");
msg.put("age",20);
rabbitTemplate.convertAndSend("object.queue",msg);
}
结果:
3.6.1.配置JSON转换器
publisher与consumer都需要配置
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.9.10</version>
</dependency>
@SpringBootApplication
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
}
结果: