最近在做电商项目时,订单需求要求用户在规定时间内(比如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-exchange和x-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);
}