RabbitMQ 高级特性

RabbitMQ 高级特性

1.1 消息的可靠投递

在使用 RabbitMQ 的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景。RabbitMQ 为我们提供了两种方式用来控制消息的投递可靠性模式。

  • confirm 确认模式

📌

确认队列中的消息是否被消费者消费

  • return 退回模式

💡

交换机中的消息是否到达队列里面

到 returnCallBack

不到 走returnCallBack

rabbitmq 整个消息投递的路径为:

producer—>rabbitmq-broker—>exchange—>queue—>consumer

生产:消息从 producer 到 exchange 则会返回一个 confirmCallback()。都会执行,返回false就失败

内部:消息从 exchange–>queue 投递失败则会返回一个 returnCallback()。

我们将利用这两个 callback 控制消息的可靠性投递

确认:

使用springboot项目整合生产者

1.配置文件:
spring:
  rabbitmq:
    host: 192.168.159.34
    username: root
    password: root
    virtual-host: /root
    port: 5672
    publisher-confirms: true #设置开启确认模式
server:
  port: 8081
  #消息的可靠性传递

2.配置类
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ConfirmConfig {
    public static final String QUEUE_CONFIRM_NAME="test_confirm_queue";
    public static final String EXCHAGE_CONFIRM_NAME="test_confirm_exchange";
    //定义一个交换机,定义一个queue
    @Bean("test_confirm_queue")
    public Queue getQueue(){
        return QueueBuilder.durable(QUEUE_CONFIRM_NAME).build();
    }
    /**
     * 定义一个交换机
     */
    @Bean("test_confirm_exchange")
    public Exchange getExchange(){
      return ExchangeBuilder.directExchange(EXCHAGE_CONFIRM_NAME).durable(true).build();
    }
    /**
     * 绑定一下交换机和queue
     */
    @Bean
    public Binding bindQueueAndExchange(@Qualifier("test_confirm_queue")Queue queue,@Qualifier("test_confirm_exchange")Exchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with("confirm").noargs();
    }
}
3.测试类:
import org.junit.Test;
import org.junit.runner.RunWith;
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 org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import yyl.ProducerApplication;
import yyl.config.ConfirmConfig;
import yyl.config.RabbitMqConfig;
import javax.annotation.Resource;
@SpringBootTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = ProducerApplication.class)
public class ProcedureTest {
    @Resource
    private RabbitTemplate rabbitTemplate;
    //确认模式的测试
    //1.开启确认模式 开启publisher-confirms="true"
    //2. 定义回调  在rabbitTemplate中设置信息的确认
    @Test
    public void testConfirm(){
           /**
            * @param correlationData 相关配置信息
            * @param ack   exchange交换机 是否成功收到了消息。true 成功,false代表失败
            * @param cause 失败原因
            */
        //匿名的内部类
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String s) {
                System.out.println("confirm方法执行了。。。。。。");
                if(ack){
                    System.out.println("接收成功");
                }else{
                    System.out.println("接收失败,失败的原因是:"+s);
                }
            }
        });
        //发送消息
        rabbitTemplate.convertAndSend(ConfirmConfig.EXCHAGE_CONFIRM_NAME,"confirm","confirm message");
    }
}
4.总结:

设置publisher-confirms="true" 开启 确认模式。

使用rabbitTemplate.setConfirmCallback设置回调函数。当消息发送到exchange后回调confirm方法。在方法中判断ack,如果为true,则发送成功,如果为false,则发送失败,需要处理。

回退:

1.配置文件:
spring:
  rabbitmq:
    host: 192.168.159.34
    username: root
    password: root
    virtual-host: /root
    port: 5672
   # publisher-confirms: true #设置开启确认模式
    publisher-returns: true #设置开启回退模式
server:
  port: 8081
  #消息的可靠性传递
2.配置类

跟确认中的配置文件保持一致即可

3.测试类
  /**
     * return的测试
     * 1.开启 回退模式
     * 2.在rabbitTemplate中设置returnCallBack
     * 3.设置exchange处理消息的模式
     *    1. 如果消息没有路由到queue 则丢弃消息
     *    2. 如果没有路由到queue 则将消息返回给发送方 returnCallBack
     */
    @Test
    public void testReturn(){
        //设置交换机处理失败消息的模式
        rabbitTemplate.setMandatory(true);
        //匿名的内部类
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            @Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                System.out.println("return 执行了....");


//                System.out.println(message);
//                System.out.println(replyCode);
//                System.out.println(replyText);
//                System.out.println(exchange);
//                System.out.println(routingKey);
            }
        });
        //发送消息
        rabbitTemplate.convertAndSend(ConfirmConfig.EXCHAGE_CONFIRM_NAME,"confirm","confirm message");
    }
总结:

设置publisher-returns="true"开启 退回模式。

使用rabbitTemplate.setReturnCallback设置退回函数,当消息从exchange路由到queue失败后,如果设置了rabbitTemplate.setMandatory(true)参数,则会将消息退回给producer。并执行回调函数returnedMessage。

事务机制

在RabbitMQ中也提供了事务机制,但是性能较差。

使用channel下列方法,完成事务控制:

txSelect() :用于将当前channel设置成transaction模式

txCommit():用于提交事务

txRollback():用于回滚事务

有兴趣自己操作

1.2 Consumer Ack

ack指Acknowledge,确认。 表示消费端收到消息后的确认方式。

broker发送消息给消费端的一种可靠性保证

有三种确认方式:

自动确认:acknowledge="none" 。不管处理成功与否,业务处理异常也不管

(当消费者意担接收到消息之后,消费者就会给broker一个回执,证明已经接收到消息 了,不管消息到底是否成功)

手动确认:acknowledge="manual" 。可以解决业务异常的情况

(收到消息之后不会立马确认收到消息,当业务处理没有问题的时候手动的调用代码的方 式来进行处理,如果业务失败了,就可以进行额外的操作)

根据异常情况确认:acknowledge="auto",(这种方式使用麻烦,不作讲解)

其中自动确认是指,当消息一旦被Consumer接收到,则自动确认收到,并将相应 message 从 RabbitMQ 的消息缓存中移除。但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失。如果设置了手动确认方式,则需要在业务处理成功后,调用channel.basicAck(),手动签收,如果出现异常,则调用channel.basicNack()方法,让其自动重新发送消息。

自动确认:

消费端:

1.配置文件:
spring:
  rabbitmq:
    host: 192.168.159.34
    username: root
    password: root
    virtual-host: /root
    port: 5672
2.监听类:
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageListener;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class AckConsumer implements MessageListener {
    @RabbitListener(queues = "test_confirm_queue")
    @Override
    public void onMessage(Message message) {
        System.out.println(new String(message.getBody()));
    }  
}
总结:

什么都不需要设置使用的就是自动确认模式

手动确认:

1.配置文件
spring:
  rabbitmq:
    host: 192.168.159.34
    username: root
    password: root
    virtual-host: /root
    port: 5672
    listener:
      simple:
        acknowledge-mode: manual #设置手动确认
2.监听类

手动签收 使用的channel里的成功或者是失败的方法

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageListener;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;
/**
 * Consumer ACK机制:
 *  1. 设置手动签收。acknowledge="manual"
 *  2. 让监听器类实现ChannelAwareMessageListener接口
 *  3. 如果消息成功处理,则调用channel的 basicAck()签收
 *  4. 如果消息处理失败,则调用channel的basicNack()拒绝签收,broker重新发送给consumer
 */
@Component
public class AckConsumer implements ChannelAwareMessageListener {
    @Override
    @RabbitListener(queues = "test_confirm_queue")
    public void onMessage(Message message, Channel channel) throws Exception {
        Thread.sleep(2000);
        //1.接收消息
        System.out.println(new String(message.getBody()));
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            //2.处理业务luoji
            System.out.println("开始处理业务逻辑");
            int i=3/0;
            //3.成功就直接签收
            /**
             *  basicAck(long deliveryTag, boolean multiple)
             *  参数1 当前收到的消息的tag标签的内容
             *  参数2 是否允许多条消息被同时签收
             */
            //得到第一个参数
            channel.basicAck(deliveryTag, true);
        }catch(Exception e){
            //发生异常的时候就拒接签收
            /**
             * basicNack(long deliveryTag, boolean multiple, boolean requeue)
             * 参数3 是否重回队列  如果设置为true,则消息重新回到queue,broker会重新发送该消息给消费端
             */
            channel.basicNack(deliveryTag,true,true);
        }
    }
}
总结:

设置手动模式需要在配置文件里面添加 acknowlwdge-mode:manual

如果消费端没有异常就会走basicAck的方法,确认签收消息,如果有异常就会调用basicNack的方法拒接签收消息,让MQ重新发送消息

消息可靠性总结

持久化

  • exchange要持久化
  • queue要持久化
  • message要持久化

(当broker重启的时候信息还在)

生产方确认Confirm

消费方确认Ack

Broker高可用

1.3 消费端限流

请求瞬间增多,每秒5000个请求

在配置 prefetch属性设置消费端一次拉取多少消息

消费端的确认模式一定为手动确认。acknowledge="manual"

需要配置一次拉取多少消息

1.配置文件:
spring:
  rabbitmq:
    host: 192.168.159.34
    username: root
    password: root
    virtual-host: /root
    port: 5672
    listener:
      simple:
        acknowledge-mode: manual
        prefetch: 1
监听类
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;
/**
 * Consumer ACK机制:
 */
@Component
public class XianLiuConsumer implements ChannelAwareMessageListener {
    @Override
    @RabbitListener(queues = "test_confirm_queue")
    public void onMessage(Message message, Channel channel) throws Exception {
        //1.接收消息
        System.out.println(new String(message.getBody()));
        //没有手动的确认接收消息 所以就会显示有一条未被确认消息
    }
}

如果没有设置限流,会直接全部拉取消息,但是并不消费

总结:

配置 prefetch属性设置消费端一次拉取多少消息

消费端的确认模式一定为手动确认。acknowledge="manual"

1.4 TTL

全称 Time To Live(存活时间/过期时间)。

当消息到达存活时间后,还没有被消费,会被自动清除。

RabbitMQ可以对消息设置过期时间,也可以对整个队列(Queue)设置过期时间。

可以在管理台新建队列、交换机,绑定

图形化操作:

添加队列

添加交换机

将交换机和对应的队列进行绑定

代码:

1.配置文件
spring:
  rabbitmq:
    host: 192.168.159.34 # ip
    port: 5672
    username: root
    password: root
    virtual-host: /root
2.配置类
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TtlRabbitMqConfig {
    //1.交换机的名字
    public static final String EXCHANGE_NAME="exchange_topic_ttl";
    //2.队列名字
    public static final String QUEUE_NAME_1="queue_topic_ttl";
    //1.获取交换机
    @Bean("exchange_name")
    public Exchange getExcahnge(){
        //return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();
        return ExchangeBuilder.topicExchange(EXCHANGE_NAME).build();
    }
    //队列1
    @Bean("queue_name")
    public Queue getQueue(){
        //设置队列的过期时间 5s
        return  QueueBuilder.durable(QUEUE_NAME_1).withArgument("x-message-ttl",30000).build();
    }
    //队列和交换机进行绑定
    @Bean
    public Binding bindExchangeAndQueue(@Qualifier("exchange_name") Exchange exchange,@Qualifier("queue_name") Queue queue){
        return BindingBuilder.bind(queue).to(exchange).with("ttl.*").noargs();
    }
}
3.测试类
package proceduretest;
import org.springframework.test.context.ContextConfiguration;
import yyl.ProcedureApplication;
import yyl.config.RabbitMqConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import yyl.ttlconfig.TtlRabbitMqConfig;
import javax.annotation.Resource;
@SpringBootTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes= ProcedureApplication.class)
public class ProcedureTest {
    @Resource
    private RabbitTemplate rabbitTemplate;
    @Test
    public void testProcedure(){
        rabbitTemplate.convertAndSend(TtlRabbitMqConfig.EXCHANGE_NAME,"ttl.hh","我就是测试一下设置的过期时间而已!!!");
    }
}
可以设置队列的统一的过期时间也可以设置消息的单独的过期时间
package proceduretest;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.test.context.ContextConfiguration;
import yyl.ProcedureApplication;
import yyl.config.RabbitMqConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import yyl.ttlconfig.TtlRabbitMqConfig;
import javax.annotation.Resource;

@SpringBootTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes= ProcedureApplication.class)
public class ProcedureTest {
    @Resource
    private RabbitTemplate rabbitTemplate;
    @Test
    public void testProcedure(){
        // 消息后处理对象,设置一些消息的参数信息
        MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                //1.设置message的信息
                message.getMessageProperties().setExpiration("5000");//消息的过期时间
                //2.返回该消息
                return message;
            }
        };
        rabbitTemplate.convertAndSend(TtlRabbitMqConfig.EXCHANGE_NAME,"ttl.hh","我就是测试一下设置的过期时间而已!!!",messagePostProcessor);
    }
}
总结:

设置队列过期时间使用参数:x-message-ttl,单位:ms(毫秒),会对整个队列消息统一过期。

设置消息过期时间使用参数:expiration。单位:ms(毫秒),当该消息在队列头部时(消费时),会单独判断这一消息是否过期。

如果两者都进行了设置,以时间短的为准。

消息过期后,只有消息在队列顶端,才会判断其是否过期(移除掉)

1.5 死信队列

死信队列,英文缩写:DLX 。Dead Letter Exchange(死信交换机,因为其他MQ产品中没有交换机的概念),当消息成为Dead message后,可以被重新发送到另一个交换机,这个交换机就是DLX。

比如消息队列的消息过期,如果绑定了死信交换器,那么该消息将发送给死信交换机

消息在什么情况下会成为死信?(面试会问)

1.队列消息长度到最大的限制

最大的长度设置为10当第11条消息进来的时候就会成为死信

2. 消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false(不重新回到队列中)

设置消费者为手动签收的状态

3. 原队列存在消息过期设置,消息到达超时时间未被消费;

队列绑定交换机的方式是什么?

给队列设置参数: x-dead-letter-exchange 和 x-dead-letter-routing-key

// 1. 交换机 :正常的交换机 死信交换机

// 2.队列 :正常的 死信

//3.绑定 正常ex - 正常的que

正常的que和死信交换机

死信ex-死信queue

代码:

配置文件

(略)不需要额外配置信息

配置类
package yyl.deadconfig;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DeadRabbitMqConfig {
    //1.正常交换机的名字
    public static final String NORMAL_EXCHANGE_NAME="normal_exchange";
    //2.正常队列名字
    public static final String NORMAL_QUEUE_NAME="normal_queue";
    //1.死信交换机的名字
    public static final String DEAD_EXCHANGE_NAME="dead_exchange";
    //2.死信队列名字
    public static final String DEAD_QUEUE_NAME="dead_queue";
    //1.正常交换机
    @Bean("normal_exchange")
    public Exchange getNormalExcahnge(){
        //return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();
        return ExchangeBuilder.topicExchange(NORMAL_EXCHANGE_NAME).build();
    }
    @Bean("dead_exchange")
    public Exchange getDeadExcahnge(){
        //return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();
        return ExchangeBuilder.topicExchange(DEAD_EXCHANGE_NAME).build();
    }
    //正常队列
    @Bean("normal_queue")
    public Queue getNormalQueue(){
        //设置队列的过期时间 5s
        Queue queue=QueueBuilder.durable(NORMAL_QUEUE_NAME).withArgument("x-message-ttl",30000)
                //最大的长度   10 超过10 条就会成为死信消息
                .withArgument("x-max-length",10)
                //正常队列的死信消息 绑定对应的死信交换机
                .withArgument("x-dead-letter-exchange",DEAD_EXCHANGE_NAME)
                //发送信息时携带的routingkey
                .withArgument("x-dead-letter-routing-key","dead.msg")
                .build();
        return queue;
        //return  QueueBuilder.durable(NORMAL_QUEUE_NAME).withArgument("x-message-ttl",30000).build();
    }
     //死信队列
    @Bean("dead_queue")
    public Queue getDeadQueue(){
        //设置队列的过期时间 5s
        return  QueueBuilder.durable(DEAD_QUEUE_NAME).build();
    }


    //正常队列和交换机进行绑定
    @Bean
    public Binding bindNormalExchangeAndQueue(@Qualifier("normal_exchange") Exchange exchange,@Qualifier("normal_queue") Queue queue){

        return BindingBuilder.bind(queue).to(exchange).with("normal.*").noargs();
    }
    //死信队列和交换机
    @Bean
    public Binding bindDeadExchangeAndQueue(@Qualifier("dead_exchange") Exchange exchange,@Qualifier("dead_queue") Queue queue){


        return BindingBuilder.bind(queue).to(exchange).with("dead.*").noargs();
    }
}
测试类:
 /**
     * 发送测试死信消息:
     *  1. 过期时间
     *  2. 长度限制
     *  3. 消息拒收
     */
    @Test
    public void testDlx(){
        //1. 测试过期时间,死信消息
        //rabbitTemplate.convertAndSend(DeadRabbitMqConfig.NORMAL_EXCHANGE_NAME,"normal.msg","我是一条消息,我会死吗?");
        //2. 测试长度限制后,消息死信
//    for (int i = 0; i < 20; i++) {
//            rabbitTemplate.convertAndSend(DeadRabbitMqConfig.NORMAL_EXCHANGE_NAME,"normal.msg","我是一条消息,我会死吗?");
//        }
        //3. 测试消息拒收
        rabbitTemplate.convertAndSend(DeadRabbitMqConfig.NORMAL_EXCHANGE_NAME,"normal.msg","我是一条消息,我会死吗?");
    }
拒收消息的消费者
package yyl.listener;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageListener;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;
/**
 * Consumer ACK机制:
 *  1. 设置手动签收。acknowledge="manual"
 *  2. 让监听器类实现ChannelAwareMessageListener接口
 *  3. 如果消息成功处理,则调用channel的 basicAck()签收
 *  4. 如果消息处理失败,则调用channel的basicNack()拒绝签收,broker重新发送给consumer
 */
@Component
public class AckConsumer implements ChannelAwareMessageListener {
    @Override
    @RabbitListener(queues = "normal_queue")
    public void onMessage(Message message, Channel channel) throws Exception {
        //Thread.sleep(2000);
        //1.接收消息
        System.out.println(new String(message.getBody()));
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            //2.处理业务luoji
            System.out.println("开始处理业务逻辑");
            int i=3/0;
            //3.成功就直接签收
            /**
             *  basicAck(long deliveryTag, boolean multiple)
             *  参数1 当前收到的消息的tag标签的内容
             *  参数2 是否允许多条消息被同时签收
             */
            //得到第一个参数
            channel.basicAck(deliveryTag, true);
        }catch(Exception e){
            //发生异常的时候就拒接签收
            /**
             * basicNack(long deliveryTag, boolean multiple, boolean requeue)
             * 参数3 是否重回队列  如果设置为true,则消息重新回到queue,broker会重新发送该消息给消费端
             */
            channel.basicNack(deliveryTag,true,false);
        }
    }
} 
总结:

1. 死信交换机和死信队列和普通的没有区别

2. 当消息成为死信后,如果该队列绑定了死信交换机,则消息会被死信交换机重新路由到死信队列

3. 消息成为死信的三种情况:

1. 队列消息长度到达限制;

2. 消费者拒接消费消息,并且不重回队列;

3. 原队列存在消息过期设置,消息到达超时时间未被消费;

1.6 延迟队列

延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。

需求:

  • 1. 下单后,30分钟未支付,取消订单,回滚库存。
  • 2. 新用户注册成功7天后,发送短信问候。

实现方式:

1. 定时器

2. 死信队列

在RabbitMQ中并未提供延迟队列功能。但是可以使用:TTL+死信队列 组合实现延迟队列的效果。

代码

配置文件

配置类:
package yyl.delayconfig;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
 延迟队列:
 1. 定义正常交换机(normal_exchange)和队列(normal_queue) 订单
 2. 定义死信交换机(dead_exchange)和队列(dead_queue)
 3. 绑定,设置正常队列过期时间为30分钟
 */
@Configuration
public class DelayRabbitMqConfig {
    //1.正常交换机的名字
    public static final String NORMAL_EXCHANGE_NAME="normal_exchange";
    //2.正常队列名字
    public static final String NORMAL_QUEUE_NAME="normal_queue";
    //1.死信交换机的名字
    public static final String DEAD_EXCHANGE_NAME="dead_exchange";
    //2.死信队列名字
    public static final String DEAD_QUEUE_NAME="dead_queue";
    //1.正常交换机
    @Bean("normal_exchange")
    public Exchange getNormalExcahnge(){
        return ExchangeBuilder.topicExchange(NORMAL_EXCHANGE_NAME).durable(true).build();
    }
    @Bean("dead_exchange")
    public Exchange getDeadExcahnge(){
        return ExchangeBuilder.topicExchange(DEAD_EXCHANGE_NAME).durable(true).build();
    }
    //正常队列
    @Bean("normal_queue")
    public Queue getNormalQueue(){
        //设置队列的过期时间 5s
        Queue queue=QueueBuilder.durable(NORMAL_QUEUE_NAME)
                .withArgument("x-message-ttl",10000)
                //最大的长度   10 超过10 条就会成为死信消息
               // .withArgument("x-max-length",10)
                //正常队列的死信消息 绑定对应的死信交换机
                .withArgument("x-dead-letter-exchange",DEAD_EXCHANGE_NAME)
                //发送信息时携带的routingkey
                .withArgument("x-dead-letter-routing-key","dlx.order.cancel")
                .build();
        return queue;
        //return  QueueBuilder.durable(NORMAL_QUEUE_NAME).withArgument("x-message-ttl",30000).build();
    }
     //死信队列
    @Bean("dead_queue")
    public Queue getDeadQueue(){
        return  QueueBuilder.durable(DEAD_QUEUE_NAME).build();
    }
    //正常队列和交换机进行绑定
    @Bean
    public Binding bindNormalExchangeAndQueue(@Qualifier("normal_exchange") Exchange exchange,@Qualifier("normal_queue") Queue queue){
        return BindingBuilder.bind(queue).to(exchange).with("order.*").noargs();
    }
    //死信队列和交换机
    @Bean
    public Binding bindDeadExchangeAndQueue(@Qualifier("dead_exchange") Exchange exchange,@Qualifier("dead_queue") Queue queue){
        return BindingBuilder.bind(queue).to(exchange).with("dlx.order.*").noargs();
    }
}

测试类:(发布消息)
@Test
public void testDelay(){
//发送的是订单的消息
    rabbitTemplate.convertAndSend(DelayRabbitMqConfig.NORMAL_EXCHANGE_NAME,"order.msg","订单信息:id=1,time="+new Date());
}
消费类:
package yyl.listener;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;
@Component
public class DelayListener implements ChannelAwareMessageListener {
    @Override
    @RabbitListener(queues = "dead_queue")
    public void onMessage(Message message, Channel channel) throws Exception {
        //Thread.sleep(2000);
        //1.接收消息
        System.out.println(new String(message.getBody()));
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            //2.处理业务luoji
            System.out.println("开始处理业务逻辑");
            System.out.println("根据订单的id查询订单的状态");
            System.out.println("判断订单是否已经支付成功");
            System.out.println("如果取消订单就回滚库存");
            //确认消息
            channel.basicAck(deliveryTag, true);
        }catch(Exception e){

            //发生异常的时候就拒接签收
            /**
             * basicNack(long deliveryTag, boolean multiple, boolean requeue)
             * 参数3 是否重回队列  如果设置为true,则消息重新回到queue,broker会重新发送该消息给消费端
             */
            channel.basicNack(deliveryTag,true,false);
        }
    }
}

总结:

1. 延迟队列 指消息进入队列后,可以被延迟一定时间,再进行消费。

2. RabbitMQ没有提供延迟队列功能,但是可以使用 : TTL + DLX 来实现延迟队列效果。

注意:

消费者在监听队列的时候监听的是死信队列

1.7 rabbitmqctl管理和监控

# 查看队列

rabbitmqctl list_queues

# 查看exchanges

rabbitmqctl list_exchanges

# 查看用户

rabbitmqctl list_users

# 查看连接

rabbitmqctl list_connections

# 查看消费者信息

rabbitmqctl list_consumers

# 查看环境变量

rabbitmqctl environment

# 查看未被确认的队列

rabbitmqctl list_queues name messages_unacknowledged

# 查看单个队列的内存使用

rabbitmqctl list_queues name memory

# 查看准备就绪的队列

rabbitmqctl list_queues name messages_ready

2. RabbitMQ 应用问题

2.1. 消息可靠性保障

需求:100%确保消息发送成功

•消息补偿机制

2发送正常消息,3过会再发一条相同的消息

2发送的消息在Q1中被正常消费到写入DB,发送ack给Q2。回调检查服务监听到Q2的消息,将消息写入MDB

如果1成功2失败,因为3页发送了消息放入Q3。此时回调检查服务也监听到了Q3,要去比对MDB是否一致,如果一致则代表消费过。如果MDB中不存在,就代表2失败了,就走8让生产者重新发。

如果2个都发送失败了,有MDB的定时检查服务,比对业务数据库DB与消息数据库MDB,就能发现差异

2.2. 消息幂等性保障

幂等性指一次和多次请求某一个资源,对于资源本身应该具有同样的结果。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。在MQ中指,消费多条相同的消息,得到与消费该消息一次相同的结果

数据库

面试题:

1.如何保证消息按顺序执行

出现顺序错乱的场景

(1)rabbitmq

①一个queue,有多个consumer去消费,这样就会造成顺序的错误,consumer从MQ里面读取数据是有序的,但是每个consumer的执行时间是不固定的,无法保证先读到消息的consumer一定先完成操作,这样就会出现消息并没有按照顺序执行,造成数据顺序错误。

②一个queue对应一个consumer,但是consumer里面进行了多线程消费,这样也会造成消息消费顺序错误。

保证消息的消费顺序

1)rabbitmq

①拆分多个queue,每个queue一个consumer,就是多一些queue而已,确实是麻烦点;这样也会造成吞吐量下降,可以在消费者内部采用多线程的方式取消费。

②或者就一个queue但是对应一个consumer,然后这个consumer内部用内存队列做排队,然后分发给底层不同的worker来处理

2.设计一个秒杀活动

秒杀活动的特点:

同时并发量大

秒杀时会有大量用户在同一时间进行抢购,瞬时并发访问量突增 10 倍,甚至 100 倍以上都有。

库存量少

一般秒杀活动商品量很少,这就导致了只有极少量用户能成功购买到。

业务简单

流程比较简单,一般都是下订单、扣库存、支付订单

技术难点

有业务的冲击

秒杀是营销活动中的一种,如果和其他营销活动应用部署在同一服务器上,肯定会对现有其他活动造成冲击,极端情况下可能导致整个电商系统服务宕机

直接下订单

下单页面是一个正常的 URL 地址,需要控制在秒杀开始前,不能下订单,只能浏览对应活动商品的信息。简单来说,需要 Disable 订单按钮

页面流量突增

秒杀活动开始前后,会有很多用户请求对应商品页面,会造成后台服务器的流量突增,同时对应的网络带宽增加,需要控制商品页面的流量不会对后台服务器、DB、Redis 等组件的造成过大的压力

架构设计思想
限流

由于活动库存量一般都是很少,对应的只有少部分用户才能秒杀成功。所以我们需要限制大部分用户流量,只准少量用户流量进入后端服务器

削峰

秒杀开始的那一瞬间,会有大量用户冲击进来,所以在开始时候会有一个瞬间流量峰值。如何把瞬间的流量峰值变得更平缓,是能否成功设计好秒杀系统的关键因素。实现流量削峰填谷,一般的采用缓存和 MQ 中间件来解决

异步

秒杀其实可以当做高并发系统来处理,在这个时候,可以考虑从业务上做兼容,将同步的业务,设计成异步处理的任务,提高网站的整体可用性

缓存

秒杀系统的瓶颈主要体现在下订单、扣减库存流程中。在这些流程中主要用到 OLTP 的数据库,类似 MySQL、SQLServer、Oracle。由于数据库底层采用 B+ 树的储存结构,对应我们随机写入与读取的效率,相对较低。如果我们把部分业务逻辑迁移到内存的缓存或者 Redis 中,会极大的提高并发效率

整体架构

秒杀整体流程图

秒杀服务层和后端

秒杀业务的核心是库存处理,用户购买成功后会进行减库存操作,并记录购买明细。当秒杀开始时,大量用户同时发起请求,这是一个并行操作,多条更新库存数量的SQL语句会同时竞争秒杀商品所处数据库表里的那行数据,导致库存的减少数量与购买明细的增加数量不一致,因此,我们使用RabbitMQ进行削峰限流并且将请求数据串行处理

削峰填谷加延迟加载

秒杀450件商品

设置每次最大拉取100个人,

@1.查询剩余的秒杀商品的个数

@2.用拉取数跟剩余的秒杀商品的个数进行比较如果小于就直接拉取100个,如果大于就将拉取的值改为剩余的商品的数量,拉取成功并保证是同一个人的下单之后并将其添加到订单表中,库存的数量减少。

@3.设置超时时间为30分钟,当30分钟之后还没有付款的取消订单,然后将对应的库存补上

  • 7
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值