rabbitMQ todo

MQ Message Queue(消息队列)

队列:

  • 队头存数据,队尾取数据,先进先出的队列
  • 队头存数据,对头取数据,先进后出的队列(栈)
  • 两头都可以存/取数据,双端队列

MQ的应用场景

MQ的主要功能

  1. 异步通信(解耦合)
  2. 队列

异步处理

下图第一个是正常调用流程,第二个是采用异步的方式,第三个则是使用mq的方式。

给消息中间件写消息的耗时是非常短的,数据库插入数据持久化可能需要50ms,写消息类似于操作redis一样,只会花费5ms。

rabbitMQ todo_第1张图片

应用解耦

订单系统下订单完成之后,会调用库存系统来减库存,假设库存系统经常升级的话,接口的参数也可能发生变化,这时候订单系统也不得不跟着升级。

使用消息中间件,订单完成之后,我们将用户及所购买商品等等信息存入消息队列中,就无需关心库存系统了。
库存系统实时订阅队列中的内容,只要有内容库存系统就会收到消息,最终库存系统来实现业务逻辑。

rabbitMQ todo_第2张图片
以下示例解耦合主要体现在:

下单业务如果不通过mq的话,它需要等待支付业务、订单物流业务、第三方商家业务等
如果此时有任何一个业务出现问题,那么受牵连的将会是所有业务,
而通过mq发送关键信息到其他业务,此时任何一个业务出现问题都不会关联到其他业务。
rabbitMQ todo_第3张图片
rabbitMQ todo_第4张图片

流量控制(流量削峰)

例如秒杀业务,瞬间流量会非常大,瞬间百万个请求都要进来秒杀一个商品,就算我们前端服务器接收了所有请求,我们要执行业务代码,秒杀完要下订单等,整个流程会非常慢,后台一直阻塞,可能就会导致最终的资源耗尽,服务器宕机,

此时我们可以将大并发量的请求全部进来,先存储到消息队列中,直接响应,后台相关业务处理的服务订阅消息队列中的秒杀请求,然后根据服务器的能力进行消费和处理,则不会导致我们的服务器宕机。

rabbitMQ todo_第5张图片

mq概述

  • 消息代理:安装了消息中间件的服务器。负责发送消息和接收消息。

rabbitMQ todo_第6张图片

模式

只要是消息中间件一定有点对点式和发布订阅式。

  1. 点对点式
  • 消息发送者发送消息,消息代理将其放入一个队列中,消息接收者从队列中获取消息内容,消息读取后被移出队列

  • 消息只有唯一的发送者和接受者,但并不是说只能有一个接收者,消息最终只会交给一个人,谁先抢到就是谁的。

  1. 发布订阅式:
  • 发送者(发布者)发送消息到主题,多个接收者(订阅者)监听(订阅)这个主题,那么就会在消息到达时同时收到消息

消息队列协议

  1. JMS(Java Message Service)JAVA消息服务:
  • 基于JVM消息代理的规范。ActiveMQ、HornetMQ是JMS实现
  1. AMQP(Advanced Message Queuing Protocol)
  • 高级消息队列协议,也是一个消息代理的规范,兼容JMS
  • RabbitMQ是AMQP的实现

如果后端全部是用java来实现的,我们可以首选jms,多语言的话就使用AMQP。
rabbitMQ todo_第7张图片

spring支持

rabbitMQ todo_第8张图片

RabbitMQ介绍

RabbitMQ 是一个由 Erlang 语言(面向并发的编程语言)开发的 AMQP(高级消息队列协议) 的开源实现。

RabbitMQ 最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性、扩展 性、高可用性等方面表现不俗。

架构图

rabbitMQ todo_第9张图片

rabbitMQ todo_第10张图片

交换机中可以设置路由键,并且也可以绑定消息队列,
我们访问的时候只需要指定路由键,交换机就会根据路由键的规则,发送到绑定的消息队列中。

  1. 无论是生产者发消息还是消费者接收消息,它们都必须和rabbitmq建立连接,所有的收发数据都在连接里开辟信道来进行收发。(网络底层,可以不用理解)
  2. 消息,有头和体,头是对消息设置一些参数(固定),消息体是消息的真正内容。指定发给哪个交换机。
  3. 消息指定好路由键,消息首先来到rabbitmq服务器指定的虚拟主机里,由指定的交换机收到以后,根据路由键通过交换机和队列的绑定关系最终决定将消息发送到哪个队列中。
  4. 消费者监听队列,队列中的内容就会被实时拿到。

建立长连接的好处,一旦消费者出现了宕机或其他问题,导致连接中断了,rabbitmq就会实时感知有消费者下线,消息没办法派发了,就会再次存储起来,不造成大面积的消息丢失等。
如果队列不能感知消费者的话,把消息发出去之后认为发送成功了就会删除该消息,就会造成消息丢失。

核心概念

  • RabbitMQ Server
    也叫broker server(表示消息队列服务器实体),它是一种传输服务。 他的角色就是维护一条从Producer到Consumer的路线,保证数据能够按照指定的方式进行传输。

  • Producer(Publisher)
    消息生产者,也是一个向交换器发布消息的客户端应用程序。如图A、B、C,数据的发送方。消息生产者连接RabbitMQ服务器然后将消息投递到Exchange。

  • Consumer
    消息消费者,表示一个从消息队列中取得消息的客户端应用程序。如图1、2、3,数据的接收方。消息消费者订阅队列, RabbitMQ将Queue中的消息发送到消息消费者。

  • Message
    消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。

  • RoutingKey(路由键)
    生产者在将消息发送给Exchange的时候,一般会指定一个routing key, 来指定这个消息的路由规则,而这个routing key需要与Exchange Type及binding key联 合使用才能最终生效。在Exchange Type与binding key固定的情况下(在正常使用时一 般这些内容都是固定配置好的),我们的生产者就可以在发送消息给Exchange时,通过 指定routing key来决定消息流向哪里。RabbitMQ为routing key设定的长度限制为255 bytes。

  • Exchange(交换机)
    用来接收生产者发送的消息并将这些消息路由给服务器中的队列。
    生产者将消息发送到Exchange(交换器),由Exchange将消息路由到一个或多个Queue中(或者丢弃)。Exchange并不存储消息。RabbitMQ中的Exchange有 direct(默认)、fanout、topic、headers四种类型,同类型的Exchange转发消息的策略有所区别。

  • Queue
    消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。

  • Binding
    绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。Exchange 和Queue的绑定可以是多对多的关系。

  • Connection
    网络连接,比如一个TCP连接。Producer和Consumer都是通过TCP连接到RabbitMQ Server 的。以后我们可以看到,程序的起始处就是建立这个TCP连接。(长连接,一直保持连接)

  • Channel(信道)
    信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内的虚拟连接,AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接。

  • VirtualHost(虚拟主机)
    权限控制的基本单位,一个VirtualHost里面有若干Exchange和MessageQueue,以及指定被哪些user使用。例如商品空间,订单空间,保证消息队列的安全性,互不影响。

    虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个 vhost 本质上就是一个 mini 版的 RabbitMQ 服务器,拥有自己的队列、交换器、绑定和权限机制。vhost 是 AMQP 概念的基础,必须在连接时指定,RabbitMQ 默认的 vhost 是 / ,虚拟主机是按路径来分的。

    例如不同的语言,不同的服务都需要使用rabbitMq,然而配置需要隔离,就需要用到虚拟主机。

具体特点

  1. 可靠性(Reliability) RabbitMQ 使用一些机制来保证可靠性,如持久化、传输确认、发布确认。

    消息确认(需要开启):MQ将消息发送给消费者,MQ并没有将消息删除,MQ把这条消息锁定,消息消费者用完这条消息之后,消费者会给MQ一个确认,当MQ收到返回之后,解除锁定并删除。

  2. 灵活的路由(Flexible Routing)
    在消息进入队列之前,通过 Exchange(交换机) 来路由消息的。对于典型的路由功能,RabbitMQ 已经提供了一些内置的 Exchange 来实现。针对更复杂的路由功能,可以将多个 Exchange 绑定在一起,也通过插件机制实现自己的 Exchange 。

  3. 消息集群(Clustering)
    多个 RabbitMQ 服务器可以组成一个集群,形成一个逻辑 Broker 。

  4. 高可用(Highly Available Queues)
    队列可以在集群中的机器上进行镜像,使得在部分节点出问题的情况下队列仍然可用。

  5. 跟踪机制(Tracing)
    如果消息异常,RabbitMQ 提供了消息跟踪机制,使用者可以找出发生了什么。

  6. 插件机制(Plugin System)
    RabbitMQ 提供了许多插件,来从多方面进行扩展,也可以编写自己的插件。

docker 安装 rabbitmq

// https://www.rabbitmq.com/networking.html
// 4369, 25672 (Erlang发现&集群端口)
// 5672, 5671 (AMQP端口)
// 15672 (web管理后台端口)
// 61613, 61614 (STOMP协议端口)
// 1883, 8883 (MQTT协议端口)
docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 25672:25672 -p 15671:15671 -p 15672:15672 --restart=always rabbitmq:management

web界面介绍

  • 端口号 15672
  • 账密 guest

rabbitMQ todo_第11张图片

菜单栏介绍

  1. overview(概览)

    • refreshed,默认每5s刷新一次
    • Virtual host,默认访问的是所有的虚拟主机
    • Totals
      • queued messages,消息队列中的消息
      • currently idle,当前的一些空闲信息
      • message rates,消息的收发速率
      • global counts,全局属性的监控
        • 统计共有多少个连接,通道,交换机,队列,消费者
  • nodes,rabbitmq的节点信息,内存,磁盘占用等信息
  • churn statistics,静态统计的图表信息,每秒多少个连接,通道,队列
  • ports and contexts,端口和上下文
    • amqp(高级消息队列协议),5672,客户端使用高级消息队列协议来收发消息就使用这个端口。
    • context中可以指定访问的ip
  • export definitions,import definitions,新装的rabbitmq需要迁移配置时,从旧的rabbitmq使用export导出配置文件,新的rabbitmq使用import导入配置文件
  1. connections,有多少个客户端与其建立连接,一个客户端只建立一个连接
  2. channel,信道
  3. exchanges,交换机,默认有7个,消息进入/发出的速率
  4. queues,队列
  5. admin
    • 默认用户是guest
    • virtual hosts,虚拟主机,默认是 /,不同的虚拟主机是由路径来区分的
      • message中的ready,unacked,total代表当前就绪状态的消息,没有回复状态的消息,共有多少个消息
      • network,客户端信息
      • message rates,publish发布速率,deliver/get,派发速率
      • limits,虚拟主机的连接限制

创建交换机

rabbitMQ todo_第12张图片

  • durability,是否持久化,durable是持久化,transient临时,临时的话rabbitmq重启该交换机就没了
  • auto delete,是否自动删除,如果是的话交换机在没有绑定任何东西的情况下就会自动删除
  • internal,是否是内部的交换机,如果是内部的交换机,客户端就不能给该交换机发消息,是供我们内部转发路由使用的

rabbitmq的运行机制

交换机和队列的关系是多对多
rabbitMQ todo_第13张图片

exchange的类型

  • direct,直接
  • fanout,扇出
  • topic,主题(发布订阅)
  • headers,

direct和headers都是点对点的实现,fanout和topic都是发布订阅的实现,headers已被弃用。

  1. direct(路由键与队列名完全匹配)
    rabbitMQ todo_第14张图片

  2. fanout,广播模式(不处理路由键,简单的将队列绑定到交换机上)
    无论交换机的路由键是什么,消息都会被该交换机发送到绑定的所有队列中。

  3. topic,主题模式(实际上也是广播模式),(处理/区分路由键)

    只能匹配单词,不能匹配字母
    rabbitMQ todo_第15张图片

springboot整合

spring.rabbitmq.host=192.168.56.10
# 虚拟主机
spring.rabbitmq.virtual-host=/
# 生产端消息确认机制
# 老师的配置是这个,但我的版本比老师的高,这个源码中过时配置级别设置了Error,已经不能使用了
#spring.rabbitmq.publisher-confirms=true

# SIMPLE,
# CORRELATED,发布消息成功到交换器后会触发回调方法
# NONE; 禁用发布确认模式,是默认值
spring.rabbitmq.publisher-confirm-type=CORRELATED
# 开启发送端消息抵达队列的确认
spring.rabbitmq.publisher-returns=true
# 只要抵达队列以异步方式优先回调returnConfirm
spring.rabbitmq.template.mandatory=true

# 手动ack(acknowledge,确认收获)消息
spring.rabbitmq.listener.simple.acknowledge-mode=manual





<dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-amqpartifactId>
        dependency>


// 如果远程仓库获取不到的话,换下面的仓库地址
<mirror>
		<id>aliyunmavenid>
		<mirrorOf>*mirrorOf>
		<name>阿里云公共仓库name>
		<url>https://maven.aliyun.com/repository/publicurl>
	mirror>

启动类

package com.atlinxi.gulimall.order;

import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;


/**
 * 使用rabbitmq
 *
 * 1. 引入amqp依赖,RabbitAutoConfiguration就会自动生效
 * 2. 给容器中自动配置了
 *      RabbitTemplate,AmqpAdmin,CachingConnectionFactory,RabbitMessagingTemplate
 *
 *      所有属性都是 @ConfigurationProperties(
 *          prefix = "spring.rabbitmq"
 *          )
 *          public class RabbitProperties
 *
 * 3. 给配置文件中配置spring.rabbitmq.xxx
 * 4. @EnableRabbit,开启功能
 * 5. 监听消息,使用@RabbitListener,必须有@EnableRabbit
 *      @RabbitListener:类/方法上(监听哪些队列)
 *      @RabbitHandler:标在方法上(重载区分不同的消息)
 *          不同的消息实际上就是不同的实体类
 */
@EnableRabbit
@SpringBootApplication
public class GulimallOrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(GulimallOrderApplication.class, args);
    }

}

config

package com.atlinxi.gulimall.order.config;

import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;

@Configuration
public class MyRabbitConfig {


    @Autowired
    RabbitTemplate rabbitTemplate;

    /**
     * rabbitmq在发送消息时,如果消息是实体类的话,就会进行序列化,我们是看不懂的
     *
     * 指定消息转换器为Jackson,就会帮我们转换成json
     * @return
     */
    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }


    /**
     * 定制rabbitTemplate
     *
     * 1. rabbitmq服务器收到消息就回调
     *      1. spring.rabbitmq.publisher-confirm-type=CORRELATED
     *      2. 设置确认回调
     *
     * 2. 消息正确抵达队列进行回调
     *
     *      spring.rabbitmq.publisher-returns=true
     *      spring.rabbitmq.template.mandatory=true
     *
     *
     * 3. 消费端消息确认(保证每个消息被正确消费,此时才可以rabbitmq才可以删除这个消息)
     *      spring.rabbitmq.listener.simple.acknowledge-mode=manual 手动确认
     *      1. 默认是自动确认的,只要消息接收到,客户端会自动确认,服务端就会移除这个消息
     *          问题:
     *              我们收到很多消息,自动回复给服务器ack,只有一个消息处理成功,宕机了.发生消息丢失.
     *              手动确认.(处理一个消息,确认一个消息,没处理的不能被删除)
     *                  只要我们没有明确告诉mq,消息被消费,没有ack,消息一直就是unack状态.
     *                  即使consumer宕机,消息不会丢失,会重新变为ready状态,下一次有新的
     *                  consumer连接进来就发给它
     *
     *
     *      2. 如何确认消息
     *           channel.basicAck(deliveryTag,false);确认消息,业务逻辑完成就应该确认
     *           channel.basicNack(deliveryTag,false,true);拒绝消息,业务逻辑失败就拒绝
     *
     *
     *
     * @PostConstruct 对象创建完之后再来调用这个方法
     *      在这里就指的是MyRabbitConfig对象创建完成之后
     */
    @PostConstruct
    public void initRabbitTemplate(){
        // 设置消息抵达rabbitmq服务器确认回调
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {


            /**
             *
             * 1. 只要消息抵达broker ack就为true
             *
             * @param correlationData   当前消息的唯一关联数据(这个是消息的唯一id),消息发送成功为null
             * @param ack   消息是否成功收到
             * @param cause 失败的原因,否则为null
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {


                System.out.println("confirm...correlationData:" + correlationData);

                System.out.println("confirm ack:" + ack);

                System.out.println("confirm cause:" + cause);


                System.out.println();
            }
        });





        // 设置消息抵达队列的确认回调
        rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {

            /**
             *
             * 只要消息没有投递给指定的队列,就触发这个失败回调,否则不会触发
             *
             * @param returned  投递失败的相关信息
             *
             *     private final Message message;  投递失败的消息的详细信息
             *     private final int replyCode;     回复的状态码
             *     private final String replyText;  回复的文本内容
             *     private final String exchange;   交换机
             *     private final String routingKey; 路由键
             */
            @Override
            public void returnedMessage(ReturnedMessage returned) {

                System.out.println("=========消息抵达队列的确认回调==========");
                // ReturnedMessage [message=(Body:'"Hello World"' MessageProperties [headers={__TypeId__=java.lang.String},
                // contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT,
                // priority=0, deliveryTag=0]), replyCode=312, replyText=NO_ROUTE, exchange=hello-java-exchange, routingKey=hello22.java]
                System.out.println(returned);
            }
        });
    }
}


虚拟主机和消息发送

package com.atlinxi.gulimall.order;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.core.AmqpAdmin;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.UUID;

@SpringBootTest
@Slf4j
class GulimallOrderApplicationTests {


    @Autowired
    AmqpAdmin amqpAdmin;

    @Autowired
    RabbitTemplate rabbitTemplate;


    /**
     * 1. 如何创建exchange[hello-java-exchange],queue,binding
     *      1) 使用AmqpAdmin进行创建
     * 2. 如何收发消息
     */
    @Test
    void creatExchange() {

        // amqpAdmin

        // 创建交换机
        // public DirectExchange(String name, boolean durable,
        // boolean autoDelete, Map arguments)
        DirectExchange directExchange = new DirectExchange("hello-java-exchange",true,false);
        amqpAdmin.declareExchange(directExchange);
        log.info("exchange创建成功{}","hello-java-exchange");
    }



    @Test
    void creatQueue() {

        //public Queue(String name, boolean durable, boolean exclusive,
        // boolean autoDelete, @Nullable Map arguments)

        // exclusive 排它,只允许一个connection连接,
        Queue queue = new Queue("hello-java-queue",true,false,false);
        DirectExchange directExchange = new DirectExchange("hello-java-exchange",true,false);
        amqpAdmin.declareQueue(queue);
        log.info("queue创建成功{}","hello-java-queue");
    }










    @Test
    void creatBinding() {

        /**
         *     public Binding(
         *          String destination, 目的地(队列)
         *          Binding.DestinationType destinationType,目的地类型
         *          String exchange, 交换机
         *          String routingKey, 路由键
         *          @Nullable Map arguments,自定义参数
         *          ) {
         *
         *     将exchange和destination进行绑定,使用routingKey作为路由键
         */

        Binding binding = new Binding("hello-java-queue",Binding.DestinationType.QUEUE,
                "hello-java-exchange","hello.java",null);

        Queue queue = new Queue("hello-java-queue",true,false,false);
        DirectExchange directExchange = new DirectExchange("hello-java-exchange",true,false);
        amqpAdmin.declareBinding(binding);
        log.info("binding创建成功{}","hello-java-binding");
    }









    @Test
    void sendMessage() {

        // 1. 发送消息

        // 如果发送的消息是个对象,我们会使用序列化机制,将对象写出去。对象必须实现Serializable接口
        // 发送的对象类型的消息,可以是一个json
//        rabbitTemplate.convertAndSend("hello-java-exchange","hello.java","Hello World");


        /**
         * 测试消息抵达队列的确认回调
         * CorrelationData,设置唯一id,消息失败我们好确认
         *
         * 在发送消息到rabbitmq之外,还可以保存到mysql中,每一个唯一id对应子消息,如果rabbitmq收到消息就在数据库中给个提示,
         *      同时就会知道哪些消息没有收到,我们可以定时扫描一下数据库,把没有送达的消息再重新投递一次
         */
        rabbitTemplate.convertAndSend("hello-java-exchange","hello22.java","Hello World",new CorrelationData(UUID.randomUUID().toString()));

        log.info("消息发送完成{}","hello-java-exchange","Hello World");
    }





}

监听消息

package com.atlinxi.gulimall.order.service.impl;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.Map;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.atlinxi.common.utils.PageUtils;
import com.atlinxi.common.utils.Query;

import com.atlinxi.gulimall.order.dao.OrderItemDao;
import com.atlinxi.gulimall.order.entity.OrderItemEntity;
import com.atlinxi.gulimall.order.service.OrderItemService;


@Service("orderItemService")
@RabbitListener(queues = {"hello-java-queue"})
public class OrderItemServiceImpl extends ServiceImpl<OrderItemDao, OrderItemEntity> implements OrderItemService {

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<OrderItemEntity> page = this.page(
                new Query<OrderItemEntity>().getPage(params),
                new QueryWrapper<OrderItemEntity>()
        );

        return new PageUtils(page);
    }


    /**
     * queues,声明需要监听的所有队列
     *
     * 消息类型是 class org.springframework.amqp.core.Message
     *
     * 接收消息的参数可以写以下类型
     * 1. Message message,原生消息详细信息。消息头和消息体,需要进行json转换
     * 2. 第二个参数 T<发送的消息的类型>,就写java的实体类就行
     *      发送的消息是哪个实体类,接收消息就写哪个实体类即可
     *      spring帮我们进行json的转换
     * 3. Channel channel:当前传输数据的通道
     *
     *
     *
     * Queue:可以很多人都来监听。只要收到消息,队列就会删除消息,而且只能有一个收到此消息
     *
     * 场景:
     *      1)订单服务启动多个,同一个消息只能有一个客户端收到
     *      2)只有一个消息完全处理完(该监听函数业务逻辑全部运行完成),
     *          才可以接收到下一个消息
     *
     */
//    @RabbitListener(queues = {"hello-java-queue"})
//    @RabbitHandler
    public void receiveMessage(/**Object message*/Message message, Channel channel) {
//    public void receiveMessage(JavaEntity javaEntity){

        // 消息的内容
        byte[] body = message.getBody();

        // 消息头属性信息
        MessageProperties messageProperties = message.getMessageProperties();

        // 当前channel内按顺序自增的
        long deliveryTag = messageProperties.getDeliveryTag();

        // 确认消息,非批量模式
        try {
            // 确认消息
            channel.basicAck(deliveryTag,false);

            // 两个都是拒收消息,上面的可以批量,下面的不能批量
            // channel.basicNack();
            // channel.basicReject();

            /**
             * param1,唯一标识
             * param2,是否批量拒收,之前的消息都被拒收
             * param3,消息是否重新入队
             */
            channel.basicNack(deliveryTag,false,true);
        }catch (IOException e){
            // 网络中断
        }


        System.out.println("接收到消息,内容:" + message + "==>类型:" + message.getClass());
    }

}

消息确认机制(可靠投递,发送端确认)

todo应该和下面那个结合着看,有时间再说吧

在分布式系统中,有非常多的微服务连接消息队列服务器,监听消息,但是可能会由于网络抖动原因,网络闪断,包括服务器的宕机,mq的宕机等各种问题,都可能导致消息的丢失。

rabbitMQ todo_第16张图片

  1. 生产者发送消息到rabbitmq服务器
  2. exchange将消息路由到queue
  3. 消费者消费消息

以上三个步骤都有可能会失败,2可能是由于路由的错误,或者在将消息存入队列的时候有客户端对队列进行了删除操作等。

每一步骤都对应着不同的回调函数或机制。

confirmCallback(生产端消息抵达服务器确认机制)

rabbitMQ todo_第17张图片

returnCallback (生产端消息抵达队列确认机制)

rabbitMQ todo_第18张图片

ack(消费端确认消息)

-----------------------------------

下面的五种模式感觉和上面的交换机的模式可能是一个意思,有时间再说吧

rabbitmq的5种模式

直接模式(Direct)和工作模式(workQueues)

直接模式:我们需要将消息发给唯一一个节点时使用这种模式,这是最简单的一种形式。一个生产者对应一个消费者

工作模式:在直接模式的基础上,一个生产者,多个消费者
分配有两种,一种是平均分配(默认),例如生产者生产消息1234,消费者1消费消息13,消费者2消费24,
另一种是按劳分配,见下面代码的注释

消费者

package com.itheima.rabbit.consumer.listener;

import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
//设置Rabbit的监听器,这里监听器监听的是itcast队列
@RabbitListener(queues = "itheima")
public class MyListener {

    //消息处理注解,会获取消息,把消息内容注入到方法的参数中
    @RabbitHandler
    public void handlerMessage(String message) {
        System.out.println("itheima消费者2消息是:" + message);
    }
}

生产者

@Test
    public void test() {
        //第一个参数设置路由键,消息发送到对应名字的队列中
        //第二个参数设置发送的消息内容
        for (int i = 0; i < 10; i++) {
            rabbitTemplate.convertAndSend("abc", "直接模式消息发送" + i);
        }
        //以下方式成为工作模式
        //在直接模式基础上,增加队列监听的消费者,
        // 默认情况下,消息消费是轮询的方式(平均分配的工作模式)
        // 另一种方式,可以根据消费者消费消息的速度来分配消息数量,能力强的处理更多的消息,能力弱的处理更少的消息
        //成为能者多劳模式,参考课后资料
    }

分列模式(fanout)

交换机绑定队列

  1. 这种模式需要提前将Exchange与Queue进行绑定一个Exchange可以绑定多个 Queue,一个Queue可以同多个Exchange进行绑定(这个操作可以在rabbitmq界面操作)。
  2. 如果接受到消息的Exchange没有与任何Queue绑定,则消息会被抛弃。

交换机不能持久化消息,队列可以

工作模式是把消息平均分配给每个消费者,分列模式是把每一个消息发送给每一个消费者。

生产者

    //1. 把队列和交换机进行绑定
    //2. 生产者把消息发给交换机
    //3. 交换机根据绑定的信息,把消息转发给指定的队列,交换机本身保存消息
    @Test
    public void test2() {
        //第一个参数是交换机名字,消息发给哪个交换机
        //第二个参数是路由键,本例不做使用,填写""空串
        //第三个参数是消息
        for (int i = 0; i < 10; i++) {
            rabbitTemplate.convertAndSend("chuanzhi", "", "测试分列模式消息");
        }
    }


路由模式(routing)

在分列模式的前提下,指定路由键(其实就是再增加一次筛选)

下图error就是路由键,并不是错误,警告的字面意思
rabbitMQ todo_第19张图片
rabbitMQ todo_第20张图片

生产者

//路由模式
    //Routing
    //需要指定路由键,无论是路由模式,分列模式,主题模式进行切换,或者改变使用情况的时候,都不需要修改消费者
    // 修改以上那些完全可以用rabbitmq的官方界面来完成
    @Test
    public void test3() {
        //第一个参数是交换机名字,消息发给哪个交换机
        //路由模式需要指定交换机的类型为direct,需要指定路由键绑定队列
        //第二个参数是路由键
        //第三个参数是消息
        //for (int i = 0; i < 10; i++) {
        rabbitTemplate.convertAndSend("chuanzhi", "abc", "测试路由模式消息abc");
        //}
    }

主题模式

路由键,*代表一个词,#代表多个词,以点为分隔,判断几个词

配置这个路由键,是在管理界面配置的
rabbitMQ todo_第21张图片

生产者

//主题模式
    //Topics
    //对路由键使用了*(代表不多不少一个词)或者#(代表零个一个或多个)
    @Test
    public void test5() {
        //第一个参数是交换机名字,消息发给哪个交换机
        //主题模式需要指定交换机的类型为topic,需要指定路由键绑定队列
        //第二个参数是路由键
        //第三个参数是消息
        rabbitTemplate.convertAndSend("chuanzhi", "itcast", "主题模式消息123");

    }

rabbitmq消息发送一些问题的解决

如果消息发送失败如何处理

如果消息发送失败如何处理

  1. 重发 重发3次 6次

  2. 消息重发失败怎么处理

    1. RabbitMQ 确认机制

      默认是自动应答, 手动应答 需要消费者提供应答,如果正确应答,消息消费成功 没有正确应答,消息失败

    2. 死信队列

      就是把所有发送失败的消息都发送到一个队列中,这个队列就被称为死信队列

      rabbitmq的管理界面在创建队列的时候,可以添加参数为x-dead-letter-exchange绑定死信交换机,并设置x-message-ttl超时时间,如果消息在规定时间内消息没有被成功消费,则发送到死信交换机中,进而进入到死信队列

      设置死信队列要根据具体的业务场景去应用,一般应用在当正常业务处理时出现异常时,将消息拒绝则会进入到死信队列中,有助于统计异常数据并做后续处理

      延迟队列存储的是延迟消息,延迟消息指的是,当消息被发发布出去之后,并不立即投递给消费者,而是在指定时间之后投递。如:在订单系统中,订单有30秒的付款时间,在订单超时之后在投递给消费者处理超时订单。
      rabbitMq没有直接支持延迟队列,可以通过死信队列实现

rabbitmq崩了怎么办

  1. 使用备用方案 重要的业务场景设置备用方案

    例如两个服务,a服务调用b服务,因为业务场景的原因,直接调用耦合性太高,所以我们通过MQ来处理,当mq的服务崩了,我们启用备用服务,a服务直接调用b服务,这也会增加开发成本。

  2. 使用MySQL(数据库)存放消息信息
    在发送消息的同时,存放消息的关键信息到mysql中,消息是否消费可以通过状态码来判断,当消息发送失败时,可以设置定时任务,来执行业务逻辑。

消息确认

消息确认(生产者推送消息成功,消费者接收消息成功)。

生产者的消息确认机制

package com.anhangxunxi.consumer.config;

import com.anhangxunxi.common.entity.RabbitMQPayment;
import com.anhangxunxi.common.util.ObjectUtil;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScans;
import org.springframework.context.annotation.Configuration;

/**
 * dzh
 *	rabbitmq的配置类
 *	1.交换机和队列及它俩的绑定
 * 2. 生产者的消息确认机制
 * 		消息的回调,即消息确认(生产者推送消息成功,消费者接收消息成功(这个在ConsumerMonitor中))
 * 	如果出现找不到交换机或者队列的情况就会触发消息的回调,可以把结果打印在日志中
 */
@Configuration
@EnableRabbit
// dzh,开启注解扫描
@ComponentScan
public class RabbitMQConfig {

	// dzh,这三个都是起名
	public final static String PAYMENT_QUEUE_NAME = "payment_queue";

	public final static String PAYMENT_EXCHANGE_NAME = "payment_exchange";

	public final static String PAYMENT_BINDING_NAME = "payment_binding";

	@Value("${info.rabbitmq.host}")
	private String host;

	@Value("${info.rabbitmq.port}")
	private Integer port;

	@Value("${info.rabbitmq.username}")
	private String userName;

	@Value("${info.rabbitmq.password}")
	private String password;

	/**
	 * 创建支付功能队列
	 * @return
	 */
	@Bean
	public Queue paymentQueue() {

		// dzh,durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
		// dzh,exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
		// dzh,autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除
		//dzh,一般设置一下队列的持久化就好,其余两个就是默认false

		return new Queue(PAYMENT_QUEUE_NAME);
	}

	/**
	 * 创建topic交换器
	 * @return
	 */
	@Bean
	public TopicExchange paymentExchange() {
		return new TopicExchange(PAYMENT_EXCHANGE_NAME);
	}

	/**
	 * dzh,绑定  将队列和交换机绑定, 并设置用于匹配键PAYMENT_BINDING_NAME
	 * @return
	 */
	@Bean
	public Binding binding() {
		return BindingBuilder.bind(paymentQueue()).to(paymentExchange()).with(PAYMENT_BINDING_NAME);
	}

	@Bean
	public ConnectionFactory connectionFactory() {
		CachingConnectionFactory connectionFactory = new CachingConnectionFactory(host, port);
		connectionFactory.setUsername(userName);
		connectionFactory.setPassword(password);
		// dzh,确认消息已发送到交换机
		// 消息发送确认,到达exchange返回
		connectionFactory.setPublisherConfirms(true);
		// 启动消息失败返回,没有到达队列
		// dzh,确认消息已发送到队列(Queue)
		connectionFactory.setPublisherReturns(true);
		return connectionFactory;
	}

	@Bean
	public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
		SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
		factory.setConnectionFactory(connectionFactory);
		factory.setMessageConverter(new Jackson2JsonMessageConverter());
		// 手动确认消息
		factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
		return factory;
	}

	/**
	 * 配置相关的消息确认回调函数
	 * @param connectionFactory
	 * @return
	 */
	@Bean
	public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
		RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
		// 开启mandatory returncallback才会生效
		rabbitTemplate.setMandatory(true);
		// 消息没有正确到达队列时触发回调,如果正确到达队列不执行。
		rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
			byte[] bytes = message.getBody();
			RabbitMQPayment rabbitMQPayment = (RabbitMQPayment) ObjectUtil.getObjectFromBytes(bytes);
			System.out.println("ackMQSender 发送消息被退回" + rabbitMQPayment.toString().toString());
		});
		// 只确认消息是否正确到达 Exchange 中
		rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
			if (!ack) {
				System.out.println("ackMQSender 消息发送失败!" + correlationData.getReturnedMessage().toString());
			}
			else {
				byte[] bytes = correlationData.getReturnedMessage().getBody();
				RabbitMQPayment rabbitMQPayment = (RabbitMQPayment) ObjectUtil.getObjectFromBytes(bytes);
				System.out.println("ackMQSender 消息发送成功 " + rabbitMQPayment.toString());
			}
		});
		return rabbitTemplate;
	}

}

消费者的消息确认机制

package com.aihangxunxi.consumer.monitor;

import com.aihangxunxi.common.entity.RabbitMQPayment;
import com.aihangxunxi.common.util.ObjectUtil;
import com.aihangxunxi.consumer.config.RabbitMQConfig;
import com.aihangxunxi.consumer.entity.PaymentMqErrorRecord;
import com.aihangxunxi.consumer.repository.PaymentMqErrorRecordMapper;
import com.aihangxunxi.consumer.service.PaymentedBusiness;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.io.IOException;
import java.time.LocalDateTime;

/**
 * dzh,消费者接收到消息的消息确认机制
 *
 * 和生产者的消息确认机制不同,因为消息接收本来就是在监听消息,符合条件的消息就会消费下来。
 所以,消息接收的确认机制主要存在三种模式:

 ①自动确认, 这也是默认的消息确认情况。  AcknowledgeMode.NONE
 RabbitMQ成功将消息发出(即将消息成功写入TCP Socket)中立即认为本次投递已经被正确处理,不管消费者端是否成功处理本次投递。
 所以这种情况如果消费端消费逻辑抛出异常,也就是消费端没有处理成功这条消息,那么就相当于丢失了消息。
 一般这种情况我们都是使用try catch捕捉异常后,打印日志用于追踪数据,这样找出对应数据再做后续处理。

 ② 根据情况确认, 这个不做介绍
 ③ 手动确认 , 这个比较关键,也是我们配置接收消息确认机制时,多数选择的模式。
 消费者收到消息后,手动调用basic.ack/basic.nack/basic.reject后,RabbitMQ收到这些消息后,才认为本次投递成功。
 basic.ack用于肯定确认
 basic.nack用于否定确认(注意:这是AMQP 0-9-1的RabbitMQ扩展)
 basic.reject用于否定确认,但与basic.nack相比有一个限制:一次只能拒绝单条消息

 消费者端以上的3个方法都表示消息已经被正确投递,但是basic.ack表示消息已经被正确处理。
 而basic.nack,basic.reject表示没有被正确处理:

 着重讲下reject,因为有时候一些场景是需要重新入列的。

 channel.basicReject(deliveryTag, true);  拒绝消费当前消息,如果第二参数传入true,就是将数据重新丢回队列里,那么下次还会消费这消息。设置false,就是告诉服务器,我已经知道这条消息数据了,因为一些原因拒绝它,而且服务器也把这个消息丢掉就行。 下次不想再消费这条消息了。

 使用拒绝后重新入列这个确认模式要谨慎,因为一般都是出现异常的时候,catch异常再拒绝入列,选择是否重入列。

 但是如果使用不当会导致一些每次都被你重入列的消息一直消费-入列-消费-入列这样循环,会导致消息积压。



 顺便也简单讲讲 nack,这个也是相当于设置不消费某条消息。

 channel.basicNack(deliveryTag, false, true);
 第一个参数依然是当前消息到的数据的唯一id;
 第二个参数是指是否针对多条消息;如果是true,也就是说一次性针对当前通道的消息的tagID小于当前这条消息的,都拒绝确认。
 第三个参数是指是否重新入列,也就是指不确认的消息是否重新丢回到队列里面去。

 同样使用不确认后重新入列这个确认模式要谨慎,因为这里也可能因为考虑不周出现消息一直被重新丢回去的情况,导致积压。
 */
@Component
public class ConsumerMonitor {

	@Autowired
	private PaymentedBusiness paymentedBusiness;

	@Resource
	private PaymentMqErrorRecordMapper paymentMqErrorRecordMapper;

	@RabbitListener(queues = RabbitMQConfig.PAYMENT_QUEUE_NAME)
	public void consumePaymentMessage(byte[] msg, Channel channel, Message message) {
		RabbitMQPayment rabbitMQPayment = null;
		try {
			// 将字节码转化为对象
			rabbitMQPayment = (RabbitMQPayment) ObjectUtil.getObjectFromBytes(msg);
			System.out.println("consume message:" + rabbitMQPayment.toString());
			String type = rabbitMQPayment.getType();
			if ("hotelReservation".equals(type)) {
				paymentedBusiness.hotelReservationPayment(rabbitMQPayment);
			}
			else if ("eCardReservation".equals(type)) {
				paymentedBusiness.cardBuyPayment(rabbitMQPayment);
			}
			else {
				paymentedBusiness.otherPayment(rabbitMQPayment);
			}
			// 告诉服务器收到这条消息 已经被我消费了 可以在队列删掉 这样以后就不会再发了 否则消息服务器以为这条消息没处理掉 重启应用后还会在发
			channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
			System.out.println("ACK_QUEUE_B 接受信息成功");
		}
		catch (Exception e) {
			e.printStackTrace();
			try {
				if (rabbitMQPayment != null) {
					PaymentMqErrorRecord paymentMqErrorRecord = new PaymentMqErrorRecord();
					BeanUtils.copyProperties(rabbitMQPayment, paymentMqErrorRecord);
					paymentMqErrorRecord.setConsumerType(rabbitMQPayment.getType());
					paymentMqErrorRecord.setErrorMessage("消息接收后处理业务异常");
					paymentMqErrorRecord.setCreateAt(LocalDateTime.now());
					paymentMqErrorRecordMapper.insert(paymentMqErrorRecord);
					// 消息重新回到队列
					// channel.basicNack(message.getMessageProperties().getDeliveryTag(),
					// false, true);
					// 告诉服务器收到这条消息 已经被我消费了 可以在队列删掉 这样以后就不会再发了 否则消息服务器以为这条消息没处理掉 重启应用后还会在发
				}
				channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
			}
			catch (IOException ex) {
				ex.printStackTrace();
			}
			System.out.println("ACK_QUEUE_B 接受信息异常");
		}
	}

}

部分知识引用自:
https://blog.csdn.net/qq_35387940/article/details/100514134

李老师软音软语对她们说:“不然我有诺贝尔文学奖全集?”这一幕晞晞正好。诺贝尔也正好。扮演好一个期待女儿的爱的父亲角色。一个偶尔泄露出灵魂的教书匠,一个流浪到人生的中年还等不到理解的语文老师角色。一整面墙的原典标榜他的学问,一面课本标榜孤独,一面小说等于灵魂。没有一定要上过他的课。没有一定要谁家的女儿。

房思琪的初恋乐园
林奕含

你可能感兴趣的:(中间件,rabbitmq,java,分布式)