消息队列MQ

目录

概述

消息队列目的地形式

RabbitMQ简介

核心概念

Docker安装RabbitMQ 

SpringBoot整合RabbitMQ

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

RabbitMQ消息可靠性

消息丢失

消息重复

消息积压

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

消息TTL(Time To Live)

Dead Letter Exchage(DLX)

RabbitMQ延时队列模拟定时关单&库存自动解锁 


概述

  • 提升系统异步通信、扩展解耦、流量控制。
  • 消息代理(message broker)、目的地(destination)。
  • 消息发送者发送消息后,将由消息代理接管,消息代理保证消息传递到指定目的地。

消息队列目的地形式

  • 队列(queue):点对点消息通信(point-to-point)。
  1. 消息发送者发送消息,消息代理将其放入一个队列中,消息接收者从队列中获取消息内容,消息读取后被移除队列。
  2. 消息只有唯一发送者,接收者可以有多个,但最终只有一个接收者获取到消息。
  • 主题(topic):发布(publish)/ 订阅(subscribe)消息通信。
  1. 发送者发送消息到主题,多个接收者订阅这个主题,消息到达时同时接收到消息。
JMS(Java Message Service) AMQP(Advanced Message Queuing Protocol)
定义

Java api 基于JVM消息代理的规范

高级消息队列协议,也是消息代理的规范,网络协议通信,兼容JMS
跨语言
跨平台
Model

提供两种消息模型:

  1. Peer-2-Peer
  2. Pub/Sub

提供五种消息模型:

  1. direct exchange
  2. fanout exchage
  3. topic exchange
  4. headers exchange
  5. system exchange

本质来讲,后四种和JMS的pub/sub模型没有太大差别,但是在路由机制上做了更详细的划分

支持消息类型

多种消息类型:

  • TextMessage
  • MapMessage
  • BytesMessage
  • StreamMessage
  • ObjectMessage
  • Message(只有消息和属性)

byte[]

实际应用有复杂的消息,可以将消息序列化后发送

综合评价 JMS定义了java api层面的标准,在java体系中,多个client均可以通过JMS进行交互,不需要应用修改代码,但是对其跨平台的支持差 AMQP定义wire-level层的协议标准,天然具有跨平台、跨语言特性
Spring支持
  • spring-jms
  • JmsTemplate 发送消息
  • 需要ConnectionFactory的实现来连接消息代理
  • @JmsListener 监听消息代理发布的消息
  • @EnbaleJms 开启支持
  • spring-rabbit
  • RabbitTemplate 发送消息
  • 需要ConnectionFactory的实现来连接消息代理
  • @RabbitListener  监听消息代理发布的消息
  • @EnableRabbit 开启支持
SpringBoot自动配置 JmsAutoConfiguration RabbitAutoConfiguration
实现 ActiveMQ、HornetMQ RabbitMQ

RabbitMQ简介

  • RabbitMQ是一个由erlang开发的AMQP开源实现

核心概念

Message

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

Publisher

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

Exchange

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

  • direct(默认)

消息中的路由键(routing key)如果和Binding中binding key一致,交换器就将消息发送到对应的队列中。路由键与队列名完全匹配,如果一个队列绑定到交换机要求路由键为“dog”,则只转发routing key标记为“dog”的消息,不会转发给“dog.puppy”,它是完全匹配,单播模式。

消息队列MQ_第1张图片

  • fanout

每个发到fanout类型交换器的消息都会分发到有绑定的队列中,fanout交换器不处理路由键,只是简单的将队列绑定到交换器上,每个发送到交换器的消息都会被转发到与该交换器绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。fanout类型转发消息是最快的。

消息队列MQ_第2张图片

  • topic

通过匹配模式匹配分配消息的路由键属性,将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。它将路由键和绑定键的字符串切分成单词,这些单词之间用点隔开,它同样也会识别两个通配符(#、*)。#:匹配0个或者多个单词,* 匹配一个单词。

消息队列MQ_第3张图片

  • header

header匹配AMQP消息的header,而不是路由,headers交换器和direct交换器完全一致,但性能差很多。

Queue

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

Binding

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

Exchange和Queue的绑定可以是多对多的关系

Connection

网络连接,比如一个TCP连接。

Channel

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

Consumer

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

Virtual Host

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

消息队列MQ_第4张图片

Docker安装RabbitMQ 

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

4369、25672(Erlang发现&集群端口)

5671、5672(AMQP端口)

15672(web管理后台端口)

61613、61614(STOMP协议端口)

1883、8883(MQTT协议端口)

http://www.rabbitmq.com/networking.html

SpringBoot整合RabbitMQ

  • 引入spring-boot-start-amqp:RabbitAutoConfiguration 就会自动生效
  • RabbitAutoConfiguration 给容器中配置了

RabbitTemplate、AmqpAdmin、CachingConnectionFactory、RabbitMessagingTemplate


    org.springframework.boot
    spring-boot-starter-amqp
  • 所有属性都是spring.rabbitmq,配置 spring.rabbitmq 信息
spring.rabbitmq.addresses=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.virtual-host=/
# 开启发送端消息抵达服务(Broker)的确认
spring.rabbitmq.publisher-confirm-type=correlated
# 开启发送端消息抵达队列(Queue)的确认
spring.rabbitmq.publisher-returns=true
# 只要消息抵达队列,以异步发送优先回调 returnConfirm
spring.rabbitmq.template.mandatory=true
# 开启手动ack机制
spring.rabbitmq.listener.direct.acknowledge-mode=manual
  • @EnableRabbit 开启功能

  • 监听消息:@RabbitListener、@RabbitHandler

  1. @RabbitListener:标注类、方法
  2. @RabbitHandler:标注在方法上(重载区分不同的消息)

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

  • 保证消息不丢失,可靠抵达,可以使用事务消息,性能下降250倍,为此引入确认机制

  • publisher confirmCallback 确认模式

  1. 在创建 connectionFactory 的时候设置 PublisherConfirms(true)选项,开 confirmCallback。
  2. CorrelationData:用来表示当前消息唯一性。
  3. 消息只要被 broker 接收到回执行 confirmCallback,如果是 cluster 模式,需要所有的 broker 接收到才会调用 confirmCallback。
  4. 被broker接收到只能表示message已经抵达服务器,并不能保证消息一定会被投递到目标queue里。
  • publisher returnsCallback 未投递到 queue 退回

  1. confirm 模式只能保证消息抵达broker,不能保证消息准确投递到目标queue里。我们需要保证消息一定要投递到目标queue里,此时需要 return 退回模式。
  2. 如果未能投递到目标queu里将调用returnsCallback,可以记录详细投递数据,定期巡检或者自动纠错都需要这些数据。
  • consumer ack 机制

  1. 消费者收到消息,默认会自动ack,broker将移除此消息。但是如果无法确定此消息是否被处理完成或者成功处理,我们可以开启手动ack模式。
  2. basic.ack 用于肯定确认,broker 将移除此消息。
  3. basic.nack 用于否定确认,可以指定broker是否丢弃此消息,可以批量。
  4. basic.reject 用于否定确认,同basic.nack,但不能批量。
  5. 消息如果一直没有调用ack/nack,broker认为此消息正在被处理,不会投递给别人,也不会被broker移除。

消息队列MQ_第5张图片

@EnableRabbit
@Configuration
public class RabbitmqConfig {

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

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

    /**
     * 定制RabbitTemplate
     * 1、服务(Broker)收到消息回调
     *    1)spring.rabbitmq.publisher-confirm-type=correlated
     *    2)设置ConfirmCallback
     * 2、消息正确抵达队列(Queue)回调
     *    1)spring.rabbitmq.publisher-returns=true
     *       spring.rabbitmq.template.mandatory=true
     *    2)设置ReturnCallback
     * 3、消费端确认(默认自动确认)
     *    设置手动签收,没有ack,消息就会一直处于Unacked状态,即使consumer宕机,消息也不会丢失,会重新变为Ready,下次连接重新发送
     *    spring.rabbitmq.listener.direct.acknowledge-mode=manual
     *    channel.basicAck(deliveryTag, false) 签收
     *    channel.basicNack(deliveryTag, false, false) 拒签
     *    long deliveryTag: channel内自增
     */
    public void initRabbitTemplate(RabbitTemplate rabbitTemplate) {
        // 设置消息抵达服务(Broker)回调
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            /**
             * 确认回调机制
             *
             * @param correlationData 当前消息的唯一关联数据(消息唯一ID)
             * @param b 消息是否成功收到
             * @param s 失败原因
             */
            @Override
            public void confirm(@Nullable CorrelationData correlationData, boolean b, @Nullable String s) {
                System.out.println("ConfirmCallback==>correlationData["+correlationData+"]==>b["+b+"]==>s["+s+"]");
            }
        });
        // 设置消息抵达队列(Queue)确认回调
        rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
            /**
             * 只要消息没有投递成功,就会触发这个失败回调
             *
             * @param returnedMessage
             */
            @Override
            public void returnedMessage(ReturnedMessage returnedMessage) {
                System.out.println("ReturnsCallback==>"+returnedMessage.toString());
            }
        });
    }

    @Bean
    public Queue orderDelayQueue() {
        Map map = new HashMap<>();
        map.put("x-dead-letter-exchange", OrderConstant.ORDER_EVENT_EXCHANGE);
        map.put("x-dead-letter-routing-key", OrderConstant.ORDER_RELEASE_ORDER_ROUTING_KEY);
        map.put("x-message-ttl", OrderConstant.ORDER_MESSAGE_TTL);

        Queue queue = new Queue(OrderConstant.ORDER_DELAY_QUEUE, true, false, false, map);

        return queue;
    }

    @Bean
    public Queue orderReleaseQueue() {
        Queue queue = new Queue(OrderConstant.ORDER_RELEASE_QUEUE, true, false, false);

        return queue;
    }

    @Bean
    public Exchange orderEventExchange() {
        return new TopicExchange(OrderConstant.ORDER_EVENT_EXCHANGE, true, false);
    }

    @Bean
    public Binding orderCreateBinding() {
        return new Binding(OrderConstant.ORDER_DELAY_QUEUE,
                           Binding.DestinationType.QUEUE,
                           OrderConstant.ORDER_EVENT_EXCHANGE,
                           OrderConstant.ORDER_CREATE_ORDER_ROUTING_KEY,
                           null);
    }

    @Bean
    public Binding orderReleaseBinding() {
        return new Binding(OrderConstant.ORDER_RELEASE_QUEUE,
                           Binding.DestinationType.QUEUE,
                           OrderConstant.ORDER_EVENT_EXCHANGE,
                           OrderConstant.ORDER_RELEASE_ORDER_ROUTING_KEY,
                           null);
    }

    @Bean
    public Binding orderReleaseOtherBinding() {
        return new Binding(WareConstant.STOCK_RELEASE_QUEUE,
                           Binding.DestinationType.QUEUE,
                           OrderConstant.ORDER_EVENT_EXCHANGE,
                           OrderConstant.ORDER_RELEASE_STOCK_ROUTING_KEY,
                           null);
    }

}

RabbitMQ消息可靠性

消息丢失

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

消息重复

  • 消息消费失败,由于重试机制,自动又将消息发送出去。
  • 消息成功消费,ack时宕机,Broker的消息重新由unack变为ready,Broker又重新发送。
  1. 消费者的业务消费接口设计幂等性
  2. 使用防重表,发送消息每一个都有业务的唯一标识,处理过就不在处理。
  3. rabbitmq的每一个消息都有redelivered字段,可以获取是否是被重新投递过来的,而不是第一次投递过来的。

消息积压

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

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

消息TTL(Time To Live)

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

Dead Letter Exchage(DLX)

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

消息队列MQ_第6张图片

消息队列MQ_第7张图片

RabbitMQ延时队列模拟定时关单&库存自动解锁 

消息队列MQ_第8张图片

  • 订单提交发送消息
rabbitTemplate.convertAndSend(OrderConstant.ORDER_EVENT_EXCHANGE, OrderConstant.ORDER_CREATE_ORDER_ROUTING_KEY, orderResult);
  • 定时关单
@Slf4j
@Service
@RabbitListener(queues = OrderConstant.ORDER_RELEASE_QUEUE)
public class OrderListener {

    @Autowired
    OrderService orderService;

    @RabbitHandler
    public void listener(OrderResultVO orderResult, Channel channel, Message msg) throws Exception {
        log.info("订单过期,收到订单关闭消息====>{}", msg);
        try {
            orderService.closeOrder(orderResult);
            //TODO 如果定时关单先于支付宝响应,手动调用支付宝收单
            channel.basicAck(msg.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            channel.basicReject(msg.getMessageProperties().getDeliveryTag(), true);
        }
    }

}
  •  库存锁定发送消息
rabbitTemplate.convertAndSend(OrderConstant.ORDER_EVENT_EXCHANGE, OrderConstant.ORDER_CREATE_ORDER_ROUTING_KEY, orderResult);
  • 库存自动解锁
@Slf4j
@Service
@RabbitListener(queues = WareConstant.STOCK_RELEASE_QUEUE)
public class StockListener {

    @Autowired
    WareSkuService wareSkuService;

    @RabbitHandler
    public void unLockStock(StockLockedVO lockedVO, Channel channel, Message msg) throws Exception {
        log.info("订单过期,收到库存解锁消息====>{}", msg);
        try {
            wareSkuService.unLockStock(lockedVO);
            channel.basicAck(msg.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            channel.basicReject(msg.getMessageProperties().getDeliveryTag(), true);
        }
    }

    @RabbitHandler
    public void unLockStock(OrderTO orderTO, Channel channel, Message msg) throws Exception {
        log.info("订单关闭,收到库存解锁消息====>{}", msg);
        try {
            wareSkuService.unLockStock(orderTO);
            channel.basicAck(msg.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            channel.basicReject(msg.getMessageProperties().getDeliveryTag(), true);
        }
    }
}

你可能感兴趣的:(redis,java,数据库)