【Gulimall+】消息队列 - MQ:可靠性抵达回调确认+利用死信实现延迟队列

1 功能

1、异步处理
消息发送的时间取决于业务执行的最长的时间
2、应用解耦
原本是需要订单系统直接调用库存系统
只需要将请求发送给消息队列,其他的就不需要去处理了,节省了处理业务逻辑的时间
3、流量消峰
某一时刻如果请求特别的大,那就先把它放入消息队列,从而达到流量消峰的作用

2 概述

大多应用中,可通过消息服务中间件来提升系统异步通信,扩展解耦能力
消息服务中两个重要概念:
    消息代理(message broker) 和 目的地(destination)
当消息发送者发送消息后,将由消息代理接管,消息代理保证消息传递到指定目的地
消息队列主要有两种形式的目的地
    队列(Queue):点对点消息通信(point - to - point)
    主题(topic):发布(publish)/订阅(subscribe)消息通信
点对点式:
    消息发送者发送消息,消息代理将其放入一个队列中,消息接收者从队列中获取消息内容,消息读取后被移出队列
    消息只有唯一的发送者和接受者,单并不是说只能有一个接收者
发布订阅式:
    发送者(发布者)发到消息到主题,多个接收者(订阅者)监听(订阅)这个主题,那么就会在消息到达时同时收到消息
JMS(Java Message Service) Java消息服务:
    基于JVM消息代理的规范,ActiveMQ、HornetMQ是JMS的实现
AMQP(Advanced Message Queuing Protocol)
    高级消息队列协议,也是一个消息代理的规范,兼容JMS
    RabbitMQ是AMQP的实现
Spring 支持
    spring - jms提供了对JMS的支持
    spring - rabbit提供了对AMQP的支持
    需要ConnectionFactory的实现来连接消息代理
    提供 JmsTemplate、RabbitTemplate 来发送消息
    @JmsListener(JMS)、@RabbitListener(AMQP)注解在方法上监听消息代理发布的消息
    @EnableJms、@EnableRabbit开启支持
Spring Boot 自动配置
    JmsAutoConfiguration
    RabbitAutoConfiguration
市面上的MQ产品
    ActiveMQ、RabbitMQ、RocketMQ,kafka

3 RabbitMQ

RabbitMQ简介

RabbitMQ是一由erlang开发的AMQP(Advanved Message Queue Protocol)的开源实现

核心概念
【Gulimall+】消息队列 - MQ:可靠性抵达回调确认+利用死信实现延迟队列_第1张图片

Message

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

Publisher

消息的生产者,也是一个像交换器发布消息的客户端应用程序

Exchange

交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列

Exchange有4种类型:direct(默认)、fanout、topic,和heades,不同类型的 Exchange 转发消息的策略有所区别

Queue

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

Binding

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

Connection

网路连接,比如一个TCP连接

Channel

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

Consumer

消息的消费者,表示一个消息队列中取得消息的客户端应用程序

Virtual Host

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

Broker

表示消息队列服务器实体

RabbitMQ配置

Docker 安装
docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 25672:25672 -p 15671:15671 -p 15672:15672 rabbitmq:management

 # 自动启动
docker update rabbitmq --restart=always

端口说明

4369, 25672 (Erlang发现&集群端口)
5672, 5671 (AMQP端口)
15672 (web管理后台端口)
61613, 61614 (STOMP协议端口)
1883, 8883 (MQTT协议端口)
运行机制

1、 AMQP 中的消息路由
AMQP 中消息的路由过程和 Java 开发者熟悉的 JMS 存在一些差别,AMQP中增加了 Exchange 和 Binding 的角色 生产者把消息发布到 Exchange 上,消息最终到达队列并被消费者接收,而 Binding 决定交换器的消息应该发送给那个队列

2、Exchange 类型
Exchange 分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、tanout、topic、headers header匹配AMQP消息的 header 而不是路由键,headers 交换器和 direct 交换器完全一致,但性能差能多,目前几乎用不到了,所以直接看另外三种类型

RabbitMQ 整合

1、引入 Spring-boot-starter-amqp:RabbitAutoConfig就会自动生效


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

2、application.properties配置

spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.virtual-host=/

#开启发送端确认
spring.rabbitmq.publisher-confirms=true

#开启发送端消息抵达队列确认
spring.rabbitmq.publisher-returns=true
# 只要抵达队列,以异步发送优先回调这个returnConfirm
spring.rabbitmq.template.mandatory=true

#手动ack消息
spring.rabbitmq.listener.simple.acknowledge-mode=manual

3、注解使用
监听消息:使用@RabbitListener,必须有@EnableRabbit
@RabbitListener:类+方法上(监听哪些队列即可)
@RabbitHandler:标在方法上(重载区分不同的消息

4、测试RabbitMQ
AmqpAdmin:管理组件,利用AmqpAdmin创建Exchange,Queue,Binding
RabbitTemplate:消息发送处理组件
PS:测试这儿就不粘贴了,直接用配置类处理更有条理些

RabbitMQ消息确认机制 - 可靠到达

官方link

保证消息不丢失,可靠抵达,可以使用事务消息,性能下降250倍,为此引入确认机制
publisher confirmCallback 确认模式
publisher returnCallback 未投递到 queue 退回
consumer ack 机制

在这里插入图片描述

1、可靠抵达 - ConfirmCallback
在创建 connectionFactory 的时候设置 PublisherConfirms(true) 选项,开启 confirmcallback。

spring.rabbitmq.publisher-confirms=true

CorrelationData 用来表示当前消息唯一性
消息只要被 broker 接收到就会执行 confirmCallback,如果 cluster 模式,需要所有 broker 接收到才会调用 confirmCallback被 broker 接收到只能表示 message 已经到达服务器,并不能保证消息一定会被投递到目标 queue 里,所以需要用到接下来的 returnCallback
2、可靠抵达 - ReturnCallback

spring.rabbitmq.publisher-retuns=true
spring.rabbitmq.template.mandatory=true

confirm 模式只能保证消息到达 broker,不能保证消息准确投递到目标 queue 里。在有些模式业务场景下,我们需要保证消息一定要投递到目标 queue 里,此时就需要用到 return 退回模式

这样如果未能投递到目标 queue 里将调用 returnCallback,可以记录下详细到投递数据,定期的巡检或者自动纠错都需要这些数据

3、可靠抵达 - Ack 消息确认机制

消费者获取到消息,成功处理,可以回复Ack给Broker
    basic.ack 用于肯定确认:broker 将移除此消息
    basic.nack 用于否定确认:可以指定 beoker 是否丢弃此消息,可以批量
    basic.reject用于否定确认,同上,但不能批量
默认,消息被消费者收到,就会从broker的queue中移除
消费者收到消息,默认自动ack,但是如果无法确定此消息是否被处理完成,或者成功处理,我们可以开启手动ack模式
    消息处理成功,ack(),接受下一条消息,此消息broker就会移除
    消息处理失败,nack()/reject() 重新发送给其他人进行处理,或者容错处理后ack
    消息一直没有调用ack/nack方法,brocker认为此消息正在被处理,不会投递给别人,此时客户端断开,消息不会被broker移除,会投递给别人

4、配置 RabbitTemplate设置回调
com/atguigu/gulimall/order/config/MyRabbitConfig.java

@Slf4j
@Configuration
public class MyRabbitConfig {
//    @Autowired
    RabbitTemplate rabbitTemplate;

    @Primary
    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory){
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        this.rabbitTemplate = rabbitTemplate;
        rabbitTemplate.setMessageConverter(messageConverter());
        initRabbitTemplate();
        return rabbitTemplate;
    }

    /**
     * 使用json序列化机制,进行消息转换
     *
     * @return
     */
    @Bean
    public MessageConverter messageConverter() {
        Jackson2JsonMessageConverter jsonMessageConverter = new Jackson2JsonMessageConverter();
        return jsonMessageConverter;
    }

    /**
     * 1.设置确认回调: ConfirmCallback
     * 先在配置文件中开启 publisher-confirms: true
     *
     * @PostConstruct: MyRabbitConfig对象创建完成以后 执行这个方法
     * 

* 2.消息抵达队列的确认回调 *   开启发送端消息抵达队列确认 * publisher-returns: true * 只要抵达队列,以异步优先回调我们这个 returnconfirm * template: * mandatory: true * 3.消费端确认(保证每一个消息被正确消费才可以broker删除消息) * 1.默认是自动确认的 只要消息接收到 服务端就会移除这个消息 *

* 如何签收: * 签收: channel.basicAck(deliveryTag, false); * 拒签: channel.basicNack(deliveryTag, false,true); * 配置文件中一定要加上这个配置 * listener: * simple: * acknowledge-mode: manual */ // @PostConstruct public void initRabbitTemplate() { /** * 设置确认回调 * correlationData: 消息的唯一id * ack: 消息是否成功收到 * cause:失败的原因 */ rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> log.info("\n收到消息: " + correlationData + "\tack: " + ack + "\tcause: " + cause)); /** * 设置消息抵达队列回调:可以很明确的知道那些消息失败了 * * message: 投递失败的消息详细信息 * replyCode: 回复的状态码 * replyText: 回复的文本内容 * exchange: 当时这个发送给那个交换机 * routerKey: 当时这个消息用那个路由键 */ //这个失败才会回调 rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routerKey) -> log.error("Fail Message [" + message + "]" + "\treplyCode: " + replyCode + "\treplyText:" + replyText + "\texchange:" + exchange + "\trouterKey:" + routerKey)); } }

RabbitMQ 延时队列(实现定时任务)

场景:
比如未付款的订单,超过一定时间后,系统自动取消订单并释放占有物品
解决:rabbitmq的消息TTL和死信Exchange结合
【Gulimall+】消息队列 - MQ:可靠性抵达回调确认+利用死信实现延迟队列_第2张图片

消息的TTL(Time To Live)

消息的TTL 就是消息的存活时间,
RabbitMQ可以对队列还有消息分别设置TTL
    对队列设置就是没有消费者连着的保持时间,也可以对每一个消息单独的设置,超过了这个时间我们可以认为这个消息他死了,称之为死信
    如果队列设置了,消息也设置了,那么会取小,所以一个消息如果被路由到不同的队列中,这个消息死亡时间有可能不一样的(不同队列设置),这里讲的是单个TTL 因为他是实现延时任务的关键,可以通过设置消息的 expiration 字段或者 x-message-ttl 来设置时间两者是一样的效果

Dead Letter Exchange(DLX)

一个消息在满足如下条件下,会进死信路由,记住这里是路由不是队列,一个路由可以对应很多队列,(什么是死信)
    一个消息被Consumer拒收了,并且reject方法的参数里requeue是false。也就是说不被再次放在队列里,被其他消费者使用。(basic.reject/ basic.nack)
    requeue= false上面的消息的TTL到了,消息过期了。
    队列的长度限制满了。排在前面的消息会被丢弃或者扔到死信路由上
Dead Letter Exchange其实就是一种普通的exchange, 和创建其他exchange没有两样。只是在某一个设置 Dead Letter Exchange的队列中有消息过期了自动触发消息的转发,发送到Dead Letter Exchange中去。
我们既可以控制消息在一段时间后变成死信, 又可以控制变成死信的消息被路由到某一个指定的交换机, 结合C者,其实就可以实现一个延时队列

消息队列构建

流程图地址link
【Gulimall+】消息队列 - MQ:可靠性抵达回调确认+利用死信实现延迟队列_第3张图片
在这里插入图片描述
com/atguigu/gulimall/order/config/MyMQConfig.java

@Configuration
public class MyMQConfig {
//    @RabbitListener(queues = "order.release.order.queue")  //有监听才会创建该配置中的Binding, Queue, Exchange
//    public void listener(OrderEntity entity, Channel channel, Message message) throws IOException {
//
//        System.out.println("收到过期的订单信息,准备关闭订单" + entity.getOrderSn());
//        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
//    }

    /**
     * 容器中的 Binding, Queue, Exchange 都会自动创建(RabbitMQ没有的情况
     * @return
     */

    @Bean
    public Queue orderDelayQUeue() {
//        public Queue(String name, boolean durable, boolean exclusive, boolean autoDelete, @Nullable Map arguments)
        // 特殊参数(死信队列
        Map<String,Object> map = new HashMap<>();
        map.put("x-dead-letter-exchange", "order-event-exchange"); // 死信交换机
        map.put("x-dead-letter-routing-key","order.release.order"); // 死信路由键
        // 消息过期时间
        map.put("x-message-ttl",60000);

        Queue queue = new Queue("order.delay.queue", true, false, false,map);
        return queue;
    }

    @Bean
    public Queue orderReleaseOrderQueue() {
        Queue queue = new Queue("order.release.order.queue", true, false, false);
        return queue;
    }

    @Bean
    public Exchange orderEventExchange() {
        //	public TopicExchange( 
        //	String name, 交换机的名字
        //	boolean durable, 是否持久
        //	boolean autoDelete, 是否自动删除
        //	Map arguments)
        TopicExchange exchange = new TopicExchange("order-event-exchange", true, false);
        return exchange;
    }

    @Bean
    public Binding ordrCreateOrderBingding() {
        // public Binding(String destination, 目的地
        // DestinationType destinationType, 目的地类型
        // String exchange,交换机  //Topic交换机支持绑定模糊的路由键
        // String routingKey,//路由键
        //@Nullable Map arguments
        Binding binding = new Binding("order.delay.queue", Binding.DestinationType.QUEUE, "order-event-exchange", "order.create.order", null);
        return binding;
    }

    @Bean
    public Binding orderReleaseOrderBinding() {
        Binding binding = new Binding("order.release.order.queue", Binding.DestinationType.QUEUE, "order-event-exchange", "order.release.order", null);
        return binding;
    }

    /**
     * 订单释放直接和库存释放绑定
     * @return
     */
    @Bean
    public Binding orderReleaseOther() { //给库存释放队列
        Binding binding = new Binding("stock.release.stock.queue", Binding.DestinationType.QUEUE, "order-event-exchange", "order.release.other", null);
        return binding;
    }

    @Bean
    public Queue orderSeckillOrderQueue() {
        Queue queue = new Queue("order.seckill.order.queue", true, false, false);
        return queue;
    }

    @Bean
    public Binding ordrSeckillOrderBingding() {
        // public Binding(String destination, 目的地
        // DestinationType destinationType, 目的地类型
        // String exchange,交换机
        // String routingKey,//路由键
        //@Nullable Map arguments
        Binding binding = new Binding("order.seckill.order.queue", Binding.DestinationType.QUEUE, "order-event-exchange", "order.seckill.order", null);
        return binding;
    }
}

订单创建成功会基于路由键借助交换机切换给order.delay.queue发消息
com/atguigu/gulimall/order/service/impl/OrderServiceImpl.java

rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder());

这里就仅仅给出订单释放消息的监听
com/atguigu/gulimall/order/listener/OrderCloseListener.java

@RabbitListener(queues = "order.release.order.queue")
@Service
public class OrderCloseListener {
    @Autowired
    OrderService orderService;

    @RabbitHandler
    public void listener(OrderEntity entity, Channel channel, Message message) throws IOException {

        System.out.println("收到过期的订单信息,准备关闭订单" + entity.getOrderSn());

        try {
            orderService.closeOrder(entity); //这个关闭订单方法里还会通知提前解锁库存 rabbitTemplate.convertAndSend("order-event-exchange","order.release.other",orderTo);
               
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true); //放回消息队列
        }
    }
}

去RabbitMQ控制台看看效果,link 账号密码默认都是guest
【Gulimall+】消息队列 - MQ:可靠性抵达回调确认+利用死信实现延迟队列_第4张图片

如何保证消息可靠性 - 消息丢失 & 消息重复

1、消息丢失

消息发送出去,由于网络问题没有抵达服务器
    做好容错方法(try-Catch) ,发送消息可能会网络失败,失败后要有重试机制,可记录到数据库,采用定期扫描重发的方式
    做好日志记录,每个消息状态是否都被服务器收到都应该记录
    做好定期重发,如果消息没有发送成功,定期去数据库扫描未成功的消息进行重发
消息抵达Broker, Broker要将消息写入磁盘(持久化)才算成功。此时Broker尚未持久化完成,宕机。
    publisher也必须加入确认回调机制,确认成功的消息,修改数据库消息状态。
自动ACK的状态下。消费者收到消息,但没来得及消息然后宕机
    一定开启手动ACK,消费成功才移除,失败或者没来得及处理就noAck并重新入队

2、消息重复

消息消费成功,事务已经提交,ack时,机器宕机。导致没有ack成功,Broker的消息重新由unack变为ready,并发送给其他消费者
消息消费失败,由于重试机制,自动又将消息发送出去
成功消费,ack时宕机,消息由unack变为ready, Broker又重新发送
    消费者的业务消费接口应该设计为幂等性的。比如扣库存有工作单的状态标志
    使用防重表(redis/mysq|) ,发送消息每一 个都有业务的唯一 标识,处理过就不用处理
    rabbitMQ的每一个消息都有redelivered字段, 可以获取是否是被重新投递过来的,而不是第一次投递过来的

3、消息积压

消费者积压
消费者消费能力不足积压
发送者发送流量太大
    上线更多消费者,进行正常消费
    上线专门的队列消费服务,将消息先批量取出来,记录数据库,离线慢慢处理

你可能感兴趣的:(#,Gulimall,消息队列RabbitMQ,延迟队列,可靠性抵达)