RabbitMQ第二讲

文章详细介绍了RabbitMQ中确保消息可靠性的几种策略,包括消息确认模式(确认与回退)、事务机制以及消费端限流。还讨论了TTL(消息存活时间)和死信队列的概念,以及如何设置和测试延迟队列。此外,文章还提到了RabbitMQ管理命令和面试中可能遇到的问题,如消息的顺序执行和秒杀活动的实现。
摘要由CSDN通过智能技术生成

目录

二、RabbitMQ-02

2.1 消息的可靠性投递(Procedure)

2.1.1 消息投递介绍

2.1.2 确认模式

2.1.3 回退模式

2.2 事务机制

2.3 确认方式(Consumer)

2.3.1 确认方式介绍

2.3.2 自动确认

2.3.3 手动确认

2.4 消费端限流

2.4.1 代码实现

2.5 TTL(存活时间)

2.5.1 可视化

2.5.2 代码实现

2.5.3 总结

2.6 死信队列

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

2.6.2 代码实现

2.6.3 总结

2.7 延迟队列

2.7.1 代码实现

2.7.2 总结

2.8 rabbitmqctl 管理和监控

2.9 RabbitMQ应用问题

2.9.1 消息可靠性保障

2.9.2 消息幂等性保障

2.10 RabbitMQ面试题

2.10.1 保证消息按顺序执行

2.10.2 设置秒杀活动


二、RabbitMQ-02

2.1 消息的可靠性投递(Procedure)

2.1.1 消息投递介绍

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

• confirm 确认模式
• return 退回模式
​
rabbitmq 整个消息投递的路径为:
producer —> rabbitmq-broker —> exchange —> queue —> consumer
​
生产:消息从 producer 到 exchange 则会返回一个 confirmCallback()。都会执行,返回false就失败
内部:消息从 exchange–>queue 投递失败则会返回一个 returnCallback()。
我们将利用这两个 callback 控制消息的可靠性投递

2.1.2 确认模式

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

1、配置文件(application.properties)

#主机的IP地址:默认为localhost
spring.rabbitmq.host=192.168.111.127
#虚拟主机的名字:默认为/
spring.rabbitmq.virtual-host=/lwl
#登录的用户名:默认为guest
spring.rabbitmq.username=lwl
#登录的密码:默认为guest
spring.rabbitmq.password=密码
#RabbitMQ的端口号:默认为5672
spring.rabbitmq.port=5672
#设置开启确认模式
spring.rabbitmq.publisher-confirms=true

2、配置类

package com.aaa.procedure.test;
​
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:声明为配置类,可以更早加载
 */
@Configuration
public class ProcedureConfigConfirm {
    //声明队列的名字
    public static final String ROUTING_QUEUE_NAME1 = "confirm_queue_name1";
    public static final String ROUTING_QUEUE_NAME2 = "confirm_queue_name2";
​
    //声明一个交换机的名字
    public static final String ROUTING_EXCHANGE_NAME = "confirm_exchange_name";
​
    /**
     * 声明一个交换机并作为bean注入到spring容器中
     * durable默认为durable
     * autoDelete默认为true
     * 交换机起名字,在绑定队列时使用
     */
    @Bean(name = "ex")
    public Exchange getExchange(){
        //     交换机构建器.交换机类型(交换机名字).是否持久化.是否自动删除.构建
        return ExchangeBuilder.directExchange(ROUTING_EXCHANGE_NAME).durable(false).autoDelete().build();
    }
​
    /**
     * 声明一个队列并作为bean注入到spring容器中
     * autoDelete默认为true
     * 队列起名字,在绑定交换机时使用
     */
    @Bean(name = "qu1")
    public Queue getQueue1(){
        //      队列构建器.不持久化(队列名字).自动删除.构建
        return QueueBuilder.nonDurable(ROUTING_QUEUE_NAME1).autoDelete().build();
    }
    @Bean(name = "qu2")
    public Queue getQueue2(){
        //      队列构建器.不持久化(队列名字).自动删除.构建
        return QueueBuilder.nonDurable(ROUTING_QUEUE_NAME2).autoDelete().build();
    }
​
​
    /**
     * 绑定队列和交换机后作为bean注入到容器中
     * @Qualifier注解:为了标明对应的交换机和队列
     */
    @Bean
    public Binding bingQu1AndEx(@Qualifier("ex") Exchange exchange,@Qualifier("qu1") Queue queue){
        //  绑定构建器.队列名字.交换机名字.routingkey.参数
        return BindingBuilder.bind(queue).to(exchange).with("error").noargs();
    }
    @Bean
    public Binding bingQu2AndEx(@Qualifier("ex") Exchange exchange,@Qualifier("qu2") Queue queue){
        //  绑定构建器.队列名字.交换机名字.routingkey.参数
        return BindingBuilder.bind(queue).to(exchange).with("warnings").noargs();
    }
}

3、测试类

package com.aaa.procedure;
​
import com.aaa.procedure.test.ProcedureConfigConfirm;
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.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
​
import javax.annotation.Resource;
​
/**
 * @SpringBootTest:声明为测试类
 * 因为SpringBoot的版本较低,所以还需要添加这两个注解来确保项目没有问题
 * @RunWith(SpringRunner.class)
 * @ContextConfiguration(classes = {ProcedureRabbitTest.class})
 */
@SpringBootTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = ProcedureRabbitTest.class)
public class TestPublish {
​
    //将操作mq的对象引入进来,是通过jar包amqp进行封装的对象
    @Resource
    private RabbitTemplate rabbitTemplate;
​
    /**
     * 在进行测试时,生产者队列中不能有两个相同的bean,否则项目将不会正常启动比如,@Bean(name="ex")不能存在两个
     */
    @Test
    public void testConfirm(){
        //在rabbitTemplate中设置信息的确认
        //这里的ConfirmCallback()接口,是一个函数式接口,接口中只有一个方法,可以转换成lambda表达式的形式
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            /**
             * (CorrelationData correlationData, boolean b, String s)
             * @param correlationData  消息相关数据
             * @param b  是否接收成功
             * @param s  失败的原因
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean b, String s) {
                if (b){
                    System.out.println("接收成功");
                }else {
                    System.out.println("接收失败的原因是:"+s);
                }
            }
        });
​
        /**
         * 使用convertAndSend发送消息
         * (String exchange, String routingKey, Object object)
         * exchange:交换机的名字
         * routingKey:定向路由
         * object:消息的内容
         */
        rabbitTemplate.convertAndSend(ProcedureConfigConfirm.ROUTING_EXCHANGE_NAME,"warnings","确认模式");
    }
}
​
控制台输出  “接收成功”  代表项目成功且消息传递到MQ了

总结

设置publisher-confirms="true" 开启 确认模式。
使用rabbitTemplate.setConfirmCallback设置回调函数。当消息发送到exchange后回调confirm方法。在方法中判断ack,如果为true,则发送成功,如果为false,则发送失败,需要处理。

2.1.3 回退模式

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

1、配置文件

#主机的IP地址:默认为localhost
spring.rabbitmq.host=192.168.111.127
#虚拟主机的名字:默认为/
spring.rabbitmq.virtual-host=/lwl
#登录的用户名:默认为guest
spring.rabbitmq.username=lwl
#登录的密码:默认为guest
spring.rabbitmq.password=密码
#RabbitMQ的端口号:默认为5672
spring.rabbitmq.port=5672
#设置回退模式
spring.rabbitmq.publisher-returns=true

2、配置类

package com.aaa.procedure.test;
​
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:声明为配置类,可以更早加载
 */
@Configuration
public class ProcedureConfigReturn {
    //声明队列的名字
    public static final String ROUTING_QUEUE_NAME1 = "return_queue_name1";
    public static final String ROUTING_QUEUE_NAME2 = "return_queue_name2";
​
    //声明一个交换机的名字
    public static final String ROUTING_EXCHANGE_NAME = "return_exchange_name";
​
    /**
     * 声明一个交换机并作为bean注入到spring容器中
     * durable默认为durable
     * autoDelete默认为true
     * 交换机起名字,在绑定队列时使用
     */
    @Bean(name = "ex")
    public Exchange getExchange(){
        //     交换机构建器.交换机类型(交换机名字).是否持久化.是否自动删除.构建
        return ExchangeBuilder.directExchange(ROUTING_EXCHANGE_NAME).durable(false).autoDelete().build();
    }
​
    /**
     * 声明一个队列并作为bean注入到spring容器中
     * autoDelete默认为true
     * 队列起名字,在绑定交换机时使用
     */
    @Bean(name = "qu1")
    public Queue getQueue1(){
        //      队列构建器.不持久化(队列名字).自动删除.构建
        return QueueBuilder.nonDurable(ROUTING_QUEUE_NAME1).autoDelete().build();
    }
    @Bean(name = "qu2")
    public Queue getQueue2(){
        //      队列构建器.不持久化(队列名字).自动删除.构建
        return QueueBuilder.nonDurable(ROUTING_QUEUE_NAME2).autoDelete().build();
    }
​
​
    /**
     * 绑定队列和交换机后作为bean注入到容器中
     * @Qualifier注解:为了标明对应的交换机和队列
     */
    @Bean
    public Binding bingQu1AndEx(@Qualifier("ex") Exchange exchange,@Qualifier("qu1") Queue queue){
        //  绑定构建器.队列名字.交换机名字.routingkey.参数
        return BindingBuilder.bind(queue).to(exchange).with("error").noargs();
    }
    @Bean
    public Binding bingQu2AndEx(@Qualifier("ex") Exchange exchange,@Qualifier("qu2") Queue queue){
        //  绑定构建器.队列名字.交换机名字.routingkey.参数
        return BindingBuilder.bind(queue).to(exchange).with("warnings").noargs();
    }
}

3、测试类

package com.aaa.procedure;
​
import com.aaa.procedure.test.ProcedureConfigReturn;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
​
import javax.annotation.Resource;
​
/**
 * @SpringBootTest:声明为测试类
 * 因为SpringBoot的版本较低,所以还需要添加这两个注解来确保项目没有问题
 * @RunWith(SpringRunner.class)
 * @ContextConfiguration(classes = {ProcedureRabbitTest.class})
 */
@SpringBootTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = ProcedureRabbitTest.class)
public class TestPublish {
​
    //将操作mq的对象引入进来,是通过jar包amqp进行封装的对象
    @Resource
    private RabbitTemplate rabbitTemplate;
​
    /**
     * 在进行测试时,生产者队列中不能有两个相同的bean,否则项目将不会正常启动比如,@Bean(name="ex")不能存在两个
     */
    @Test
    public void testReturn(){
        //设置交换机处理消息失败的模式
        rabbitTemplate.setMandatory(true);
​
        //这里的ReturnCallback()接口,是一个函数式接口,接口中只有一个方法,可以转换成lambda表达式的形式
        // 如果注释写在函数式接口里面,是不能转成lambda表达式的
        /**
         * 下面对应的参数
         * (Message message, int replyCode, String replyText, String exchange, String routingKey)
         */
        rabbitTemplate.setReturnCallback((message, i, s, s1, s2) -> System.out.println("routingKey:"+s2));
        /**
         * 使用convertAndSend发送消息
         * (String exchange, String routingKey, Object object)
         * exchange:交换机的名字
         * routingKey:定向路由
         * object:消息的内容
         */
        rabbitTemplate.convertAndSend(ProcedureConfigReturn.ROUTING_EXCHANGE_NAME,"warnings","回退模式");
    }
}

总结

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

2.2 事务机制

在RabbitMQ中也提供了事务机制,但是性能较差。
​
使用channel下列方法,完成事务控制:
    txSelect() :用于将当前channel设置成transaction模式
    txCommit():用于提交事务
    txRollback():用于回滚事务

2.3 确认方式(Consumer)

2.3.1 确认方式介绍

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

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

有三种确认方式
​
    1、自动确认:acknowledge="none" 。不管处理成功与否,业务处理异常也不管(当消费者意担接收到消息之后,消费者就会给broker一个回执,证明已经接收到消息了,不管消息到底是否成功)
    
   2、    手动确认:acknowledge="manual" 。可以解决业务异常的情况(收到消息之后不会立马确认收到消息,当业务处理没有问题的时候手动的调用代码的方   式来进行处理,如果业务失败了,就可以进行额外的操作)
   
   3、 根据异常情况确认:acknowledge="auto",(这种方式使用麻烦,不作讲解)
   
​
其中自动确认是指,当消息一旦被Consumer接收到,则自动确认收到,并将相应 message 从 RabbitMQ 的消息缓存中移除。但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失。如果设置了手动确认方式,则需要在业务处理成功后,调用channel.basicAck(),手动签收,如果出现异常,则调用channel.basicNack()方法,让其自动重新发送消息。

2.3.2 自动确认

1、配置类

#主机的IP地址:默认为localhost
spring.rabbitmq.host=192.168.111.127
#虚拟主机的名字:默认为/
spring.rabbitmq.virtual-host=/lwl
#登录的用户名:默认为guest
spring.rabbitmq.username=lwl
#登录的密码:默认为guest
spring.rabbitmq.password=密码
#RabbitMQ的端口号:默认为5672
spring.rabbitmq.port=5672

2、监听类

/**
 * @Component:将该类作为注解bean注入
 * @RabbitListener:标注监听队列的名字
 */
@Component
public class ConsumerListener {
​
    //监听队列里面的消息:可以同时监听很多个队列
    @RabbitListener(queues = {"routing_queue_name1","routing_queue_name2"})
    public void listenerQueues(Message message){
        //取出队列里面的消息(bytes类型),并将其转换为String类型
        String msg = new String(message.getBody());
        System.out.println("监听到的消息为:"+msg);
    }
}

2.3.3 手动确认

1、配置类

#主机的IP地址:默认为localhost
spring.rabbitmq.host=192.168.111.127
#虚拟主机的名字:默认为/
spring.rabbitmq.virtual-host=/lwl
#登录的用户名:默认为guest
spring.rabbitmq.username=lwl
#登录的密码:默认为guest
spring.rabbitmq.password=密码
#RabbitMQ的端口号:默认为5672
spring.rabbitmq.port=5672
#设置手动确认
spring.rabbitmq.listener.simple.acknowledge-mode=manual

2、监听类

package com.aaa.consumer.test;
​
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;
​
import java.io.IOException;
​
/**
 * @Component:将该类作为注解bean注入
 * @RabbitListener:标注监听队列的名字
 *  1. 设置手动签收。acknowledge="manual"
 *  2. 让监听器类实现ChannelAwareMessageListener接口
 *  3. 如果消息成功处理,则调用channel的 basicAck()签收
 *  4. 如果消息处理失败,则调用channel的basicNack()拒绝签收,broker重新发送给consumer
 */
@Component
public class ConsumerListenerAck implements ChannelAwareMessageListener {
​
    @Override
    @RabbitListener(queues = "return_queue_name2")
    public void onMessage(Message message, Channel channel) throws Exception {
​
        //模拟处理业务逻辑
        Thread.sleep(2000);
​
        //获取消息
        String msg = new String(message.getBody());
        System.out.println("msg = " + msg);
        //获取标识信息的id
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
​
        try {
            /** 确认消息
             * basicAck(long deliveryTag, boolean multiple)
             * deliveryTag:用来标识消息的id
             * multiple:是否批量。true:将一次性 ACK 所有小于 deliveryTag 的消息
             */
            int a = 2/0;
            System.out.println("确认接收消息");
            channel.basicAck(deliveryTag, false);
            //这里捕获的异常需要是Exception,如果只捕获IOException,会报异常,然后关闭
        } catch (Exception e) {
            /** 不确认消息
             * basicNack(long deliveryTag, boolean multiple, boolean requeue)
             * deliveryTag:用来标识消息的id
             * multiple:是否批量不签收
             * requeue:是否重新回到队列,不确认的消息就会变为Unacked,
             *          如果这里设置为true,那么消息会回到队列,重新在进行发送
             *          如果这里设置为false,那么消息不会回到队列
             */
            System.out.println("消息重新回到队列里面");
            channel.basicNack(deliveryTag,true,true);
        }
    }
}
设置手动模式需要在配置文件里面添加 acknowlwdge-mode:manual
如果消费端没有异常就会走basicAck的方法,确认签收消息,如果有异常就会调用basicNack的方法拒接签收消息,让MQ重新发送消息

2.4 消费端限流

消费端限流是为了避免请求瞬间增多的情况
​
①消费端的确认模式一定为手动确认。acknowledge="manual"
②在配置 prefetch属性设置消费端一次拉取多少消息,如果没有设置限流,会直接全部拉取消息,但是并不消费

 

2.4.1 代码实现

1、配置类

RabbitMQ的配置参考可靠性投递的配置类即可
​
#设置为手动确认模式
spring.rabbitmq.listener.simple.acknowledge-mode=manual
#每次拉取的条数,每次拉取一条
#如果没有设置限流,会直接全部拉取消息,但是并不消费
spring.rabbitmq.listener.simple.prefetch=1

2、监听类

package com.aaa.consumer.test;
​
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:将该类作为注解bean注入
 * @RabbitListener:标注监听队列的名字
 */
@Component
public class ConsumerListenerPrefetch implements ChannelAwareMessageListener {
​
    @Override
    @RabbitListener(queues = "return_queue_name2")
    public void onMessage(Message message, Channel channel) throws Exception {
​
        //模拟处理业务逻辑
        Thread.sleep(6000);
​
        //获取消息
        String msg = new String(message.getBody());
        System.out.println("msg = " + msg);
        //获取标识信息的id
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
​
        try {
            /** 确认消息
             * basicAck(long deliveryTag, boolean multiple)
             * deliveryTag:用来标识消息的id
             * multiple:是否批量。true:将一次性 ACK 所有小于 deliveryTag 的消息
             */
            int a = 2/0;
            System.out.println("确认接收消息");
            channel.basicAck(deliveryTag, false);
            //这里捕获的异常需要是Exception,如果只捕获IOException,会报异常,然后关闭
        } catch (Exception e) {
            /** 不确认消息
             * basicNack(long deliveryTag, boolean multiple, boolean requeue)
             * deliveryTag:用来标识消息的id
             * multiple:是否批量不签收
             * requeue:是否重新回到队列,不确认的消息就会变为Unacked,
             *          如果这里设置为true,那么消息会回到队列,重新在进行发送
             *          如果这里设置为false,那么消息不会回到队列
             */
            channel.basicNack(deliveryTag,true,false);
        }
    }
}
//没有手动的确认接收消息 所以就会显示有一条未被确认消息
//每次都会拉取一条信息进行消费,错误后不回到队列,会被丢弃

 

 

2.5 TTL(存活时间)

全称 Time To Live(存活时间/过期时间)。
​
当消息到达存活时间后,还没有被消费,会被自动清除。
​
RabbitMQ可以对消息设置过期时间,也可以对整个队列(Queue)设置过期时间。

2.5.1 可视化

使用可视化客户端创建队列时添加ttl参数

 

2.5.2 代码实现

1、配置类文件

RabbitMQ的配置参考可靠性投递的配置类

2、配置类

package com.aaa.procedure.test;
​
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:声明为配置类,可以更早加载
 * 这里测试只使用一个队列进行测试
 */
@Configuration
public class ProcedureConfigTTL {
    //声明队列的名字
    public static final String ROUTING_QUEUE_NAME1 = "ttl_queue_name";
​
    //声明一个交换机的名字
    public static final String ROUTING_EXCHANGE_NAME = "ttl_exchange_name";
​
    /**
     * 声明一个交换机并作为bean注入到spring容器中
     * durable默认为durable
     * autoDelete默认为true
     * 交换机起名字,在绑定队列时使用
     */
    @Bean(name = "ex")
    public Exchange getExchange(){
        //     交换机构建器.交换机类型(交换机名字).是否持久化.是否自动删除.构建
        return ExchangeBuilder.directExchange(ROUTING_EXCHANGE_NAME).durable(false).autoDelete().build();
    }
​
    /**
     * 声明一个队列并作为bean注入到spring容器中
     * 声明队列的时候添加参数
     * withArgument("x-message-ttl",10000):此队列的生存周期为5秒
     */
    @Bean(name = "qu1")
    public Queue getQueue1(){
        //      队列构建器.不持久化(队列名字).自动删除.构建
        return QueueBuilder.nonDurable(ROUTING_QUEUE_NAME1).autoDelete().withArgument("x-message-ttl",10000).build();
    }
​
    /**
     * 绑定队列和交换机后作为bean注入到容器中
     * @Qualifier注解:为了标明对应的交换机和队列
     */
    @Bean
    public Binding bingQu1AndEx(@Qualifier("ex") Exchange exchange,@Qualifier("qu1") Queue queue){
        //  绑定构建器.队列名字.交换机名字.routingkey.参数
        return BindingBuilder.bind(queue).to(exchange).with("ttl").noargs();
    }
}

3、测试类

package com.aaa.procedure;
​
import com.aaa.procedure.test.ProcedureConfigTTL;
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.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
​
import javax.annotation.Resource;
​
@SpringBootTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = ProcedureRabbitTest.class)
public class TestT {
​
    @Resource
    private RabbitTemplate rabbitTemplate;
​
    @Test
    public void testTTL(){
                //设置交换机处理消息失败的模式
        rabbitTemplate.setMandatory(true);
​
        //这里的ReturnCallback()接口,是一个函数式接口,接口中只有一个方法,可以转换成lambda表达式的形式
        // 如果注释写在函数式接口里面,是不能转成lambda表达式的
        /**
         * 下面对应的参数
         * (Message message, int replyCode, String replyText, String exchange, String routingKey)
         */
        rabbitTemplate.setReturnCallback((message, i, s, s1, s2) -> System.out.println("routingKey:"+s2));
        /**
         * 使用convertAndSend发送消息
         * (String exchange, String routingKey, Object object)
         * exchange:交换机的名字
         * routingKey:定向路由
         * object:消息的内容
         */
        for (int i = 0; i < 10; i++) {
            rabbitTemplate.convertAndSend(ProcedureConfigTTL.ROUTING_EXCHANGE_NAME,"ttl","测试队列生存周期");
        }
    }
}
​
会生成十条消息在队列中,在10秒钟生命周期到期时,十条消息都会直接丢掉

如果要测试消息的存活时间,可以使用消息后处理对象,只需要更改测试类代码即可

package com.aaa.procedure;
​
import com.aaa.procedure.test.ProcedureConfigTTL;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
​
import javax.annotation.Resource;
​
@SpringBootTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = ProcedureRabbitTest.class)
public class TestT {
​
    @Resource
    private RabbitTemplate rabbitTemplate;
​
    @Test
    public void testTTL(){
        //消息后处理对象,设置一些消息的参数信息
        MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                //设置message的信息,设置了过期时间
                message.getMessageProperties().setExpiration("7000");
                return message;
            }
        };
        //设置交换机处理消息失败的模式
        rabbitTemplate.setMandatory(true);
​
        //这里的ReturnCallback()接口,是一个函数式接口,接口中只有一个方法,可以转换成lambda表达式的形式
        // 如果注释写在函数式接口里面,是不能转成lambda表达式
        /**
         * 下面对应的参数
         * (Message message, int replyCode, String replyText, String exchange, String routingKey)
         */
        rabbitTemplate.setReturnCallback((message, i, s, s1, s2) -> System.out.println("routingKey:" + s2));
        /**
         * 使用convertAndSend发送消息
         * (String exchange, String routingKey, Object object)
         * exchange:交换机的名字
         * routingKey:定向路由
         * object:消息的内容
         */
        for (int i = 0; i < 10; i++) {
            rabbitTemplate.convertAndSend(ProcedureConfigTTL.ROUTING_EXCHANGE_NAME,"ttl","测试队列中消息的生存周期");
        }
    }
}

2.5.3 总结

上面队列设置的过期时间为10秒,而消息的过期时间设置的是7秒,所以消息会在7秒之后就过期。
    当既设置队列的过期时间,也设置消息的过期时间时,哪个时间短就按照哪个时间来执行

2.6 死信队列

死信队列,英文缩写:DLX 。

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

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

 

队列绑定交换机的方式是什么?
给队列设置参数: x-dead-letter-exchange 和 x-dead-letter-routing-key
​
1.交换机:正常交换机   死信交换机
2.队列:正常队列  死信队列
3.绑定   正常交换机 - 正常队列
        死信交换机 - 正常队列
        死信交换机 - 死信队列

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

1.队列消息长度到最大的限制
最大的长度设置为10当第11条消息进来的时候就会成为死信
2. 消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false(不重新回到队列中)
设置消费者为手动签收的状态 
3. 原队列存在消息过期设置,消息到达超时时间未被消费;

2.6.2 代码实现

1、配置类文件

不需要配置额外的信息
RabbitMQ的配置参考可靠性投递的配置类

2、配置类

package com.aaa.procedure.test;
​
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 ProcedureDLX {
    //正常交换机的名字
    private static final String NORMAL_EXCHANGE_NAME = "normal_exchange";
    //死信交换机的名字
    private static final String DEAD_EXCHANGE_NAME = "dead_exchange";
    //正常队列的名字
    private static final String NORMAL_QUEUE_NAME = "normal_queue";
    //死信队列的名字
    private static final String DEAD_QUEUE_NAME = "dead_queue";
​
    //声明正常交换机
    @Bean(name = "normalexchange")
    public Exchange getNormalExchange(){
        return ExchangeBuilder.directExchange(NORMAL_EXCHANGE_NAME).durable(false).build();
    }
​
    //声明死信交换机
    @Bean(name = "deadexchange")
    public Exchange getDeadExchange(){
        return ExchangeBuilder.directExchange(DEAD_EXCHANGE_NAME).durable(false).build();
    }
​
    //声明正常队列:顺便绑定对应的死信队列
    @Bean(name = "normalqueue")
    public Queue getNormalQueue(){
        return QueueBuilder.nonDurable(NORMAL_QUEUE_NAME)
                //队列的过期时间设置为30秒
                .withArgument("x-message-ttl",30000)
                //最大的长度为6,超过6,后面的消息就会成为死信消息
                .withArgument("x-max-length",6)
                //绑定死信交换机的名字
                .withArgument("x-dead-letter-exchange",DEAD_EXCHANGE_NAME)
                //和死信交换机绑定的路由名称一致
                .withArgument("x-dead-letter-routing-key","deadmsg")
                .build();
    }
​
    //声明死信队列
    @Bean(name = "deadqueue")
    public Queue getDeadQueue(){
        return QueueBuilder.nonDurable(DEAD_QUEUE_NAME).build();
    }
​
    //正常交换机和正常队列进行绑定
    public Binding norExAndNorQu(@Qualifier("normalexchange")Exchange exchange,@Qualifier("normalqueue")Queue queue){
        return BindingBuilder.bind(queue).to(exchange).with("normalmsg").noargs();
    }
​
    //死信交换机和死信队列绑定
    @Bean
    public Binding deadExAndDeadQu(@Qualifier("deadexchange")Exchange exchange,@Qualifier("deadqueue")Queue queue){
        return BindingBuilder.bind(queue).to(exchange).with("deadmsg").noargs();
    }
}

①消息长度过长和消息时间过期

package com.aaa.procedure;
​
import com.aaa.procedure.test.ProcedureDLX;
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.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
​
import javax.annotation.Resource;
​
@SpringBootTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = ProcedureRabbitTest.class)
public class TestDLX {
    @Resource
    private RabbitTemplate rabbitTemplate;
​
    @Test
    public void testTTL(){
        //设置交换机处理消息失败的模式
        rabbitTemplate.setMandatory(true);
​
        /**
         * 下面对应的参数
         * (Message message, int replyCode, String replyText, String exchange, String routingKey)
         */
        rabbitTemplate.setReturnCallback((message, i, s, s1, s2) -> System.out.println("routingKey:" + s2));
        /**
         * 使用convertAndSend发送消息
         * (String exchange, String routingKey, Object object)
         * exchange:交换机的名字
         * routingKey:定向路由
         * object:消息的内容
         */
        for (int i = 0; i < 10; i++) {
            rabbitTemplate.convertAndSend(ProcedureDLX.NORMAL_EXCHANGE_NAME,"normalmsg","测试死信队列的消息"+i);
        }
    }
}
​
发布了十条消息,消息队列的最大长度是6,所以队列中只会有6条消息,剩余的消息会变成死信消息
    因为消息队列有先进先出的性质,所以只有最后六条消息留在消息队列中

②拒绝接收消息

package com.aaa.consumer.test;
​
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 ConsumerListenerDLX implements ChannelAwareMessageListener {
​
    @Override
    @RabbitListener(queues = "normal_queue")
    public void onMessage(Message message, Channel channel) throws Exception {
​
        //模拟处理业务逻辑
        Thread.sleep(6000);
​
        //获取消息
        String msg = new String(message.getBody());
        System.out.println("msg = " + msg);
        //获取标识信息的id
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
​
        try {
            /** 确认消息
             * basicAck(long deliveryTag, boolean multiple)
             * deliveryTag:用来标识消息的id
             * multiple:是否批量。true:将一次性 ACK 所有小于 deliveryTag 的消息
             */
            int a = 2/0;
            System.out.println("确认接收消息");
            channel.basicAck(deliveryTag, false);
            //这里捕获的异常需要是Exception,如果只捕获IOException,会报异常,然后关闭
        } catch (Exception e) {
            /** 不确认消息
             * basicNack(long deliveryTag, boolean multiple, boolean requeue)
             * deliveryTag:用来标识消息的id
             * multiple:是否批量不签收
             * requeue:是否重新回到队列,不确认的消息就会变为Unacked,
             *          如果这里设置为true,那么消息会回到队列,重新在进行发送
             *          如果这里设置为false,那么消息不会回到队列
             */
            channel.basicNack(deliveryTag,true,false);
        }
    }
}

2.6.3 总结

1. 死信交换机和死信队列和普通的没有区别
2. 当消息成为死信后,如果该队列绑定了死信交换机,则消息会被死信交换机重新路由到死信队列
3. 消息成为死信的三种情况:
    ①. 队列消息长度到达限制;
    ②. 消费者拒接消费消息,并且不重回队列;
    ③. 原队列存在消息过期设置,消息到达超时时间未被消费;

2.7 延迟队列

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

需求:
• 1. 下单后,30分钟未支付,取消订单,回滚库存。
• 2. 新用户注册成功7天后,发送短信问候。
​
实现:
1. 定时器
2. 死信队列
在RabbitMQ中并未提供延迟队列功能。但是可以使用:TTL+死信队列 组合实现延迟队列的效果。 

 

 

2.7.1 代码实现

1、配置类文件

不需要配置额外的信息
RabbitMQ的配置参考可靠性投递的配置类

2、配置类

package com.aaa.procedure.test;
​
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 ProcedureDLX {
    //正常交换机的名字
    public static final String NORMAL_EXCHANGE_NAME = "normal_exchange";
    //死信交换机的名字
    public static final String DEAD_EXCHANGE_NAME = "dead_exchange";
    //正常队列的名字
    public static final String NORMAL_QUEUE_NAME = "normal_queue";
    //死信队列的名字
    public static final String DEAD_QUEUE_NAME = "dead_queue";
​
    //声明正常交换机
    @Bean(name = "normalexchange")
    public Exchange getNormalExchange(){
        return ExchangeBuilder.directExchange(NORMAL_EXCHANGE_NAME).durable(false).build();
    }
​
    //声明死信交换机
    @Bean(name = "deadexchange")
    public Exchange getDeadExchange(){
        return ExchangeBuilder.directExchange(DEAD_EXCHANGE_NAME).durable(false).build();
    }
​
    //声明正常队列:顺便绑定对应的死信队列
    @Bean(name = "normalqueue")
    public Queue getNormalQueue(){
        return QueueBuilder.nonDurable(NORMAL_QUEUE_NAME)
                //队列的过期时间设置为30秒
                .withArgument("x-message-ttl",180000)
                //最大的长度为6,超过6,后面的消息就会成为死信消息
                .withArgument("x-max-length",6)
                //绑定死信交换机的名字
                .withArgument("x-dead-letter-exchange",DEAD_EXCHANGE_NAME)
                //和死信交换机绑定的路由名称一致
                .withArgument("x-dead-letter-routing-key","deadmsg")
                .build();
    }
​
    //声明死信队列
    @Bean(name = "deadqueue")
    public Queue getDeadQueue(){
        return QueueBuilder.nonDurable(DEAD_QUEUE_NAME).build();
    }
​
    //正常交换机和正常队列进行绑定
    @Bean
    public Binding norExAndNorQu(@Qualifier("normalexchange")Exchange exchange,@Qualifier("normalqueue")Queue queue){
        return BindingBuilder.bind(queue).to(exchange).with("normalmsg").noargs();
    }
​
    //死信交换机和死信队列绑定
    @Bean
    public Binding deadExAndDeadQu(@Qualifier("deadexchange")Exchange exchange,@Qualifier("deadqueue")Queue queue){
        return BindingBuilder.bind(queue).to(exchange).with("deadmsg").noargs();
    }
}

3、测试类

package com.aaa.procedure;
​
import com.aaa.procedure.test.ProcedureDLX;
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.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
​
import javax.annotation.Resource;
​
@SpringBootTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = ProcedureRabbitTest.class)
public class TestDLX {
    @Resource
    private RabbitTemplate rabbitTemplate;
​
    @Test
    public void testTTL(){
        //设置交换机处理消息失败的模式
        rabbitTemplate.setMandatory(true);
​
        /**
         * 下面对应的参数
         * (Message message, int replyCode, String replyText, String exchange, String routingKey)
         */
        rabbitTemplate.setReturnCallback((message, i, s, s1, s2) -> System.out.println("routingKey:" + s2));
        /**
         * 使用convertAndSend发送消息
         * (String exchange, String routingKey, Object object)
         * exchange:交换机的名字
         * routingKey:定向路由
         * object:消息的内容
         */
        for (int i = 0; i < 10; i++) {
            rabbitTemplate.convertAndSend(ProcedureDLX.NORMAL_EXCHANGE_NAME,"normalmsg","测试死信队列的消息"+i);
        }
    }
}

4、监听类

package com.aaa.consumer.test;
​
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 ConsumerListenerDLX implements ChannelAwareMessageListener {
​
    @Override
    @RabbitListener(queues = "dead_queue")
    public void onMessage(Message message, Channel channel) throws Exception {
​
        //获取消息
        String msg = new String(message.getBody());
        System.out.println("msg = " + msg);
        //获取标识信息的id
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
​
        try {
            /** 确认消息
             * basicAck(long deliveryTag, boolean multiple)
             * deliveryTag:用来标识消息的id
             * multiple:是否批量。true:将一次性 ACK 所有小于 deliveryTag 的消息
             */
            System.out.println("开始处理业务逻辑");
            System.out.println("根据订单的id查询订单的状态");
            System.out.println("判断订单是否已经支付成功");
            System.out.println("如果取消订单就回滚库存");
            channel.basicAck(deliveryTag, false);
            //这里捕获的异常需要是Exception,如果只捕获IOException,会报异常,然后关闭
        } catch (Exception e) {
            /** 不确认消息
             * basicNack(long deliveryTag, boolean multiple, boolean requeue)
             * deliveryTag:用来标识消息的id
             * multiple:是否批量不签收
             * requeue:是否重新回到队列,不确认的消息就会变为Unacked,
             *          如果这里设置为true,那么消息会回到队列,重新在进行发送
             *          如果这里设置为false,那么消息不会回到队列
             */
            channel.basicNack(deliveryTag,true,false);
        }
    }
}

2.7.2 总结

1. 延迟队列 指消息进入队列后,可以被延迟一定时间,再进行消费。
2. RabbitMQ没有提供延迟队列功能,但是可以使用 : TTL + DLX 来实现延迟队列效果。
​
注:
消费者在监听队列的时候监听的是死信队列

2.8 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.9 RabbitMQ应用问题

2.9.1 消息可靠性保障

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

 

消息补偿机制:
​
2发送的消息在Q1中被正常消费到写入DB,发送ack给Q2。回调检查服务监听到Q2的消息,将消息写入MDB
 
如果1成功2失败,因为3页发送了消息放入Q3。此时回调检查服务也监听到了Q3,要去比对MDB是否一致,如果一致则代表消费过。如果MDB中不存在,就代表2失败了,就走8让生产者重新发。
 
如果2个都发送失败了,有MDB的定时检查服务,比对业务数据库DB与消息数据库MDB,就能发现差异

2.9.2 消息幂等性保障

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

 

2.10 RabbitMQ面试题

2.10.1 保证消息按顺序执行

1、消息出现错乱的场景

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

 

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

 

2、保证消息消费顺序

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

 

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

 

2.10.2 设置秒杀活动

秒杀活动特点:

1、并发量大

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

2、库存量少

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

3、业务简单

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

4、技术难点

4.1 有业务的冲击
秒杀是营销活动中的一种,如果和其他营销活动应用部署在同一服务器上,肯定会对现有其他活动造成冲击,极端情况下可能导致整个电商系统服务宕机
​
4.2 直接下订单
下单页面是一个正常的 URL 地址,需要控制在秒杀开始前,不能下订单,只能浏览对应活动商品的信息。简单来说,需要 Disable 订单按钮
​
4.3 页面流量突增
秒杀活动开始前后,会有很多用户请求对应商品页面,会造成后台服务器的流量突增,同时对应的网络带宽增加,需要控制商品页面的流量不会对后台服务器、DB、Redis 等组件的造成过大的压力

5、架构设计思想

5.1 限流
由于活动库存量一般都是很少,对应的只有少部分用户才能秒杀成功。所以我们需要限制大部分用户流量,只准少量用户流量进入后端服务器
​
5.2 削峰
秒杀开始的那一瞬间,会有大量用户冲击进来,所以在开始时候会有一个瞬间流量峰值。如何把瞬间的流量峰值变得更平缓,是能否成功设计好秒杀系统的关键因素。实现流量削峰填谷,一般的采用缓存和 MQ 中间件来解决
​
5.3 异步
秒杀其实可以当做高并发系统来处理,在这个时候,可以考虑从业务上做兼容,将同步的业务,设计成异步处理的任务,提高网站的整体可用性
​
5.4 缓存
秒杀系统的瓶颈主要体现在下订单、扣减库存流程中。在这些流程中主要用到 OLTP 的数据库,类似  MySQL、SQLServer、Oracle。由于数据库底层采用 B+  树的储存结构,对应我们随机写入与读取的效率,相对较低。如果我们把部分业务逻辑迁移到内存的缓存或者 Redis 中,会极大的提高并发效率

6、整体架构

 

7、秒杀整体流程图

 

8、秒杀服务层和后端

秒杀业务的核心是库存处理,用户购买成功后会进行减库存操作,并记录购买明细。当秒杀开始时,大量用户同时发起请求,这是一个并行操作,多条更新库存数量的SQL语句会同时竞争秒杀商品所处数据库表里的那行数据,导致库存的减少数量与购买明细的增加数量不一致,因此,我们使用RabbitMQ进行削峰限流并且将请求数据串行处理
​
削峰填谷加延迟加载
​
场景:
秒杀450件商品
设置每次最大拉取100个人,
@1.查询剩余的秒杀商品的个数
@2.用拉取数跟剩余的秒杀商品的个数进行比较如果小于就直接拉取100个,如果大于就将拉取的值改为剩余的商品的数量,拉取成功并保证是同一个人的下单之后并将其添加到订单表中,库存的数量减少。
@3.设置超时时间为30分钟,当30分钟之后还没有付款的取消订单,然后将对应的库存补上
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值