预订单过期,自动取消订单

236 阅读4分钟

一、业务场景:

在商城项目中,必定会遇到下单支付的功能:用户购买商品--提交订单(预订单)--支付页面(30分钟内支付)

对于这种场景,往往需要生成预订单,等待某个时间段之后,如果用户未支付,会失效/删除 这个预订单。

二、思路

预订单即任务

  1. 定时处理任务
  2. 延时处理任务

三、参考方法

1. 定时任务

  1. 用户下单后,创建一个定时任务,到期后去检查订单是否支付。
  2. 定时轮询一下数据库/缓存,查看订单状态。通过 当前时间-下单时间 判断是否过期等。

2. 延时任务

  1. java 自带延迟队列 DelayedQueue。jvm自带,缺点很明显:停机/重启 啪!数据没了。
  2. redis 监听过期key。适用于业务比较单一的场景。且redis集群模式需要做兼容处理。
  3. rabbitMQ 死信队列-原理: image.png

四、编码

1. 延迟队列方式

import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

/**
 * @author Agao
 * @date 2024/1/24 17:01
 */
public class DiyDelayQueue<T> implements Delayed {
    /**
     * 延迟时间
     */
    long delayTime;

    /**
     * 过期时间
     */
    long expire;

    /**
     * 任务数据
     */
    T data;

    /**
     * 有参构造,构造延迟队列中的数据源
     *
     * @param delayTime     延迟时间 eg:30
     * @param delayTimeUnit 单位 eg:分
     * @param t             数据 eg: 数据对象
     */
    public DiyDelayQueue(long delayTime, TimeUnit delayTimeUnit, T t) {
        // 转成时间毫秒数,结合时间戳使用
        this.delayTime = TimeUnit.MILLISECONDS.convert(delayTime, delayTimeUnit);
        // 过期时间
        this.expire = System.currentTimeMillis() + delayTime;
        this.data = t;
    }

    @Override
    public long getDelay(TimeUnit unit) {
        // 注意convert这个方法,第一个参数是一个long类型的数值,第二个参数的意思是告诉convert第一个long类型的值的单位是毫秒
        return unit.convert(this.expire - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    }

    @Override
    public int compareTo(Delayed o) {
        long f = this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS);
        return (int) f;
    }
}

2. 利用 Redis 定期失效key的特性--实现key过期的监听器

// 订单key 缓存,30分钟后过期
String key = orderId;
redisUtil.set(key,System.currentTimeMillis(), 30, TimeUnit.MINUTES);
/**
 * redis key 过期监听器
 *
 * @author Agao
 * @date 2024/1/18 17:22
 */
@Slf4j
@Component
public class RedisKeyExpireListener extends KeyExpirationEventMessageListener {

    public RedisKeyExpireListener(RedisMessageListenerContainer listenerContainer) {
        super(listenerContainer);
    }

    @Override
    public void onMessage(Message message, byte[] pattern) {
        String expiredKey = message.toString();
        log.info("redis key: {} expired", expiredKey);

        // 订单 超时未支付
        if (expiredKey.startsWith("按照你的订单规则")) {
            // 预订单过期,逻辑处理
        }

    }
}

3. rabbitMQ 死信队列

MQ配置文件

spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: admin
    password: admin
    template:
      retry:
        enabled: true
        initial-interval: 2s
    virtual-host: /
    listener:
      simple:
        # 每个队列启动的消费者数量
        concurrency: 1
        # 每个队列最大的消费者数量
        max-concurrency: 1
        # 手动签收ACK
        acknowledge-mode: manual
        # 每次从RabbitMQ获取的消息数量
        prefetch: 1
  1. 配置rabbitMQ
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

/**
 * MQ 配置类
 *
 * @author Agao
 * @date 2024/1/24 17:31
 */
@Configuration
public class RabbitMQConfig {

    /** 队列名称 */
    public static final String ORDER_QUEUE = "order_queue";

    /** 交换机名称 */
    public static final String ORDER_EXCHANGE = "order_exchange";

    /** 订单路由key */
    public static final String ROUTING_KEY_ORDER = "routing_key_order";

    /** 死信消息队列名称 */
    public static final String DEAL_QUEUE_ORDER = "deal_queue_order";

    /** 死信交换机名称 */
    public static final String DEAL_EXCHANGE_ORDER = "deal_exchange_order";

    /** 死信 routingKey */
    public static final String DEAD_ROUTING_KEY_ORDER = "dead_routing_key_order";

    @Bean
    public Queue orderQueue() {
      // 将普通队列绑定到死信队列交换机上
      return QueueBuilder.durable(ORDER_QUEUE)
          .deadLetterExchange(DEAL_EXCHANGE_ORDER)
          .deadLetterRoutingKey(DEAD_ROUTING_KEY_ORDER)
          .build();
    }

    // 声明一个direct类型的交换机
    @Bean
    public DirectExchange orderExchange() {
      return new DirectExchange(RabbitMQConfig.ORDER_EXCHANGE);
    }

    // 绑定Queue队列到交换机,并且指定routingKey
    @Bean
    public Binding bindingDirectExchange() {
      return BindingBuilder.bind(orderQueue()).to(orderExchange()).with(ROUTING_KEY_ORDER);
    }

    // 创建配置死信队列
    @Bean
    public Queue deadQueueOrder() {
      return QueueBuilder.durable(DEAL_QUEUE_ORDER).build();
    }

    // 创建死信交换机
    @Bean
    public DirectExchange deadExchangeOrder() {
      return new DirectExchange(DEAL_EXCHANGE_ORDER);
    }

    // 死信队列与死信交换机绑定
    @Bean
    public Binding bindingDeadExchange() {
      return BindingBuilder.bind(deadQueueOrder())
          .to(deadExchangeOrder())
          .with(DEAD_ROUTING_KEY_ORDER);
    }
}
  1. 生产者
@Slf4j
@RestController
public class OrderController {

    @Autowired private AmqpTemplate amqpTemplate;
    @Autowired private ObjectMapper objectMapper;

    public void preOrder(PreOrderRo ro) throws JsonProcessingException {
      log.info("生成预订单----orderId: {}", ro.getId());
      Order order = new Order();
      order.setId(UUID.randomUUID().toString());
      order.setPrice(ro.getPrice());
      order.setStatus(OrderStatus.WAIT_PAY.getCode());
      order.setCreateDate(System.currentTimeMillis());

      // 投递消息
      amqpTemplate.convertAndSend(
          RabbitMQConfig.ORDER_EXCHANGE,
          RabbitMQConfig.ROUTING_KEY_ORDER,
          objectMapper.writeValueAsString(order),
          message -> {
            // 也可以直接设置queue 的过期时间
            message.getMessageProperties().setExpiration(1000 * 10 + "");
            return message;
          });
      log.info("订单生产者结束----------");
    }


}
  1. 消费者
import com.agao.config.RabbitMQConfig;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Map;

/**
 * 订单过期监听器
 *
 * @author Agao
 * @date 2024/1/24 17:41
 */
@Slf4j
@Component
public class OrderExpireListener {

    @Autowired private ObjectMapper objectMapper;

    // 设置订单失效的队列
    @RabbitListener(queues = RabbitMQConfig.DEAL_QUEUE_ORDER)
    public void process(
        @Payload String orderMsg,
        Message message,
        @Headers Map<String, Object> headers,
        Channel channel)
        throws IOException {
      Order order = objectMapper.readValue(orderMsg, Order.class);
      log.info("order pay expire :{}", order);
      // 判断订单是否支付,做未支付的业务处理

      // 手动ack
      Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
      // 手动签收
      channel.basicAck(deliveryTag, false);
    }

}