Rabbit高级特性

  1. 在使用Rabbit时,可能会遇到投递失败的情况,所以我门需要知道消息是否发送成功。
    1.1Confirm消息确认机制:
    在这里插入图片描述
    如何实现Confirm确认消息?

第一步:在channel上开启确认模式:channel.confirmSelect()

第二步:在channel上添加监听:addConfirmListener,监听成功和失败的返回结果,

根据具体的结果对消息进行重新发送、或者记录日志等后续处理。
rabbitmq 整个消息投递的路径为:

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

消息从 producer 到 exchange 则会返回一个 confirmCallback 。

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

我们将利用这两个 callback 控制消息的可靠性投递。
1.2:使用Spring实现:
配置省略。
Producer开启确认模式:
1,在ConnectionFactory中设置 publisher-confirms=“true”
2,给RabbitTemplate设置ConfirmCallback回调函数。
代码:

@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:spring_rabbitmq.xml")
public class Test {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @org.junit.Test
    public void test1()throws  Exception{
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback(){
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                if (ack){
                    System.out.println("成功");
                }else {
                    System.out.println("失败"+cause);
                }
            }

        });
        rabbitTemplate.convertAndSend("spring_exchange_confirm","confirm","鲨鱼的手臂");
        Thread.sleep(100);
    }

ConSumer代码:

<rabbit:listener-container connection-factory="connectionFactory" acknowledge="manual">
        <rabbit:listener ref="springConsumer" queue-names="spring_queue_confirm"></rabbit:listener>
    </rabbit:listener-container>

设置确认消息的方式为手动acknowledge=“manual”

添加ChannelAwareMessageListener监听

@Component
public class springConsumer implements ChannelAwareMessageListener {
    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            System.out.println(new String(message.getBody()));
            channel.basicAck(deliveryTag,true);
        }catch (Exception e){
            e.printStackTrace();
            System.out.println("错了吧");
           channel.basicNack(deliveryTag,true,true);
        }
    }

}

使用自动确认,在实际开发中可能会出现,消息被确认接收之后但是业务代码出现了问题,消息就会丢失。但是使用手动确认,如果业务代码出现了问题,可以调用channel.basicNack()方法,让其自动重新发送消息。

  1. 回退机制:return listener
    当我门无法将消息推送到队列,就需要监听这种不可达消息。
    在这里插入图片描述
    代码:

在这里插入图片描述
Producer代码:

 @org.junit.Test
    public void test2()throws  Exception{
        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(replyText);
                System.out.println(new String(message.getBody()));
                System.out.println(replyCode);
            }
        });

        rabbitTemplate.convertAndSend("spring_exchange_confirm","confirmss","鲨鱼的手臂2");
        Thread.sleep(100);
    }

配置文件中的ConnectionFactory 设置publisher-returns=“true”,开启回退机制。
测试代码中设置 rabbitTemplate.setMandatory(true);
(设置Exchange处理消息的模式:默认为false,表示如果没有到达队列就会将消息舍弃,设置为true,表示如果没有到达队列会将消息发送到ReturnCallBack)。

  1. Consumer限流机制:确保ack机制为manual(手动确认机制)
    Consumer端配置文件
    在监听器添加prefetch=“1”(表示每次只取一个)
 <rabbit:listener-container connection-factory="connectionFactory" acknowledge="manual" prefetch="1">
        <rabbit:listener ref="springConsumer" queue-names="spring_queue_confirm"></rabbit:listener>
    </rabbit:listener-container>
  1. TTL(Time To Live):过期时间:
    Producer将消息推送至队列中,超过时间没有被Consumer端取出,就会被自动清除。(应用于订单支付)

代码:
Producer配置文件:

<rabbit:queue id="spring_queue_confirm_ttl" name="spring_queue_confirm_ttl" auto-declare="true">
        <rabbit:queue-arguments>
            <entry key="x-message-ttl" value="10000" value-type="java.lang.Integer"></entry>
        </rabbit:queue-arguments>
    </rabbit:queue>
    <rabbit:topic-exchange name="spring_exchange_confirm_ttl" auto-declare="true">
            <rabbit:bindings>
               <rabbit:binding pattern="ttl.*" queue="spring_queue_confirm_ttl"></rabbit:binding>
            </rabbit:bindings>
    </rabbit:topic-exchange>
    

给队列设置参数:x-message-ttl(注意参数不是随意设置具体参考RabbitMQ后台客户端。)
表示设置过期时间为10s。
在这里插入图片描述
Producer测试代码:

 @org.junit.Test
    public void test4() throws Exception {
        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(new String(message.getBody()));
                System.out.println(replyCode);
                System.out.println(replyText);
                System.out.println(exchange);
                System.out.println(routingKey);
            }
        });
        rabbitTemplate.convertAndSend("spring_exchange_confirm_ttl", "ttl.huhu", "鲨鱼的手臂4");
        Thread.sleep(100);
    }
  1. 死信队列DeadLetter Exchange(DLE)
    6.1当消息消息在队列中到了过期时间,会发送到另一个交换机上,这个交换机被称为死信队列。
    6.2消息成为死信的三种情况:
    1,到了存活时间消息未被消费;
    2,消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false;
    3,队列消息长度到达限制;
<!--`````````````````创建死信队列``````````````````-->
    <rabbit:queue  id="spring_test_queue_dlx" name="spring_test_queue_dlx" auto-declare="true">
        <rabbit:queue-arguments>
            <entry key="x-dead-letter-exchange" value="spring_exchange_dlx"></entry>
            <entry key="x-dead-letter-routing-key" value="dlx.ddd"></entry>
            <entry key="x-max-length" value="10" value-type="java.lang.Integer"></entry>
        </rabbit:queue-arguments>
    </rabbit:queue>
    <rabbit:topic-exchange name="spring_test_exchange_dlx" auto-declare="true">
        <rabbit:bindings>
            <rabbit:binding pattern="dlx.*" queue="spring_test_queue_dlx"></rabbit:binding>
        </rabbit:bindings>
    </rabbit:topic-exchange>

    <rabbit:queue  id="spring_queue_dlx" name="spring_queue_dlx" auto-declare="true">

    </rabbit:queue>
    <rabbit:topic-exchange name="spring_exchange_dlx" auto-declare="true">
        <rabbit:bindings>
            <rabbit:binding pattern="dlx.*" queue="spring_queue_dlx"></rabbit:binding>
        </rabbit:bindings>
    </rabbit:topic-exchange>

解析:创建两个正常队列跟交换机,把一个交换机跟队列作为死信交换机跟死信队列。
正常队列绑定死信交换机:

 <rabbit:queue-arguments>
            <entry key="x-dead-letter-exchange" value="spring_exchange_dlx"></entry>
            <entry key="x-dead-letter-routing-key" value="dlx.ddd"></entry>
            <entry key="x-max-length" value="10" value-type="java.lang.Integer"></entry>
        </rabbit:queue-arguments>

配置表示如果队列长度到10,后面的消息就会进入死信交换机。
producer测试代码:

@org.junit.Test
    public void test5()throws Exception{
        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(new String(message.getBody()));
                System.out.println(replyCode);
                System.out.println(replyText);
                System.out.println(exchange);
                System.out.println(routingKey);
            }
        });
        for (int i = 0; i <20; i++) {
            rabbitTemplate.convertAndSend("spring_test_exchange_dlx", "dlx.huhu", "鲨鱼的手臂5");
        }

7.延时队列
例如用于订单系统中超过30分钟就会取消订单。
由于Rabbit中没有提供延时队列功能,我们可以采取TTL+死信队列组合的方式实现。
代码:
Producer配置文件:

 <!--````````````````创建延时队列`````````````````````-->
    <rabbit:queue  id="spring_test_queue_dlx_TTL" name="spring_test_queue_dlx_TTL" auto-declare="true">
        <rabbit:queue-arguments>
            <entry key="x-dead-letter-exchange" value="spring_exchange_dlx_TTL"></entry>
            <entry key="x-dead-letter-routing-key" value="dlx.ddd"></entry>
            <entry key="x-message-ttl" value="10000" value-type="java.lang.Integer"></entry>
        </rabbit:queue-arguments>
    </rabbit:queue>
    <rabbit:topic-exchange name="spring_test_exchange_dlx" auto-declare="true">
        <rabbit:bindings>
            <rabbit:binding pattern="dlx.*" queue="spring_test_queue_dlx_TTL"></rabbit:binding>
        </rabbit:bindings>
    </rabbit:topic-exchange>

    <rabbit:queue  id="spring_queue_dlx_TTL" name="spring_queue_dlx_TTL" auto-declare="true">

    </rabbit:queue>
    <rabbit:topic-exchange name="spring_exchange_dlx_TTL" auto-declare="true">
        <rabbit:bindings>
            <rabbit:binding pattern="dlx.*" queue="spring_queue_dlx_TTL"></rabbit:binding>
        </rabbit:bindings>
    </rabbit:topic-exchange>

Consumer配置文件代码:

  <rabbit:listener-container connection-factory="connectionFactory" acknowledge="manual">
        <rabbit:listener ref="springConsumer" queue-names="spring_queue_dlx_TTL"></rabbit:listener>
    </rabbit:listener-container>

注意的是,延时队列的效果一定要监听死信队列。

  1. RabbitAdmin的应用:
    ① 底层实现就是从Spring容器中获取Exchange,Bingding,Routingkey以及Queue的@bean声明
    ② 使用RabbitAdmin的execute方法执行对应的声明,修改和删除等一系列MQ的操作者
    比如:添加一个交换机,删除一个绑定, 清空一个队列里面的消息等等
    代码演示:
    新建一个项目pom文件起步配置:
<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>          
        </dependency>
        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>          
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

新建程序入口:

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class,args);
    }
}

新建配置类

@Configuration
@ComponentScan({"com.xxx.*"})
public class RabbitMQConfig {
    @Bean
    public ConnectionFactory connectionFactory() {
        CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
        connectionFactory.setAddresses("localhost:5672");
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("admin");
        connectionFactory.setVirtualHost("my_vhost");
        return connectionFactory;
    }

    @Bean
    public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) {
        RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory);
        rabbitAdmin.setAutoStartup(true);
        return rabbitAdmin;

    }
}

新建测试类

@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTests {
    @Autowired
    private RabbitAdmin rabbitAdmin;
    @Test
    public void test1(){
        //声明交换机
        rabbitAdmin.declareExchange(new DirectExchange("springboot.test.direct",true,false));
        rabbitAdmin.declareExchange(new DirectExchange("springboot.test.topic",true,false));
        rabbitAdmin.declareExchange(new DirectExchange("springboot.test.fanout",true,false));
        //声明队列
        rabbitAdmin.declareQueue(new Queue("springboot.test.direct.queue",true));

        rabbitAdmin.declareQueue(new Queue("springboot.test.topic.queue",true));

        rabbitAdmin.declareQueue(new Queue("springboot.test.fanout.queue",true));
        //交换机与队列进行绑定

        rabbitAdmin.declareBinding(BindingBuilder.bind(new Queue("springboot.test.direct.queue",true)).to(new DirectExchange("springboot.test.direct",true,false)).with("direct"));
        rabbitAdmin.declareBinding(BindingBuilder.bind(new Queue("springboot.test.topic.queue",true)).to(new DirectExchange("springboot.test.topic",true,false)).with("user.#"));
        rabbitAdmin.declareBinding(
                BindingBuilder.bind(new Queue("springboot.test.fanout.queue",true))
                        .to(new FanoutExchange("springboot.test.fanout",true,false))
        );
    }
}

使用 SpringAMQP 去声明,就需要使用SpringAMQP 的如下模式,即声明 Bean 方式

/**  
     * 针对消费者配置  
     * 1. 设置交换机类型  
     * 2. 将队列绑定到交换机  
        FanoutExchange: 将消息分发到所有的绑定队列,无routingkey的概念  
        HeadersExchange :通过添加属性key-value匹配  
        DirectExchange:按照routingkey分发到指定队列  
        TopicExchange:多关键字匹配  
     */  
    @Bean  
    public TopicExchange exchange001() {  
        return new TopicExchange("topic001", true, false);  
    }  

    @Bean  
    public Queue queue001() {  
        return new Queue("queue001", true); //队列持久  
    }  
    
    @Bean  
    public Binding binding001() {  
        return BindingBuilder.bind(queue001()).to(exchange001()).with("spring.*");  
    }  
    
    @Bean  
    public TopicExchange exchange002() {  
        return new TopicExchange("topic002", true, false);  
    }  
    
    @Bean  
    public Queue queue002() {  
        return new Queue("queue002", true); //队列持久  
    }
    
    @Bean  
    public Binding binding002() {  
        return BindingBuilder.bind(queue002()).to(exchange002()).with("rabbit.*");  
    } 
    
    @Bean  
    public Queue queue003() {  
        return new Queue("queue003", true); //队列持久  
    }
    
    @Bean  
    public Binding binding003() {  
        return BindingBuilder.bind(queue003()).to(exchange001()).with("mq.*");  
    } 
    
    @Bean  
    public Queue queue_image() {  
        return new Queue("image_queue", true); //队列持久  
    }
    
    @Bean  
    public Queue queue_pdf() {  
        return new Queue("pdf_queue", true); //队列持久  
    }
  1. RabbitMQ与SpringBoot整合
    Producer:
    pom文件起步依赖:
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
    </parent>

    <dependencies>
        <!--2. rabbitmq-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>

yml配置文件代码

spring:
  rabbitmq:
    host: 139.196.37.163
    port: 5672
    username: admin
    password: admin
    virtual-host: my_vhost

配置类:

    public static final String EXCHANGE_NAME = "boot_topic_exchange";
    public static final String QUEUE_NAME = "boot_queue";
    //交换机
    @Bean
    public Exchange bootExchange(){
        return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();
    }
    //队列
    @Bean
    public Queue bootQueue(){
        return QueueBuilder.durable(QUEUE_NAME).build();
    }
    //绑定交换机与队列
    //noargs():表示不指定参数
    @Bean
    public Binding bootBinding(Queue bootQueue,Exchange bootExchange){
        return BindingBuilder.bind(bootQueue).to(bootExchange).with("boot.*").noargs();
    }

测试类(发送消息):

@RunWith(SpringRunner.class)
@SpringBootTest
public class ProducerTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Test
    public void test1(){
        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME,"boot.haha","鲨鱼的手臂");
    }
}

Consumer(消费者代码)
pom.xml,yml配置文件跟Producer一样。
监听器类:

@Component
public class RabbimtMQListener{
    @RabbitListener(queues = "boot_queue")
    public void listBootQueue(Message message){
        System.out.println(new String(message.getBody()));
    }
}
  1. 百分百消息投递
    我们在工作时难免会遇到消息投递,而且需要保证消息不能丢失。
    在这里插入图片描述
    如图所示,在发送消息时,不像之前,直接编辑好消息就发送出去,在发送消息出去前做一些操作。
    Step 1: 首先把消息信息(业务数据)存储到数据库中,紧接着,我们再把这个消息记录也存储到一张消息记录表里(或者另外一个同源数据库的消息记录表)

Step 2:发送消息到MQ Broker节点(采用confirm方式发送,会有异步的返回结果)

Step 3、4:生产者端接受MQ Broker节点返回的Confirm确认消息结果,然后进行更新消息记录表里的消息状态。比如默认Status = 0 当收到消息确认成功后,更新为1即可!

Step 5:但是在消息确认这个过程中可能由于网络闪断、MQ Broker端异常等原因导致 回送消息失败或者异常。这个时候就需要发送方(生产者)对消息进行可靠性投递了,保障消息不丢失,100%的投递成功!(有一种极限情况是闪断,Broker返回的成功确认消息,但是生产端由于网络闪断没收到,这个时候重新投递可能会造成消息重复,需要消费端去做幂等处理)所以我们需要有一个定时任务,(比如每5分钟拉取一下处于中间状态的消息,当然这个消息可以设置一个超时时间,比如超过1分钟 Status = 0 ,也就说明了1分钟这个时间窗口内,我们的消息没有被确认,那么会被定时任务拉取出来)

Step 6:接下来我们把中间状态的消息进行重新投递 retry send,继续发送消息到MQ ,当然也可能有多种原因导致发送失败

Step 7:我们可以采用设置最大努力尝试次数,比如投递了3次,还是失败,那么我们可以将最终状态设置为Status = 2 ,最后 交由人工解决处理此类问题(或者把消息转储到失败表中)。

-- ----------------------------
-- Table structure for broker_message_log
-- ----------------------------
DROP TABLE IF EXISTS `broker_message_log`;
CREATE TABLE `broker_message_log` (
  `message_id` varchar(255) NOT NULL COMMENT '消息唯一ID',
  `message` varchar(4000) NOT NULL COMMENT '消息内容',
  `try_count` int(4) DEFAULT '0' COMMENT '重试次数',
  `status` varchar(10) DEFAULT '' COMMENT '消息投递状态 0投递中,1投递成功,2投递失败',
  `next_retry` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP COMMENT '下一次重试时间',
  `create_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP,
  `update_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`message_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for t_order
-- ----------------------------
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `message_id` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2018091102 DEFAULT CHARSET=utf8;
  1. 消息幂等性
    幂等性指一次和多次请求某一个资源,对于资源本身应该具有同样的结果。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。
    比如我们扫码付款时,由于网卡,付款失败后又扫了一次码(码没有刷新),这样多次扫,我们会担心会不会多次扣款,但是设置消息幂等性后就不会出现多次扣款的事情。
    消息幂等性保障 乐观锁机制。
    我们可以在提交付款数据时添加一个版本属性。
    例如:`
id=1,money=500,version=1

消费者接收到

id=1,money=500,version=1

第一次执行SQL语句

第一次执行:version=1
update account set money = money - 500 , version = version + 1
where id = 1 and version = 1

第二次执行:version=2

第二次执行:version=2
update account set money = money - 500 , version = version + 1
where id = 1 and version = 1
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值