SpringCloud(一)【SpringCloud基础】
SpringCloud(二)【单体服务及远程调用】
SpringCloud(三)【Eureka注册中心】
SpringCloud(四)【Ribbon负载均衡】
SpringCloud(五)【Nacos注册中心】
SpringCloud(六)【Feign】
SpringCloud(七)【Gateway网关】
SpringCloud(八)【Docker】
MQ服务异步通信
一、同步调用和异步调用概念
- 同步调用
- 优点
- 时效性强,可以立即得到返回结果
- 缺点
- 耦合度高(修改业务都需要修改原来的代码)
- 性能和吞吐能力下降(响应时间=每次调用的时间之和)
- 有额外的资源消耗(在等待过程中无法释放请求占用的资源,高并发情况下会浪费系统资源)
- 有级联失败问题(任何一个服务提供者出现问题则整体出现问题)
- 异步通信
- 优点
- 耦合度低
- 吞吐量提高
- 故障隔离(一个服务提供者失败,不影响其他)
- 流量削峰(上图)
- 缺点
- 依赖于Broker的可靠性、安全性、吞吐能力
- 架构复杂了,业务没有明显的流程线,不好追踪管理
- MQ
MQ (MessageQueue),中文是消息队列,字面来看就是存放消息的队列。也就是事件驱动架构中的Broker。
RabbitMQ | ActiveMQ | RocketMQ | Kafka | |
---|---|---|---|---|
公司/社区 | Rabbit | Apache | 阿里 | Apache |
开发语言 | Erlang | Java | Java | Scala&Java |
协议支持 | AMQP,XMPP,SMTP,STOMP | OpenWire,STOMP,REST,XMPP,AMQP | 自定义协议 | 自定义协议 |
可用性 | 高 | 一般 | 高 | 高 |
单机吞吐率 | 一般 | 差 | 高 | 非常高 |
消息延迟 | 微秒级 | 毫秒级 | 毫秒级 | 毫秒以内 |
消息可靠性 | 高 | 一般 | 高 | 一般 |
二、RabbitMQ
(一)安装
- 拉取RabbitMQ镜像
docker pull rabbitmq:3-management
- 运行MQ容器
docker run \
-e RABBITMQ_DEFAULT_USER=min \
-e RABBITMQ_DEFAULT_PASS=123456 \
--name mq \
--hostname mq1 \
-p 15672:15672 \
-p 5672:5672 \
-d \
rabbitmq:3-management
- 进入管理端口查看
http://192.168.21.158:15672/
(二)RabbitMQ基础知识
- RabbitMQ的结构和概念
- 概念
- channel:操作MQ的工具
- exchange:路由消息到队列中
- queue:缓存消息
- virtual host:虚拟主机,是对queue、exchange等资源的逻辑分组
- 常见消息模型
RabiitMQ官网查看
- 基本消息队列(BasicQueue)
- 工作消息队列(WorkQueue)
- 发布订阅(Publish、Subscribe),又根据交换机类型不同分为三种:
- Fanout Exchange:广播
- Direct Exchange:路由
- Topic Exchange:主题
- Fanout Exchange:广播
- 简单实现测试
使用单元测试模拟实现
- 生产者
@Test
public void testSendMsg2SimpleQueue() throws IOException, TimeoutException {
// 1 建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1 设置连接参数:主机名、端口号、vhost、用户名、密码
factory.setHost("192.168.21.158");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("min");
factory.setPassword("123456");
// 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 msg = "hello, mq!";
channel.basicPublish("",queueName,null,msg.getBytes(StandardCharsets.UTF_8));
System.out.println("消息发送成功:"+msg);
//5 关闭通道和连接
channel.close();
connection.close();
}
- 消费者
@Test
public void testSendMsg2SimpleQueue() throws IOException, TimeoutException {
// 1 建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1 设置连接参数:主机名、端口号、vhost、用户名、密码
factory.setHost("192.168.21.158");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("min");
factory.setPassword("123456");
// 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 msg = new String(body);
System.out.println("收到消息:"+msg);
}
});
System.out.println("等待接收消息...");
}
(三)SpringAMQP
- 什么是AMQP
Advanced Message Queuing Protocol,是用于在应用程序之间传递业务消息的开放标准。该协议与语言和平台无关,更符合微服务中独立性的要求。 - 什么是SpringAMQP
SpringAMQP官网
Spring AMQP是基于AMQP协议定义的一套API规范,提供了模板来发送和接收消息。包含两部分,其中spring-amqp是基础抽象,spring-rabbit是底层的默认实现。
(1)hellomq入门案例实现
- 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
- 生产者和消费者配置文件
spring:
rabbitmq:
host: 192.168.21.158
port: 5672
username: min
password: 123456
virtual-host: /
- 生产者发送消息
@Resource
private RabbitTemplate rabbitTemplate;
@Test
public void testSendMsg2SimpleQueue(){
String queueName = "simple_queue";
String msg = "spring使用方式1";
rabbitTemplate.convertAndSend(queueName,msg);
}
- 消费者消费消息
@Component
public class SpringRabbit {
@RabbitListener(queues = "simple_queue")
public void listenSimpleQueue(String msg){
System.out.println("消费者接收消息:"+msg);
}
}
(2)Work Queue工作队列模型
Work queue,工作队列,可以提高消息处理速度,避免队列消息堆积。
- 前置条件
假设生产者每秒生产50个消息,消费者1效率较高,消费者2效率较低,但是两个消费者加起来可以消费完生产者生产的消息。 - 生产者
@Test
public void testSendMsg2WorkQueue() throws InterruptedException {
String queueName = "work_queue";
String msg = "springAmqp的work_queue:";
for (int i = 0; i < 50; i++) {
rabbitTemplate.convertAndSend(queueName,msg + i);
Thread.sleep(20);
}
}
- 消费者
@RabbitListener(queues = "work_queue")
public void listenWorkQueue1(String msg) throws InterruptedException {
System.out.println("消费者1接收消息:"+msg + LocalDateTime.now());
Thread.sleep(20);
}
@RabbitListener(queues = "work_queue")
public void listenWorkQueue2(String msg) throws InterruptedException {
System.err.println("消费者2接收消息:"+msg + LocalDateTime.now());
Thread.sleep(200);
}
运行消费者消费服务,观察消费情况。
预期:消费者1在一秒内处理完多的消息,消费者2在一秒内处理的消息不如消费者2多。
实际结果:消费者1和消费者2的消息处理数量一样多,且消费者2没有在一秒内处理完成。
原因:存在消息预取机制。
- 消息预取
配置消费者预取数量
spring:
rabbitmq:
host: 192.168.21.158
port: 5672
username: min
password: 123456
virtual-host: /
listener:
direct:
prefetch: 1 # 消费者预取数量
重启消费者服务后,达成预期目标。
(3)发布-订阅模型
发布订阅模式与之前案例的区别就是允许将同一消息发送给多个消费者。实现方式是加入了exchange(交换机)。常见exchange类型包括:
- Fanout:广播
- Direct:路由
- Topic:话题
注意:exchange负责消息路由,而不是存储,路由失败则消息丢失
FanoutExchange
- 消息队列绑定
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 RabbitMqConfig {
@Bean
public FanoutExchange fanoutExchange() {
return new FanoutExchange("min.fanout");
}
@Bean
public Queue fanoutQueue1() {
return new Queue("fanout_queue1", false);
}
@Bean
public Binding fanoutBinding1(FanoutExchange fanoutExchange, Queue fanoutQueue1){
return BindingBuilder.bind(fanoutQueue1)
.to(fanoutExchange);
}
@Bean
public Queue fanoutQueue2() {
return new Queue("fanout_queue2", false);
}
@Bean
public Binding fanoutBinding2(FanoutExchange fanoutExchange, Queue fanoutQueue2){
return BindingBuilder.bind(fanoutQueue2)
.to(fanoutExchange);
}
}
- 生产者
@Test
public void testSendMsg2FanoutQueue() {
String exchangeName = "min.fanout";
String msg = "fanout的使用";
rabbitTemplate.convertAndSend(exchangeName,"",msg);
}
- 消费者
// fanout
@RabbitListener(queues = "fanout_queue1")
public void listenFanoutQueue1(String msg){
System.err.println("fanout_queue1接收消息:"+msg);
}
// fanout
@RabbitListener(queues = "fanout_queue2")
public void listenFanoutQueue2(String msg) {
System.err.println("fanout_queue2接收消息:"+msg);
}
DirectExchange
- 生产者
@Test
public void testSendMsg2DirectQueue() {
String exchangeName = "min.direct";
String key = "red";
String msg = "direct的使用:"+key;
rabbitTemplate.convertAndSend(exchangeName,key,msg);
}
- 消费者
// direct
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue1"),
exchange = @Exchange(name = "min.direct",type = ExchangeTypes.DIRECT),
key = {"red","blue"}
))
public void listenDirectQueue1(String msg) {
System.err.println("direct.queue1接收消息:"+msg);
}
// direct
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue2"),
exchange = @Exchange(name = "min.direct",type = ExchangeTypes.DIRECT),
key = {"red","yellow"}
))
public void listenDirectQueue2(String msg) {
System.err.println("direct.queue2接收消息:"+msg);
}
生产者依次生产key为:blue、yellow、red
TopicExchange
TopicExchange与DirectExchange类似,区别在于routingKey必须是多个单词的列表,并且以 .
分割。 Queue与Exchange指定BindingKey时可以使用通配符:
- #:代指0个或多个单词
- *:代指一个单词
- 生产者
// topic
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue1"),
exchange = @Exchange(name = "min.topic",type = ExchangeTypes.TOPIC),
key = {"china.#"}
))
public void listenTopicQueue1(String msg) {
System.err.println("topic.queue1接收消息:"+msg);
}
// topic
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue2"),
exchange = @Exchange(name = "min.topic",type = ExchangeTypes.TOPIC),
key = {"#.news"}
))
public void listenTopicQueue2(String msg) {
System.err.println("topic.queue2接收消息:"+msg);
}
- 消费者
@Test
public void testSendMsg2TopicQueue() {
String exchangeName = "min.topic";
String key = "china.news";
String msg = "topic的使用:"+key;
rabbitTemplate.convertAndSend(exchangeName,key,msg);
}
依次发送key为:china.news、china.weather
(四)消息转换器
Spring的对消息对象的处理是org.springframework.amqp.support.converter.MessageConverter
来处理的。而默认实现是SimpleMessageConverter
,基于JDK的ObjectOutputStream
完成序列化。 如果要修改只需要定义一个MessageConverter
类型的Bean即可。推荐用JSON方式序列化,步骤如下:
- 引入依赖
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
- 声明一个队列并且配置
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
@Bean
public Queue objectQueue() {
return new Queue("object_queue");
}
- 生产者
@Test
public void testSendMsg2ObjectQueue() {
String queueName = "object_queue";
Map<String,String> res = new HashMap<>();
res.put("name","name1");
res.put("age","1");
rabbitTemplate.convertAndSend(queueName,res);
}
- 消费者
@RabbitListener(queues = "object_queue")
public void listenObjectQueue2(Map<String,String> msg) {
System.err.println("object_queue接收消息:"+msg);
}