RabbitMQ消息中间件

概念

Message Queue : 消息队列,接收并存储消息,再转发。

三个作用

1.流量消峰填谷

使用消息队列做缓冲,减少突然访问的压力

2.解耦

如订单系统和库存系统之间不在互相影响,独立运行,达到了应用解耦的目的;

即RabbitMQ多用于分布式系统之间的通信

3.异步处理

在项目中,可将一些无需即时返回且耗时的操作提取出来,进行异步处理,而这种异步处理的方式大大的节省了服务器的请求响应时间,从而提高系统吞吐量。

劣势:

  1. 系统可用性降低:系统引入的外部依赖越多,系统稳定性越差
  2. 系统复杂度提高
  3. 需保证消息数据处理的一致性问题

流程图解

 

各名词解释:

Broker:接收和分发消息的应用, 消息处理中心:负责消息存储、确认、重试等,一般其中会包含多个 queue

Connection:publisher/consumer 和 broker 之间的 TCP 连接

Channel:如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP Connection 的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个 thread 创建单独的 channel 进行通讯,AMQP method 包含了 channel id 帮助客户端和 message broker 识channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的Connection 极大减少了操作系统建立 TCP connection 的开销 Exchange:message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到 queue 中去。

常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast) Queue:消息最终被送到这里等待 consumer 取走

Binding:exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key,Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据

Routing Key:路由关键字,exchange根据这个关键字进行消息投递;

四种mq对比

RoutingKey与BindingKey的区别:

RoutingKey:路由键。生产者将消息发给交换器的时候,一般会指定一个RoutingKey用来指明这个消息的路由规则,这个RoutingKey需要与交换器类型和绑定键(BindingKey)联合使用才能最终生效。在交换器类型和绑定键(BindingKey)固定的情况下,生产者可以在发送消息给交换器时,通过指定RoutingKey来决定消息流向哪里。

BindingKey:RabbitMQ中通过绑定将交换器与队列关联起来,在绑定的时候一般会指定一个绑定键(BindingKey),这样RabbitMQ就知道如何正确地将消息路由到队列了。

生产者将消息发送给交换器时,需要一个RoutingKey,当BindingKey和RoutingKey相匹配时,消息会被路由到对应的队列中。在绑定多个队列到同一个交换器的时候,这些绑定允许使用相同的BindingKey。

BindingKey并不是在所有的情况下都生效,它依赖于交换器类型,比如fanout类型的交换器就会无视BindingKey,而是将消息路由到所有绑定到该交换器的队列中。

BindingKey其实也属于路由键中的一种,官方解释为:the routing key to use fot the binding。可以翻译为:在绑定的时候使用的路由键。大多数时候,包括官方文档和RabbitMQ Java API中都把BindingKey和RoutingKey看作RoutingKey,

为了避免混淆,可以这么理解:

在使用绑定的时候,其中需要的路由键是BindingKey。 涉及的客户端方法为:channel.exchangeBind、channel.queueBind 对应的AMQP命令为:Exchange.Bind、Queue.Bind。

在发送消息的时候,其中需要的路由键是RoutingKey。 涉及的客户端方法为:channel.basicPublish 对应的AMQP命令为:Basic.Publish。

大多数情况下习惯性地将BindingKey写成RoutingKey,尤其是在使用direct类型的交换器的时候。

工作模式

1、简单模式

一个队列一个消费者

配置类RabbitmqConfig:
@Bean
public Queue simpleQueue(){
    return QueueBuilder.durable("simpleQueue").build();
}
生产者类Producter:
@Autowired
private RabbitTemplate rabbitTemplate;
/**
 * 往队列中发消息
 */
public void send(String msg){
    System.out.println("生产者发消息:"+msg);
    rabbitTemplate.convertAndSend("simpleQueue",msg);
}
消费者类Consumer:
/**
 *    @RabbitListener(queues = "simpleQueue"): 监听指定的队列
 */
@RabbitListener(queues = "simpleQueue")
public void receive(String msg){
    System.out.println("消费者接收到消息:"+msg);
}

2、work工作模式

一个队列多个消费者, c1和c2共同监听一个消息队列,但是一个消息只能被一个消费者消费。

配置类RabbitmqConfig:
@Bean
public Queue workQueue(){
    return QueueBuilder.durable("workQueue").build();
}
生产者类Producter:
@Autowired
private RabbitTemplate rabbitTemplate;


public void send(String msg){
    System.out.println("生产者发消息:"+msg);
    rabbitTemplate.convertAndSend("workQueue",msg);
}
消费者类Consumer:
@RabbitListener(queues = "workQueue")
public void receiveA(String msg){
    System.out.println("消费者接收到消息:"+msg);
}

@RabbitListener(queues = "workQueue")
public void receiveB(String msg){
    System.out.println("消费者接收到消息:"+msg);
}

3、发布订阅模式(三种)

生产者将消息不是直接发送到队列,而是发送到交换机。

exchange交换机功能:

  1. 接收生产者发送的消息。
  2. 知道如何处理消息,例如递交给某个特定队列、所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。

注意:exchange交换机只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!

3.1、Fanout(广播模式):

将消息交给所有绑定到交换机的队列  

配置类RabbitmqConfig:

 创建两个队列和一个交换机、并绑定

 /**
     * 发布订阅模式
     * 创建两个队列
     * @return
     */
@Bean
public Queue fanoutQueueA(){
    return QueueBuilder.durable("fanoutQueueA").build();
}

@Bean
public Queue fanoutQueueB(){
    return QueueBuilder.durable("fanoutQueueB").build();
}
 /**
     * 创建交换机
     */
    @Bean
    public FanoutExchange fanoutExchange(){
        return new FanoutExchange("fanoutExchange");
    }
    /**
     * 把两个队列绑定到交换机上
     */
    @Bean
    public Binding fanoutQueueAToFanoutExchange(Queue fanoutQueueA,FanoutExchange fanoutExchange){
        return BindingBuilder.bind(fanoutQueueA).to(fanoutExchange);
    }
    @Bean
    public Binding fanoutQueueBToFanoutExchange(Queue fanoutQueueB,FanoutExchange fanoutExchange){
        return BindingBuilder.bind(fanoutQueueB).to(fanoutExchange);
    }
生产者类Producter:
@Autowired
private RabbitTemplate rabbitTemplate;

public void send(String msg){
    System.out.println("生产者发消息:"+msg);
    rabbitTemplate.convertAndSend("fanoutExchange","",msg);
}
消费者类Consumer:
@RabbitListener(queues = "fanoutQueueA")
public void receiveA(String msg){
    System.out.println("消费者A接收到消息:"+msg);
}

@RabbitListener(queues = "fanoutQueueB")
public void receiveB(String msg){
    System.out.println("消费者B接收到消息:"+msg);
}

3.2、direct(Routing路由模式)

 将消息交给绑定到交换机的特定队列

特点(三点):

1、队列与交换机不是任意绑定,而是要指定一个RoutingKey(路由key);

2、消息的发送方在向Exchange发送消息时,也必须指定消息的 `RoutingKey;

3、Exchange不再把消息交给每一个绑定的队列,而是根据消息的Routing Key进行判断,只有队列的Routingkey与消息的 Routing key完全一致,才会接收到消息。

配置类RabbitmqConfig:
//创建两个队列
@Bean
public Queue directQueueA() {
    return QueueBuilder.durable("directQueueA").build();
}

@Bean
public Queue directQueueB() {
    return QueueBuilder.durable("directQueueB").build();
}
//创建一个交换机
    @Bean
    public DirectExchange directExchange() {
        return new DirectExchange("DirectExchange");
    }
//将两个队列与交换机绑定,指定RoutingKey
 @Bean
    public Binding directQueueAToDirectExchange(Queue directQueueA, DirectExchange directExchange) {
        return BindingBuilder.bind(directQueueA).to(directExchange).with("red");
    }

    @Bean
    public Binding directQueueBToDirectExchange(Queue directQueueB, DirectExchange directExchange) {
        return BindingBuilder.bind(directQueueB).to(directExchange).with("green");
    }
生产者类Producter:
@Autowired
private RabbitTemplate rabbitTemplate;

public void send(String msg,String routeKey){
    System.out.println("生产者发送消息:"+msg);
    rabbitTemplate.convertAndSend("DirectExchange",routeKey,msg);
}
消费者类Consumer:
@RabbitListener(queues = "directQueueA")
public void receiveA(String msg) {
    System.out.println("消费者A接收到的消息:" + msg);
}

@RabbitListener(queues = "directQueueB")
public void receiveB(String msg) {
    System.out.println("消费者B接收到的消息:" + msg);
}

3.3、Topics通配符模式/ 主题模式(模糊路由)

Topic类型与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key 的时候使用通配符Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert

通配符规则:

#:匹配0个或多个词

*:匹配只有1个词

举例:

item.#:能够匹配item.insert.abc 或者 item.insert

item.*:只能匹配item.insert

消息确认

yml配置:

 publisher-confirm-type: correlated #开启交换机消息确认
    publisher-returns: true #开启队列消息确认
    listener:
      simple:
        acknowledge-mode: manual #开启消费者方,手动确认
        prefetch: 1 #消费者一次消费队列中的一个消息,默认值:250个, 作用:可以限流,分批消费

发送消息确认机制:

发送到交换机、传输到队列

1)为确保消息是否发送到交换机,设置发布时确认,确认消息是否到达 Broker 服务器;

2)为确保消息是否传输到队列,设置回调函数,此方法只有在消息发送失败时才会执行。

CallbackConfig

继承RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback并重写方法  

@Slf4j
@Configuration
public class CallbackConfig implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void init() {
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnCallback(this);
        rabbitTemplate.setMandatory(true);
    }

    /**
     * 把相关的内容放入mysql:
     * 1、交换机名称
     * 2、routingKey
     * 3、消息
     */

    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            return;
        }
        //交换机名称
        MessageProperties messageProperties = correlationData.getReturnedMessage().getMessageProperties();
        String exchange = messageProperties.getReceivedExchange();
        //routingKey
        String routingKey = messageProperties.getReceivedRoutingKey();
        //消息
        byte[] msgs = correlationData.getReturnedMessage().getBody();
        //把交换机名称、routingKey、消息进行持久化(放入mysql);
        // 正常对这类消息的处理:定时扫描,并把消息重新发到mq;
        //如果定时处理出现问题,保底人工处理
        log.error("confirm error info:{}, exchange:{},routingKey:{},message:{}",
                cause, exchange, routingKey, msgs);
    }

     * // 在这个方法中,您可以处理未能被路由到队列的消息
     * // message:无法被路由的消息
     * // replyCode:响应码,用于指示失败的原因
     * // replyText:响应文本,提供了更详细的错误信息
     * // exchange:消息发送时指定的交换机
     * // routingKey:消息发送时指定的路由键
    @Override
    public void returnedMessage(Message message, intreplyCode, String replyText, String exchange, String routingKey) {
        //把交换机名称、routingKey、消息进行持久化(放入mysql);
        // 正常对这类消息的处理:定时扫描,并把消息重新发到mq;
        //如果定时处理出现问题,保底人工处理
        log.error("confirm error info:{}, exchange:{},routingKey:{},message:{}",
                replyText, exchange, routingKey, message.getBody());
    }

}
配置类RabbitmqConfig:
 * 验证消息确认队列
 */
@Bean
public Queue confirmQueue() {
    return QueueBuilder.durable("confirmQueue").build();
}

@Bean
public DirectExchange confirmExchange() {
    return new DirectExchange("confirmExchange");
}

@Bean
public Binding confirmQueueToConfirmExchange(Queue confirmQueue, DirectExchange confirmExchange) {
    return BindingBuilder.bind(confirmQueue).to(confirmExchange).with("confirm");
}

消费者消息确认机制

消费者类Consumer:
@RabbitListener(queues = "confirmQueue")
public void receive(String msg, Message message, Channel channel) throws IOException {
    try {
        //1.拿到信息
        System.out.println("消费者接收 信息:" + msg);
        String s = new String(message.getBody());
        //确认消息
        //arg1: 消息Id
        //arg2: 是否批量确认
        //channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    } catch (Exception e) {
        e.printStackTrace();
        //args3:消息是否重新放回队列,一般是false
        //没有成功被消费者消费的消息,又没有放回原队列,称为死信
        channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,false);
    }

}

基于插件的延迟队列

配置类RabbitmqConfig:
/**
 * 定义延迟队列
 */
@Bean
public Queue delayQueue() {
    return QueueBuilder.durable("delayQueue").build();
}

/**
 * customExchange:自定义交换机,其实就是fanout,direct,topic其中一种
 *
 * @return
 */
@Bean
public CustomExchange customExchange() {
    Map map = new HashMap();
    //指定交换机类型
    map.put("x-delayed-type", "direct");
    /**
     * arg1:交换机名字
     * arg2: 指定交换机里的信息类型,延迟消息
     * arg3: 是否持久化,是否将没有被消费的消息持久化
     * arg4: 没有队列绑定到交换机,交换机是否删除。
     * arg5: 初始化参数
     */
    return new CustomExchange("customExchange", "x-delayed-message", true, false, map);
}

@Bean
public Binding delayQueueToCustomExchange(Queue delayQueue, CustomExchange customExchange) {
    return BindingBuilder.bind(delayQueue).to(customExchange).with("delay").noargs();
}
生产者类Producter:
@Autowired
private RabbitTemplate rabbitTemplate;

public void send(String msg, int delayTime) {
    log.info("生产者发送消息:" + msg);
    rabbitTemplate.convertAndSend("customExchange", "delay", msg,
            new MessagePostProcessor() {
                /**
                 * 在消息发送到消息队列之前对消息进行处理
                 * @param message
                 * @return
                 * @throws AmqpException
                 */
                @Override
                public Message postProcessMessage(Message message) throws AmqpException {
                    // 给消息设置过期时间,单位是毫秒
                    message.getMessageProperties().setDelay(delayTime);
                    return message;
                }
            });
}
消费者类Consumer:
@RabbitListener(queues = "delayQueue")
public void receive(String msg, Channel channel, Message message) {
    try {
        //1.拿到消息
         log.info("消费者接收到消息:" + msg);
        //2.做业务
   
        //3.确认消息
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    } catch (Exception e) {
        try {
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

死信队列

队列消息变成死信(deadmessage)之后,它能被重新被发送到另一个交换器中,这个交换器就是DLX,全称为Dead-Letter-Exchange,绑定DLX的队列就称之为死信队列。

消息变成死信的几种情况:

1、消息被拒绝(basic.reject/ basic.nack)并且requeue=false

2、消息TTL过期

3、队列达到最大长度

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值