Springboot学习之RabbitMq实现延时队列【取消超时订单】(八)

测试前言

RabbitMQ 作为目前应用相当广泛的消息中间件,在企业级应用、微服务应用中充当着重要的角色。特别是在一些典型的应用场景以及业务模块中具有重要的作用,比如业务服务模块解耦、异步通信、高并发限流、超时业务、数据延迟处理等。
这篇文章带领大家使用RabbitMQ实现延时队列

1.搭建项目环境

工欲善其事,必先利其器,接触一个新技术之前,肯定要先安装环境和工具,本篇文章不提供安装教程,不清楚RabbitMq安装的请看我的另一篇文章《最简单的RabbitMQ消息队列搭建(windows环境下安装)》,安装成功后启动RabbitMq服务

Springboot学习之RabbitMq实现延时队列【取消超时订单】(八)_第1张图片
安装成功之后RabbitMQ,在浏览器中输入地址查看:http://127.0.0.1:15672/,运行界面就是这样的
在这里插入图片描述
这样我们的项目环境就搭建成功了。

2.延时队列–实现思路

延迟队列,也叫“延时队列”,顾名思义,其实就是“生产者生产消息,消息进入队列之后,并不会立即被指定的消费者所消费,而是会延时一段指定的时间TTL(Time To Live),最终才被消费者消费

RabbitMQ本身是不支持延时队列,而是同过二个特性来实现的:

  1. Time To Live(TTL)
    RabbitMQ可以针对Queue设置x-expires 或者 针对Message设置 x-message-ttl,来控制消息的生存时间,如果超时(两者同时设置以最先到期的时间为准),则消息变为dead letter(死信)
    RabbitMQ针对队列中的消息过期时间有两种方法可以设置。
    A: 通过队列属性设置,队列中所有消息都有相同的过期时间。
    B: 对消息进行单独设置,每条消息TTL可以不同。
    如果同时使用,则消息的过期时间以两者之间TTL较小的那个数值为准。消息在队列的生存时间一旦超过设置的TTL值,就成为dead letter(死信)
  2. Dead Letter Exchanges(DLX)
    RabbitMQ的Queue可以配置x-dead-letter-exchangex-dead-letter-routing-key(可选)两个参数,如果队列内出现了dead letter(死信),则按照这两个参数重新路由转发到指定的队列。
    x-dead-letter-exchange:出现dead letter之后将dead letter重新发送到指定exchange
    x-dead-letter-routing-key:出现dead letter之后将dead letter重新按照指定的routing-key发送路由
     

可能说这么多还是看不太懂,我准备了一张图

延时队列的执行流程就是图中所示,介绍完延时队列的概念之后,给大家举一个在项目中常见的场景:

用户创建下单记录之后,会对其进行付款,付款成功之后,该条记录将变为已支付并且有效,否则的话,一旦过了指定的时间,即超时了,则该记录将置为无效,并且不能被用于后续的业务逻辑

可能有人用过定时器(Timer)也可以实现类似的功能,但是定时器不能精准的知道哪些需要执行任务,查询范围太大,太浪费性能。
使用rabbitmap,我们只用把需要把的某个订单放入消息中间去(message),并且设置该消息的过期时间,等过期时间到达时再取出消费即可。
下面我们就用延时队列来实现,某个时间段过后取消未付款的订单


3.使用springboot+RabbitMq进行测试

1、新建SpringBoot项目如下:

Springboot学习之RabbitMq实现延时队列【取消超时订单】(八)_第2张图片

2、在pom.xml中引入项目需要的jar包

Springboot学习之RabbitMq实现延时队列【取消超时订单】(八)_第3张图片

pom.xml如下:



    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        2.2.2.RELEASE
         
    
    com.example
    rabbitmq-order-delay
    0.0.1-SNAPSHOT
    rabbitmq-order-delay
    Demo project for Spring Boot

    
        1.8
    

    
        
            org.springframework.boot
            spring-boot-starter
        
        
        
            org.springframework.boot
            spring-boot-starter-amqp
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.projectlombok
            lombok
        

        
            org.springframework.boot
            spring-boot-starter-test
            test
            
                
                    org.junit.vintage
                    junit-vintage-engine
                
            
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    


3、配置文件application.properties

Springboot学习之RabbitMq实现延时队列【取消超时订单】(八)_第4张图片

application.properties

#本机ip地址,一般装在本机直接使用localhost,若是虚拟机,则使用虚拟机的ip地址
spring.rabbitmq.host=localhost
# 端口号
spring.rabbitmq.port=5672
# rabbitmq的用户信息,默认都为guest
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

4、在与springboot启动类同级新建config和pojo和controller包,新建实体类:Order

Springboot学习之RabbitMq实现延时队列【取消超时订单】(八)_第5张图片

package com.example.rabbitmqorderdelay.pojo;

import lombok.Data;

import java.io.Serializable;

/**
 * 作者:LSH
 */
@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; // 订单名字
}

5、配置队列
(1)在config下面新建DelayRabbitConfig.java,将它作为一个配置类使用(copy之前记得看注释

Springboot学习之RabbitMq实现延时队列【取消超时订单】(八)_第6张图片

package com.example.rabbitmqorderdelay.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 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);
    }

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

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

}

(2)在config包下面新建生产者:DelaySender.java, 声明它是一个工具类,这里我们日志为了简化流程使用了springboot自带的日志

Springboot学习之RabbitMq实现延时队列【取消超时订单】(八)_第7张图片

package com.example.rabbitmqorderdelay.config;

import com.example.rabbitmqorderdelay.pojo.Order;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * 作者:LSH
 * 日期:2019/12/18 21:44
 * 生产者 生产消息
 */
@Component
@Slf4j
public class DelaySender {

    // AMQP 高级消息队列协议
    @Autowired
    private AmqpTemplate amqpTemplate;

    public void sendDelay(Order order) {

        log.info("【订单生成时间】" + new Date().toString() +"【1分钟后检查订单是否已经支付】" + order.toString() );

        this.amqpTemplate.convertAndSend(DelayRabbitConfig.ORDER_DELAY_EXCHANGE, DelayRabbitConfig.ORDER_DELAY_ROUTING_KEY, order, message -> {
            // 如果配置了 params.put("x-message-ttl", 5 * 1000); 那么这一句也可以省略,具体根据业务需要是声明 Queue 的时候就指定好延迟时间还是在发送自己控制时间
            message.getMessageProperties().setExpiration(1 * 1000 * 60 + "");
            return message;
        });
    }
}

这里声明的amqpTemplate接口,这个接口包含了发送和接收消息的一般操作,换种说法,它不是某个实现所专有的,所以AMQP存在于名称里。这个接口的实现与AMQP协议的实现紧密关联。
this.amqpTemplate.convertAndSend的第一个参数为延迟交换机的名称,第二个为延时消费routing-key,第三个参数为order操作对象,第四个参数为消息

(3)在config包下面新建消费者:DelayReceiver.java

Springboot学习之RabbitMq实现延时队列【取消超时订单】(八)_第8张图片

package com.example.rabbitmqorderdelay.config;

import com.example.rabbitmqorderdelay.pojo.Order;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;

import java.util.Date;

// 接收者 --消费者
@Component
@Slf4j
public class DelayReceiver {

    @RabbitListener(queues = {DelayRabbitConfig.ORDER_QUEUE_NAME})
    public void orderDelayQueue(Order order, Message message, Channel channel) {
        log.info("###########################################");
        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("【该订单已取消】");
        }
    }
}

在这个类中我们定义了一个普通方法,可能你会很纳闷为什么这个普通方法为什么可以进行接收消息,主要还是这个注解: @RabbitListener,下面给大家简单了解下这个注解的作用,@RabbitListener注解的方法所在的类首先是一个bean,因此,实现 BeanPostProcessor接口对每一个初始化完成的bean进行处理。比如上面 DelayRabbitConfig.ORDER_QUEUE_NAME所在的方法就是一个Bean

  • 遍历bean中由用户自己定义的所有的方法,找出其中添加了@RabbitListener注解的方法
  • 读取上面找出的所有方法上@RabbitListener注解中的值,并为每一个方法创建一个RabbitListenerEndpoint,保存在RabbitListenerEndpointRegistrar类中
  • 在所有的bean都初始化完成,即所有@RabbitListener注解的方法都创建了endpoint之后,由我们配置的RabbitListenerContainerFactory将每个endpoint创建MessageListenerContainer
  • 最后创建上面的MessageListenerContainer
  • 至此,全部完成,MessageListenerContainer启动后将能够接受到消息,再将消息交给它的MessageListener处理消息

 

(4)最后在controller包下面新建TestController.java 进行测试

Springboot学习之RabbitMq实现延时队列【取消超时订单】(八)_第9张图片

package com.example.rabbitmqorderdelay.controller;

import com.example.rabbitmqorderdelay.config.DelaySender;
import com.example.rabbitmqorderdelay.pojo.Order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {

    @Autowired
    private DelaySender delaySender;

    @GetMapping("/sendDelay")
    public Object sendDelay() {
        Order order1 = new Order();
        order1.setOrderStatus(0);
        order1.setOrderId("123321123");
        order1.setOrderName("波音747飞机");

        Order order2 = new Order();
        order2.setOrderStatus(1);
        order2.setOrderId("2345123123");
        order2.setOrderName("豪华游艇");

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

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

成功启动项目,打开浏览器,输入: http://localhost:8080/sendDelay

Springboot学习之RabbitMq实现延时队列【取消超时订单】(八)_第10张图片

Springboot学习之RabbitMq实现延时队列【取消超时订单】(八)_第11张图片

打开RabbitMq的管理页面http://127.0.0.1:15672/#/,可以看到交换机

Springboot学习之RabbitMq实现延时队列【取消超时订单】(八)_第12张图片

看到队列情况

Springboot学习之RabbitMq实现延时队列【取消超时订单】(八)_第13张图片

一分钟后观察控制台

Springboot学习之RabbitMq实现延时队列【取消超时订单】(八)_第14张图片

可以看到未支付的订单已经改变状态,至此我们实现了一个简单的超时订单取消支付,后面可以根据自己的项目需求不断添加改变

总结

本文可能在许多rabbitmq的许多概念没有说的特别清楚,但是都是自己看了这么多文章自己的理解,如有问题欢迎指出!!

你可能感兴趣的:(SpringBoot)