SpringBoot之RabbitMQ实现两种延时队列(订单延迟取消)

最近在做电商项目时,订单需求要求用户在规定时间内(比如30分钟)完成支付,否则订单关闭,释放库存。要实现这个功能有很多种方法:

1、前端处理

前端js写一个倒计时,在规定时间内用户没有支付,则用户再次进入订单时触发订单关闭操作。倒计时参考:js倒计时

不过这种前端方法有一个弊端是必须进入当前倒计时页面才会触发。比如:用户退出倒计时页面,那么即使在规定的时间内没有支付也不会关闭订单释放库存;优点是简单容易实现。

2、后端处理

对于后端处理,可以写一个定时器去查询订单,但这样的会频繁查询,也不太理想。最后选择了延迟队列来处理。我这里采用的是rabbitmq。

Rabbitmq实现延时队列一般而言有两种形式:

    第一种方式:利用两个特性: Time To Live(TTL)、Dead Letter Exchanges(DLX)

    第二种方式:利用rabbitmq中的插件 rabbitmq_delayed_message_exchange,下载地址:https://www.rabbitmq.com/community-plugins.html

第一种方式:TTL  DLX

2.1、TTL DLX是什么

    TTL

    RabbitMQ可以针对队列设置x-expires(则队列中所有的消息都有相同的过期时间),或者针对Message设置x-message-ttl(对消息进行单独设置,每条消息TTL可以不同),来控制消息的生存时间,如果超时(两者同时设置以最先到期的时间为准),则消息变为dead letter(死信)

    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发送。

在这里插入图片描述

①:生产者将消息(msg)和路由键(routekey)发送指定的死信交换机(delayexchange)上
②:死信交换机(delayexchange)根据路由键(routekey1)找到绑定自己的死信队列(delayqueue)并把消息给它
③:消息(msg)到期死亡变成死信转发给死信接收交换机(delayexchange)
④:死信接收交换机(receiveexchange)根据路由键(routekey2)找到绑定自己的死信接收队列(receivequeue)并把消息给它
⑤:死信接收队列(receivequeue)再把消息发送给监听它的消费者(customer)

2.2、为什么更多使用DLX解决

对于两种方式,TTL方式的延时队列如果你传递的是两个不同的等待时间在队列,后面的消息在延时队列中时间如果小于前面的队列等待时间也不会先执行,会按照队列的方式一个一个出队。

3、SpringBoot整合RabbitMq订单延迟取消实战(DLX方式实现延时队列)

引入maven依赖

<!-- rabbitMQ使用 -->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

mq地址配置

#rabbitmq配置
spring.rabbitmq.host= mq地址
spring.rabbitmq.username=用户名
spring.rabbitmq.password=密码
spring.rabbitmq.virtual-host=/
#spring.jackson.serialization.fail-on-empty-beans=false
#spring.rabbitmq.listener.simple.acknowledge-mode=manual

配置DLX方式的RabbitMq延时队列

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

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.amqp.SimpleRabbitListenerContainerFactoryConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration
public class RabbitmqConfiguration {

	
	@Bean("mallRabbitmqTaskContainerFactory")
    public SimpleRabbitListenerContainerFactory mallRabbitmqTaskContainerFactory(
    		SimpleRabbitListenerContainerFactoryConfigurer configurer, ConnectionFactory 
                connectionFactory) {
        SimpleRabbitListenerContainerFactory factory = new     
               SimpleRabbitListenerContainerFactory();
        factory.setPrefetchCount(1); 		//	每个消费者预取数量
        factory.setConcurrentConsumers(3);  //	并发消费者数量
        configurer.configure(factory, connectionFactory);
        return factory;
    }
	
	
	
	
	/**
	 * 订单支付超时相关延迟队列
	 */
	//	订单支付-延迟队列(死信队列)
	@Value("${mall.order.paytimeout.delay.queue}")
	private String mall_order_paytimeout_delay_queue;      
	
	//	订单支付延迟转发队列(死信转发队列)
	@Value("${mall.order.paytimeout.transpond.queue}")
	private String mall_order_paytimeout_transpond_queue;  
	
	private static final String ORDER_PAYTIMEOUT_DELAY_EXCHANGE = 
                          "mall.order.paytimeout.delay.exchange";
	private static final String ORDER_PAYTIMEOUT_TRANSPOND_EXCHANGE = 
                          "mall.order.paytimeout.transpond.exchange";
	
	
	/**
     * 	延迟(死信)队列交换机
     *
     * @return
     */
    @Bean
    public DirectExchange orderPaytimeoutDelayExchange() {
        return new DirectExchange(MallRabbitmqConfiguration.ORDER_PAYTIMEOUT_DELAY_EXCHANGE);
    }
    
	
	/**
	 * 	支付超时-延迟队列
	 * @return
	 */
	@Bean
	public Queue orderPaytimeoutDelayQueue() {
		Map<String, Object> argsMap = new HashMap<String, Object>();
		argsMap.put("x-dead-letter-exchange", RabbitmqConfiguration .ORDER_PAYTIMEOUT_TRANSPOND_EXCHANGE); // 死信转发的队列交换机
		argsMap.put("x-dead-letter-routing-key", this.mall_order_paytimeout_transpond_queue); //  死信转发队列的routing-key
		
		Queue queue = new Queue(this.mall_order_paytimeout_delay_queue, 
				true, false, false, argsMap);
		return queue;
	}
	
	/**
	 * 	延迟交换机和队列绑定
	 * @return
	 */
	@Bean
	public Binding orderPaytimeoutDelayQueueBindingExchange() {
		Binding binding = BindingBuilder.bind(orderPaytimeoutDelayQueue())	//	queue
				.to(orderPaytimeoutDelayExchange())							//	exchange
				.with(this.mall_order_paytimeout_delay_queue);				//	routingKey
		return binding;
	}
	
	
	/**
	 * 	延迟转发交换机
	 * @return
	 */
	@Bean
	public DirectExchange orderPaytimeoutTranspondExchange() {
		 return new DirectExchange(MallRabbitmqConfiguration.ORDER_PAYTIMEOUT_TRANSPOND_EXCHANGE);
	}
	
	/**
	 * 	支付超时-延迟转发队列
	 * @return
	 */
	@Bean
	public Queue orderPaytimeoutTranspondQueue() {
		Queue queue = new Queue(this.mall_order_paytimeout_transpond_queue, 
				true, false, false);
		return queue;
	}
	
	/**
	 * 	支付超时-延迟转发队列和延迟转发交换机绑定
	 * @return
	 */
	@Bean
	public Binding orderDelayTranspondQueueBindExchange() {
		Binding binding = BindingBuilder.bind(orderPaytimeoutTranspondQueue())  //	queue
			.to(orderPaytimeoutTranspondExchange())								//	exchange
			.with(this.mall_order_paytimeout_transpond_queue);					//	routingKey
		return binding;
	}
}

队列监听消费

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.alibaba.fastjson.JSONObject;
import com.kingseok.hospital.business.mall.model.order.KmMallOrder;
import com.kingseok.hospital.business.mall.service.manager.MallOrderServiceManager;
import com.kingseok.hospital.common.enums.OrderStatusEnum;
import com.kingseok.hospital.common.enums.PayStatusEnum;
import com.kingseok.hospital.common.utils.DateUtils;
import com.rabbitmq.client.Channel;


/**
 * 	快马商城订单延迟队列-消费者
 * @author Administrator
 *
 */
@Component
public class MallOrderDelayConsumer {

	private static final Logger _logger = LoggerFactory.getLogger(MallOrderDelayConsumer.class);
	
	@Autowired
	private MallOrderDelayProducer mallOrderDelayProducer;
	
	@Autowired
	private MallOrderServiceManager mallOrderServiceManager;
	
	
	/**
	 * 	订单支付超时队列消息处理
	 * @param message
	 * @param msg
	 * @param channel
	 * @throws IOException 
	 */
	@RabbitHandler
	@RabbitListener(queues = "${mall.order.paytimeout.transpond.queue}", containerFactory = "mallRabbitmqTaskContainerFactory")
	public void orderPaytimeoutProcess(String data, Message message, Channel channel) throws IOException {
		_logger.info("MallOrderDelayConsumer : orderPaytimeoutProcess -> 订单支付超时列队消息处理,接收到的消息数据data={}", data);
		try {
			JSONObject dataJson = JSONObject.parseObject(data);
			//Long userId = dataJson.getLong("userId");
			Long orderId = dataJson.getLong("orderId");
			Date sendTime = dataJson.getDate("sendTime");
			Date now = DateUtils.getCurrentDate();
			
			long interval = now.getTime() - sendTime.getTime();
			long condition = 30 * 60 * 60 * 1000;
			
			KmMallOrder order = mallOrderServiceManager.getOrder(orderId);
			//	如果订单查找不到而且 消息发送时间与当前时间间隔超过30小时,则直接丢弃
			if(order == null && interval >= condition) { return; }
			
			//	如果订单查找失败,且消息发送时间与当前时间间隔不超过30小时,则再次进入队列\
			if(order == null && interval < condition) {
				mallOrderDelayProducer.sendOrderToUnpaidTranspondQueue(data);
			}
			
			//	如果订单不是未支付状态,则表示订单已经支付或者有其他处理,则什么也不处理
			if(order != null && OrderStatusEnum.WAITING_TO_PAY.getValue() != order.getOrderStatus()) { return; }
			
			//	订单还没支付,修改订单状态为【关闭】
			KmMallOrder updateOrder = new KmMallOrder();
			updateOrder.setOrderId(orderId);
			updateOrder.setOrderStatus(OrderStatusEnum.CLOSED.getValue());
			updateOrder.setOrderClosedTime(DateUtils.getCurrentDate());
			updateOrder.setOrderClosedReason("订单超时未支付关闭");
			updateOrder.setOrderRemark("订单超时未支付关闭");
			updateOrder.setPayStatus(PayStatusEnum.PAYTIMEOUT.getValue());
			updateOrder.setUpdateTime(now);
			
			mallOrderServiceManager.updateOrder(updateOrder);
			
			//	手动确认
			//channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
		} catch (Exception e) {
			_logger.info("MallOrderDelayConsumer : orderPaytimeoutProcess -> 订单超时支付消费处理失败,订单支付超时消息补发再次进入延迟转发队列【mall.order.paytimeout.transpond.queue】!");
    		_logger.error("MallOrderDelayConsumer : orderPaytimeoutProcess -> 订单超时支付消费处理失败, 出错原因={}", e.getMessage());
			//mallOrderDelayProducer.sendOrderToUnpaidTranspondQueue(data);
    		channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
		}
	}
}

发送消息至延迟队列

@Component
public class MallOrderDelayProducer {

	private static final Logger _logger = LoggerFactory.getLogger(MallOrderDelayProducer.class);
	
	@Autowired
    private AmqpTemplate rabbitTemplate;
	
	/**
	 * 	订单支付超时相关延迟队列
	 */
	//	订单支付-延迟队列(死信队列)
	@Value("${mall.order.paytimeout.delay.queue}")
	private String mall_order_paytimeout_delay_queue;
	
	//	 订单支付超时时间
	@Value("${mall.order.paytimeout.interval}")
	private String paytimeoutInterval;
	
	//	订单延迟队列交换机
	private static final String ORDER_PAYTIMEOUT_DELAY_EXCHANGE = "mall.order.paytimeout.delay.exchange";
	
	
	
	/**
	 * 	发送 "待付款" 订单至延迟队列
	 * @param data
	 */
	public void sendOrderToUnpaidDelayQueue(String data) {
		_logger.info("MallOrderDelayProducer : sendOrderToUnpaidDelayQueue -> 发送【待付款】订单至延迟队列,订单数据data={}", data);
		MessagePostProcessor processor = new MessagePostProcessor() {

			@Override
			public Message postProcessMessage(Message message) throws AmqpException {
				//message.getMessageProperties().setContentType(MessageProperties.CONTENT_TYPE_JSON);
				//message.getMessageProperties().setContentEncoding("UTF-8");
				message.getMessageProperties().setExpiration(paytimeoutInterval);
				return message;
			}
		};
		rabbitTemplate.convertAndSend(MallOrderDelayProducer.ORDER_PAYTIMEOUT_DELAY_EXCHANGE, 
				this.mall_order_paytimeout_delay_queue, data, processor);
	}
}

第二种方式:利用RabbitMQ插件实现延迟队列

插件安装下载地址:https://www.rabbitmq.com/community-plugins.html ,下载rabbitmq_delayed_message_exchange插件,然后解压放置到RabbitMQ的插件目录。

然后,进入RabbitMQ的安装目录下的sbin目录,执行下面命令让该插件生效,然后重启RabbitMQ。

rabbitmq-plugins enable rabbitmq_delayed_message_exchange

配置声明

@Configuration
public class DelayedRabbitMQConfig {
    public static final String DELAYED_QUEUE_NAME = "delay.queue.demo.delay.queue";
    public static final String DELAYED_EXCHANGE_NAME = "delay.queue.demo.delay.exchange";
    public static final String DELAYED_ROUTING_KEY = "delay.queue.demo.delay.routingkey";

    @Bean
    public Queue immediateQueue() {
        return new Queue(DELAYED_QUEUE_NAME);
    }

    @Bean
    public CustomExchange customExchange() {
        Map<String, Object> args = new HashMap<>();
        args.put("x-delayed-type", "direct");
        return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message", true, false, args);
    }

    @Bean
    public Binding bindingNotify(@Qualifier("immediateQueue") Queue queue,
                                 @Qualifier("customExchange") CustomExchange customExchange) {
        return BindingBuilder.bind(queue).to(customExchange).with(DELAYED_ROUTING_KEY).noargs();
    }
}

controller层再添加一个入口:

@RequestMapping("delayMsg2")
public void delayMsg2(String msg, Integer delayTime) {
    log.info("当前时间:{},收到请求,msg:{},delayTime:{}", new Date(), msg, delayTime);
    sender.sendDelayMsg(msg, delayTime);
}

消息生产者的代码

public void sendDelayMsg(String msg, Integer delayTime) {
    rabbitTemplate.convertAndSend(DELAYED_EXCHANGE_NAME, DELAYED_ROUTING_KEY, msg, a ->{
        a.getMessageProperties().setDelay(delayTime);
        return a;
    });
}

创建一个消费者

@RabbitListener(queues = DELAYED_QUEUE_NAME)
public void receiveD(Message message, Channel channel) throws IOException {
    String msg = new String(message.getBody());
    log.info("当前时间:{},延时队列收到消息:{}", new Date().toString(), msg);
    channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: 在 Spring Boot 中,你可以通过 RabbitMQ 的 `x-delayed-message` 插件来实现延时队列,而不需要使用额外的插件或库。下面是实现步骤: 1. 添加依赖 在 `pom.xml` 文件中添加 RabbitMQ 的依赖: ```xml <dependency> <groupId>org.springframework.amqp</groupId> <artifactId>spring-rabbit</artifactId> <version>2.3.12.RELEASE</version> </dependency> ``` 2. 配置 RabbitMQ 的 `x-delayed-message` 插件 在 RabbitMQ 中,你需要先安装 `x-delayed-message` 插件。你可以通过 `rabbitmq-plugins` 命令来安装插件: ``` rabbitmq-plugins enable rabbitmq_delayed_message_exchange ``` 或者,你可以在 `rabbitmq.conf` 文件中添加以下配置,然后重启 RabbitMQ: ``` plugins.rabbitmq_delayed_message_exchange = {git, "https://github.com/rabbitmq/rabbitmq-delayed-message-exchange", {branch, "master"}} ``` 3. 配置 RabbitMQ 的连接信息 在 `application.properties` 中添加 RabbitMQ 的连接信息: ```properties spring.rabbitmq.host=your-rabbitmq-host spring.rabbitmq.port=5672 spring.rabbitmq.username=your-rabbitmq-username spring.rabbitmq.password=your-rabbitmq-password ``` 4. 定义队列和交换器 在 Spring Boot 中,你可以使用 `@Configuration` 和 `@Bean` 注解来定义队列和交换器。下面是一个例子: ```java @Configuration public class RabbitConfig { @Bean public Queue delayedQueue() { return QueueBuilder.durable("delayed.queue") .withArgument("x-dead-letter-exchange", "normal.exchange") .withArgument("x-dead-letter-routing-key", "normal.routingkey") .build(); } @Bean public CustomExchange delayedExchange() { Map<String, Object> args = new HashMap<>(); args.put("x-delayed-type", "direct"); return new CustomExchange("delayed.exchange", "x-delayed-message", true, false, args); } @Bean public Binding binding() { return BindingBuilder.bind(delayedQueue()) .to(delayedExchange()) .with("delayed.routingkey") .noargs(); } } ``` 在上面的例子中,我们定义了一个 `delayedQueue` 队列,它的死信交换器是 `normal.exchange`,死信路由键是 `normal.routingkey`。我们还定义了一个 `delayedExchange` 交换器,它的类型是 `x-delayed-message`,并将 `x-delayed-type` 属性设置为 `direct`。最后,我们将 `delayedQueue` 队列绑定到 `delayedExchange` 交换器上,并使用路由键 `delayed.routingkey`。 5. 发送延时消息 你可以使用 `RabbitTemplate` 类来发送消息到 `delayedQueue` 队列。在发送消息时,你需要将消息的 `headers` 属性设置为 `x-delay`,并将值设置为消息的延时时间(单位为毫秒)。 ```java @Autowired private RabbitTemplate ### 回答2: 在Spring Boot中实现RabbitMQ延时队列需要以下几个步骤: 1. 首先,我们需要定义一个交换机(Exchange),用于将消息发送到延时队列中。可以使用DirectExchange、TopicExchange或FanoutExchange等不同类型的交换机。交换机的类型根据具体的业务需求而定。 2. 接下来,我们需要定义两个队列,一个为延时队列,另一个为业务队列延时队列用于接收需要延时处理的消息,业务队列用于接收延时队列中处理完成的消息。 3. 创建并配置消息发送和接收的相关组件。使用RabbitTemplate来发送消息到延时队列,创建一个消费者来接收延时队列中的消息并处理。 4. 在消息发送时,可以通过给消息设置不同的过期时间来实现延时功能。在发送消息时,将消息携带的延时时间设置为过期时间,然后发送到延时队列中。 5. 在消费者中,监听业务队列,当接收到延时队列中的消息时,进行相应的处理,例如发送邮件、生成报表等。 这样就实现RabbitMQ延时队列的功能。通过设置消息的过期时间,可以控制消息何时被消费。延时队列可以在某个特定的时间点将消息转发到业务队列,完成后续处理。Spring Boot提供了简单而强大的集成,可以轻松实现延时队列的功能。 ### 回答3: 实现RabbitMQ延时队列的核心思想是利用RabbitMQ的插件(x-delayed-message)和Spring Boot的消息中间件(RabbitTemplate)结合使用。 首先,确保在RabbitMQ服务中安装了插件。在RabbitMQ的安装目录下,执行以下命令: ``` rabbitmq-plugins enable rabbitmq_delayed_message_exchange ``` 接下来,在Spring Boot项目的pom.xml文件中添加RabbitMQ的依赖: ```xml <dependencies> <!-- RabbitMQ --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> </dependencies> ``` 然后,创建一个配置类,用于连接RabbitMQ服务和创建延时队列: ```java @Configuration public class RabbitMQConfig { @Autowired private Environment env; @Bean public ConnectionFactory connectionFactory() { CachingConnectionFactory connectionFactory = new CachingConnectionFactory(); connectionFactory.setAddresses(env.getProperty("spring.rabbitmq.addresses")); connectionFactory.setUsername(env.getProperty("spring.rabbitmq.username")); connectionFactory.setPassword(env.getProperty("spring.rabbitmq.password")); return connectionFactory; } @Bean public RabbitTemplate rabbitTemplate() { RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory()); rabbitTemplate.setMessageConverter(jsonMessageConverter()); return rabbitTemplate; } @Bean public MessageConverter jsonMessageConverter() { return new Jackson2JsonMessageConverter(); } @Bean public Exchange delayedExchange() { Map<String, Object> args = new HashMap<>(); args.put("x-delayed-type", "direct"); return new CustomExchange("delayed-exchange", "x-delayed-message", true, false, args); } @Bean public Queue delayedQueue() { return new Queue("delayed-queue", true); } @Bean public Binding delayedBinding() { return BindingBuilder.bind(delayedQueue()).to(delayedExchange()).with("delayed-routing-key").noargs(); } } ``` 在上述代码中,我们创建了一个自定义的Exchange,将其类型设置为"x-delayed-message",并创建了一个延时队列,将其绑定在这个Exchange上。这样,消息发送到这个Exchange时,会根据消息中的延时时间属性进行延时处理。 最后,我们可以通过RabbitTemplate发送延时消息: ```java @Service public class RabbitMQService { @Autowired private RabbitTemplate rabbitTemplate; public void sendDelayedMessage(String message, int delayTime) { rabbitTemplate.convertAndSend("delayed-exchange", "delayed-routing-key", message, new MessagePostProcessor() { @Override public Message postProcessMessage(Message message) throws AmqpException { message.getMessageProperties().setHeader("x-delay", delayTime); return message; } }); } } ``` 在上述代码中,我们通过rabbitTemplate将消息发送到名为"delayed-exchange"的Exchange上,并设置消息的延时时间属性"x-delay"。最后,通过"convertAndSend"方法发送消息。 以上就是使用Spring Boot实现RabbitMQ延时队列的简单示例。通过这种方式,我们可以轻松地实现消息的延时处理,使得系统更加灵活和高效。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值