SpringBoot整合RabbitMQ延迟队列

一、什么是延迟队列

即消息进入延迟队列后,不会立即消费,会到达指定时间之后在消费。
应用场景:
用户下单后半个小时,需要判断订单是否支付金额,没有支付就取消订单
新用户注册七天后,需要发送短信问候

二、延迟队列的实现

延迟队列是由TTL+DLX(死信交换机)一起组成实现的。

TTL是设置队列消息的过期时间,有两种设置方式:
方式1:设置整个队列的过期时间,整个队列都统一过期时间,设置参数x-message-ttl,单位ms(毫秒)。
方式2:对单个消息设置过期时间,设置参数expiration,单位ms(毫秒),意思就是队列的消息过期时间都是独自设置的。

什么是死信交换机死信队列:首先需要两个交换机两个队列,一个交换机是普通交换机,一个交换机是死信交换机(Dead Letter Exchange),一个队列是普通队列,一个队列是私信队列(DLX).。生产者发送一条消息到普通交换机,普通交换机在发送给普通的队列,按照正常的逻辑普通队列需要绑定一个消费者,但是死信就不需要,普通队列需要绑定死信交换机,并且指定死信交换机的rontingKey,普通队列将消息发送给死信交换机,死信交换机再绑定死信队列,死信队列接收到死信交换机发送来的消息,再给绑定了死信队列的消费者消费。

在这里插入图片描述

上面流程哪里只讲了,普通队列将死信消息转发到死信交换机,下面就讲讲怎样才能成为死信
消息成为死信的三种情况:
1、队列消息长度达到限制
2、消费者拒接消费消息,并且不重回队列
3、队列时间存在ttl的时间设置,消费到达超时时间未被消费

三、延迟队列的案例

项目结构:
在这里插入图片描述
pom依赖

    <dependencies>

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


        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>


        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.22</version>
        </dependency>

        <!-- 引入阿里数据库连接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.18</version>
        </dependency>

        <!--swagger文档-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <!--swagger文档-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>

    </dependencies>

配置application.yml

server:
  tomcat:
    max-http-form-post-size: -1  #base64图片上传大小
  port: 8005 #测试服务器 打包

spring:
  servlet:
    multipart:
      max-file-size: 50MB #单个数据大小
      max-request-size: 100MB #总数据大小

  rabbitmq:
    port: 5672
    username: wdp123
    password: wdp123
    addresses: 39.107.90.1
    virtual-host: wdp
    publisher-confirms: true
    publisher-returns: true
    listener:
      simple:
        acknowledge-mode: manual


  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://120.24.70.201:33106/jypt?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8
    username: develop
    password: Vhcat%@32105464567
    type: com.alibaba.druid.pool.DruidDataSource




在com.wdp.rabbitmq.swagger包下创建Swagger2类

package com.wdp.rabbitmq.swagger;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class Swagger2 {
	   // http://127.0.0.1:8080/swagger-ui.html
	   @Bean
	    public Docket createRestApi() {
	        return new Docket(DocumentationType.SWAGGER_2)
	                .apiInfo(apiInfo())
	                .select()
	                .apis(RequestHandlerSelectors.basePackage("com.wdp.rabbitmq"))
	                .paths(PathSelectors.any())
	                .build();
	    }
	    private ApiInfo apiInfo() {
	        return new ApiInfoBuilder()
	                .title("Spring Boot中使用spring-boot-starter-amqp集成rabbitmq")
	                .description("测试SpringBoot整合进行各种工作模式信息的发送")
/*
	                .termsOfServiceUrl("https://www.jianshu.com/p/c79f6a14f6c9")
*/
	                .contact("roykingw")
	                .version("1.0")
	                .build();
	    }
}

创建启动类OrderMqApplication

package com.wdp.rabbitmq;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class OrderMqApplication {

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

在com.wdp.rabbitmq.entity包下创建Order

package com.wdp.rabbitmq.entity;

import lombok.Data;

import java.io.Serializable;
@Data
public class Order implements Serializable {
    private static final long serialVersionUID = -2221214252163879885L;

    private String orderId; // 订单id

    private Integer orderStatus; // 订单状态 0:未支付,1:已支付,2:订单已取消

    private String orderName; // 订单名字

}

在com.wdp.rabbitmq.service包下创建订单接口

package com.wdp.rabbitmq.service;

import com.wdp.rabbitmq.entity.Order;
import org.springframework.stereotype.Service;


public interface DelaySenderService {

    public void sendDelay(Order order);

    //public void sendDelay2(Order order);

}

在com.wdp.rabbitmq.service.impl包下创建DelaySenderServiceImpl类

package com.wdp.rabbitmq.service.impl;

import com.wdp.rabbitmq.config.DelayRabbitConfig;
import com.wdp.rabbitmq.entity.Order;
import com.wdp.rabbitmq.service.DelaySenderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.UUID;

@Service
@Slf4j
public class DelaySenderServiceImpl implements DelaySenderService {
    // AMQP 高级消息队列协议
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Override
    public void sendDelay(Order order) {
        log.info("【订单生成时间1】" + new Date().toString() +"【100毫秒后检查订单是否已经支付】" + order.toString() );
       rabbitTemplate.convertAndSend(DelayRabbitConfig.ORDER_DELAY_EXCHANGE,DelayRabbitConfig.ORDER_DELAY_ROUTING_KEY,order,message -> {
           message.getMessageProperties().setExpiration(1 * 1000 * 6 + "");
           return message;
       },new CorrelationData(UUID.randomUUID().toString()));
    }

//    @Override
//    public void sendDelay2(Order order) {
//        log.info("【订单生成时间2】" + new Date().toString() +"【1分钟后检查订单是否已经支付】" + order.toString() );
//        rabbitTemplate.convertAndSend(DelayRabbitConfig.ORDER_DELAY_EXCHANGE,DelayRabbitConfig.ORDER_DELAY_ROUTING_KEY,order,message -> {
//            message.getMessageProperties().setExpiration(1 * 10000 * 6 + "");
//            return message;
//        },new CorrelationData(UUID.randomUUID().toString()));
//    }
}

重点来了,在com.wdp.rabbitmq.config包下创建DelayRabbitConfig类,用于配置死信交换机和死信队列

package com.wdp.rabbitmq.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

@Configuration
@Slf4j
public class DelayRabbitConfig {
    // 延迟队列 TTL 名称
    private static final String ORDER_DELAY_QUEUE = "order.delay.queue";

    // DLX,dead letter发送到的 exchange
    // 延时消息就是发送到该交换机的
    public static final String ORDER_DELAY_EXCHANGE = "order.delay.exchange";

    // routing key 名称
    // 具体消息发送在该 routingKey 的
    public static final String ORDER_DELAY_ROUTING_KEY = "order_delay";

    //立即消费的队列名称
    public static final String ORDER_QUEUE_NAME = "order.queue";

    // 立即消费的exchange
    public static final String ORDER_EXCHANGE_NAME = "order.exchange";

    //立即消费 routing key 名称
    public static final String ORDER_ROUTING_KEY = "order";

    //创建一个延时队列需要死信交换机绑定
    @Bean
    public Queue delayOrderQueue() {
        Map<String, Object> params = new HashMap<>();
        // x-dead-letter-exchange 声明了队列里的死信转发到的DLX名称,
        params.put("x-dead-letter-exchange", ORDER_EXCHANGE_NAME);
        // x-dead-letter-routing-key 声明了这些死信在转发时携带的 routing-key 名称。
        params.put("x-dead-letter-routing-key", ORDER_ROUTING_KEY);

        return new Queue(ORDER_DELAY_QUEUE, true, false, false, params);
    }

    // 创建一个死信队列
    @Bean
    public Queue orderQueue() {
        // 第一个参数为queue的名字,第二个参数为是否支持持久化
        return new Queue(ORDER_QUEUE_NAME, true);
    }

    //创建延迟交换机
    @Bean
    public DirectExchange orderDelayExchange() {
        // 一共有三种构造方法,可以只传exchange的名字, 第二种,可以传exchange名字,是否支持持久化,是否可以自动删除,
        // 第三种在第二种参数上可以增加Map,Map中可以存放自定义exchange中的参数
        // new DirectExchange(ORDER_DELAY_EXCHANGE,true,false);
        return new DirectExchange(ORDER_DELAY_EXCHANGE);
    }

    //创建死信交换机
    @Bean
    public TopicExchange orderTopicExchange() {
        return new TopicExchange(ORDER_EXCHANGE_NAME);
    }

    // 把延时队列delayOrderQueue和订单延迟交换的orderDelayExchange进行绑定
    @Bean
    public Binding dlxBinding() {
        return BindingBuilder.bind(delayOrderQueue()).to(orderDelayExchange()).with(ORDER_DELAY_ROUTING_KEY);
    }

    // 把死信队列orderQueue和死信交换的exchange进行绑定
    @Bean
    public Binding orderBinding() {
        // TODO 如果要让延迟队列之间有关联,这里的 routingKey 和 绑定的交换机很关键
        return BindingBuilder.bind(orderQueue()).to(orderTopicExchange()).with(ORDER_ROUTING_KEY);
    }



}

在com.wdp.rabbitmq.config包下创建RabbitMQConfirmAndReturn类配置消息的确认和回退机制

package com.wdp.rabbitmq.config;

import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

@Component
public class RabbitMQConfirmAndReturn implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnCallback {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void init() {
        rabbitTemplate.setConfirmCallback(this);            //指定 ConfirmCallback
        rabbitTemplate.setReturnCallback(this);             //指定 ReturnCallback

    }

    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            System.out.println("消息发送成功" + correlationData+"----"+"cause:"+cause);
        } else {
            System.out.println("消息发送失败:" + cause);
        }
    }

    @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 : " + exchange);
        System.out.println("消息使用的路由键 routing : " + routingKey);
    }
}

在com.wdp.rabbitmq.receiver包下创建DelayReceiver类配置死信队列的消费者,并签收

package com.wdp.rabbitmq.receiver;

import com.rabbitmq.client.Channel;
import com.wdp.rabbitmq.config.DelayRabbitConfig;
import com.wdp.rabbitmq.entity.Order;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Date;

@Component
@Slf4j
public class DelayReceiver {


    @RabbitListener(queues = {DelayRabbitConfig.ORDER_QUEUE_NAME})
    public void orderDelayQueue(Order order, Message message, Channel channel) throws IOException {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            log.info("订单手动签收消息确认 {}",message);
            log.info("【orderDelayQueue 监听的消息】 - 【消费时间】 - [{}]- 【订单内容】 - [{}]", new Date(), order.toString());
            if (order.getOrderStatus() == 0) {
                order.setOrderStatus(2);
                log.info("【该订单未支付,取消订单】" + order.toString());
            } else if (order.getOrderStatus() == 1) {
                log.info("【该订单已完成支付】");
            } else if (order.getOrderStatus() == 2) {
                log.info("【该订单已取消】");
            }
           channel.basicAck(deliveryTag,true);
            log.info("签收成功:{}",deliveryTag);
        }catch (Exception e){
/*
            第三个参数:requeue:重回队列。如果设置为true,则消息重新回到queue,broker会重新发送该消息给消费端
             */
            channel.basicNack(deliveryTag,true,false);
        }

    }

}

在com.wdp.rabbitmq.controller包下创建订单的controller

package com.wdp.rabbitmq.controller;

import com.wdp.rabbitmq.entity.Order;
import com.wdp.rabbitmq.service.DelaySenderService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {

    @Autowired
    private DelaySenderService delaySenderService;
    @GetMapping("/sendDelay")
    @ApiOperation(value = "下单")
    public Object sendDelay() {
        Order order1 = new Order();
        order1.setOrderStatus(0);
        order1.setOrderId("123321123");
        order1.setOrderName("这是order1时间短的");

        Order order2 = new Order();
        order2.setOrderStatus(1);
        order2.setOrderId("2345123123");
        order2.setOrderName("这是order2时间长的");

        Order order3 = new Order();
        order3.setOrderStatus(2);
        order3.setOrderId("983676");
        order3.setOrderName("小米alpan阿尔法");

        delaySenderService.sendDelay(order1);
        delaySenderService.sendDelay(order2);
        delaySenderService.sendDelay(order3);
        return "test--ok";
    }


}

延迟队列有个注意事项:
那就是不能把不同的时间往同一个队列里面塞,我们来举个例子,因为消息队列是先进先出,如果我同时传入三个消息A、B、C,A为3个小时,B为两个小时,C为1个小时,这里就要出问题,B和C到了时间后根本消费不了,因为他们前面有A为3个小时的消息,必须A消费了才能放出B和C,总结出了一点,前面的消息时间不能大于后面的消息时间,这里我也做了一个测试,把sendDelay2方法放开,在controller里面调用,时间设置为不一样,就可以做测试了,这里就不演示了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值