MQ消息队列

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:话题路由

TopicDirect相比,都是可以根据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可以实现多种队列,比如延时,死信,优先级队列,之后说

相关推荐
Hx_Ma161 小时前
mybatis练习2
java·数据库·mybatis
毕设源码-邱学长2 小时前
【开题答辩全过程】以 基于Web的小型宾馆客房管理系统为例,包含答辩的问题和答案
java
Zhu_S W2 小时前
EasyExcel:让Excel操作变得简单优雅
java·前端
wjs20242 小时前
Maven 项目模板
开发语言
爱学习的小可爱卢2 小时前
JavaSE基础-Java字符串转整数与拼接实战指南
java·开发语言
Felven2 小时前
C. Yet Another Card Deck
c语言·开发语言
星辰_mya2 小时前
Kafka Producer 发送慢 → TPS 骤降 90%
java·数据库·kafka
「QT(C++)开发工程师」2 小时前
【Qt Creator 15.0.1 安装指南】
开发语言·qt
网小鱼的学习笔记2 小时前
leetcode283移动零元素
java·开发语言·算法