美团二面:如何设计一个订单超时未支付关闭订单的解决方案?我说使用ElasticJob轮训判断。

订单超时未支付自动取消是一个典型的电商和在线交易业务场景,在该场景下,用户在购物平台上下单后,系统通常会为用户提供一段有限的时间来完成支付。如果用户在这个指定的时间窗口内没有成功完成支付,系统将自动执行订单取消操作。

当然类似的业务场景还有:

  • 我们预约钉钉会议后,钉钉会在会议开始前15分钟、5分钟提醒。
  • 淘宝收到货物签收之后,超过7天没有确认收货,会自动确认收货。
  • 未使用的优惠券有效期结束后,自动将优惠券状态更新为已过期。
  • 用户登录失败次数过多后,账号锁定一段时间,利用延迟队列在锁定期满后自动解锁账号。而针对这种业务需求,我们常见的两中技术方向即:定时轮训订单之后判断是否取消以及延迟队列实现。而到具体的技术方案主要有以下几种:

美团二面:如何设计一个订单超时未支付关闭订单的解决方案?我说使用ElasticJob轮训判断。-每日运维图片

本文主要介绍以下几种主流方案。

定时轮训(SpringBoot的Scheduled实现)

定时轮训的方式都是基于定时定任务扫描订单表,按照下单时间以及状态进行过滤,之后在进行判断是否在有效期内,如果不在,则取消订单。

如以下,我们使用SpringBoot中的定时任务实现:

我们先创建定时任务的配置,设置任务每隔5秒执行一次。

@Configuration
@EnableScheduling
public class CustomSchedulingConfig implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        ThreadPoolTaskScheduler threadPoolTaskScheduler = threadPoolTaskScheduler();
        taskRegistrar.setTaskScheduler(threadPoolTaskScheduler); // 设置自定义的TaskScheduler
        // 根据任务信息创建CronTrigger
        CronTrigger cronTrigger = new CronTrigger("0/5 * * * * ?");
        // 创建任务执行器(假设TaskExecutor是实现了Runnable接口的对象)
        MyTaskExecutor taskExecutor = new MyTaskExecutor();
        // 使用自定义的TaskScheduler调度任务
        threadPoolTaskScheduler.schedule(taskExecutor, cronTrigger);
    }

    @Bean(destroyMethod = "shutdown")
    public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(5); // 设置线程池大小
        scheduler.setThreadNamePrefix("scheduled-task-"); // 设置线程名称前缀
        scheduler.setAwaitTerminationSeconds(60); // 设置终止等待时间
        return scheduler;
    }
}

然后在MyTaskExecutor中实现扫描订单以及判断订单是否需要取消:

public class MyTaskExecutor implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 在 "+ LocalDateTime.now() +" 执行MyTaskExecutor。。。。。");
    }
}

运行结果如下:

美团二面:如何设计一个订单超时未支付关闭订单的解决方案?我说使用ElasticJob轮训判断。-每日运维图片

采用定时任务机制来实现实时监测并取消超时订单的方法相对直接易行,我们可以运用诸如Quartz、XXL-Job或Elastic-Job等成熟的定时任务框架进行集群部署,从而提升任务执行效能。然而,此类方案存在显著局限:

首先,定时轮询订单表的方式在订单数量庞大的情况下会对数据库带来持续且显著的压力,因为频繁地全表扫描无疑会增加I/O负担和CPU使用率。

其次,定时任务执行的间隔设定颇为棘手。若设定的间隔时间较长,可能会导致订单超时后的取消动作出现延迟,影响用户体验;相反,若时间间隔设置得过短,则会导致大量订单被重复扫描和判断,不仅浪费计算资源,还可能导致不必要的并发问题和事务冲突,尤其是在高并发交易的高峰期。

在实际应用中,针对大流量订单场景下的超时处理,往往更倾向于采用延迟队列技术而非简单的定时任务轮询,以实现更为精确、高效的超时逻辑处理。

关于SpringBoot的定时任务实现的几种方式,请参考:玩转SpringBoot:SpringBoot的几种定时任务实现方式

JDK的延迟队列

使用JDK自带的DelayQueue实现一个延迟队列并处理超时订单,首先我们需要定义一个实现了Delayed接口的订单对象类,然后创建DelayQueue实例并不断从队列中取出已超时的订单进行处理。

我们定义一个包含订单信息和延迟时间的订单类:

@Getter
public class DelayedOrder implements Delayed {

    private final String orderNo;
    private final long expireTimeMillis; // 订单超时时间戳(毫秒)

    public DelayedOrder(String orderNo, long delayInSeconds) {
        this.orderNo = orderNo;
        // 设置订单在当前时间多少秒后超时
        this.expireTimeMillis = System.currentTimeMillis() + delayInSeconds;
    }

    @Override
    public long getDelay(TimeUnit unit) {
        long remainingNanos = expireTimeMillis - System.currentTimeMillis();
        return unit.convert(remainingNanos, TimeUnit.MILLISECONDS);
    }

    @Override
    public int compareTo(Delayed other) {
        if (other == this) {
            return 0;
        }
        DelayedOrder t = (DelayedOrder) other;
        long d = (getDelay(TimeUnit.MILLISECONDS) - t.getDelay(TimeUnit.MILLISECONDS));
        return (d == 0) ? 0 : ((d < 0) ? -1 : 1);
    }

    // 其他订单属性及方法...

    // 处理订单取消的逻辑
    public void cancelOrder() {
        // 在这里调用实际的服务接口或方法取消订单
    }
}

然后我们就可以使用DelayQueue处理超时订单:

@Component
public class OrderDelayQueue {
    private final DelayQueue delayQueue = new DelayQueue();

    public void addOrderToQueue(DelayedOrder order) {
        delayQueue.put(order);
        System.out.println("订单 " + order.getOrderNo() + "在 "+LocalDateTime.now()+" 添加到延迟队列");
    }

    // 启动订单处理线程池
    @Autowired
    private ExecutorService executorService;

    @PostConstruct
    public void init() {
        executorService.execute(this::processOrders);
    }

    private void processOrders() {
        while (true) {
            try {
                DelayedOrder order = delayQueue.take(); // 从延迟队列中取出已经过期的订单
                System.out.println("订单 " + order.getOrderNo() + "在 "+ LocalDateTime.now() +" 取消");
                order.cancelOrder();
                // 在这里执行取消订单的逻辑,比如更新数据库状态等
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

我们实现一个创建订单的接口,模拟订单创建:

@RestController
@RequestMapping("orderDelay")
public class OrderDelayController {

    @Autowired
    private OrderDelayQueue orderDelayQueue;

    @PostMapping("add")
    public void addOrder() {
        // 202403221901   2秒后取消
        DelayedOrder delayedOrder = new DelayedOrder("202403221901", 2000);
        orderDelayQueue.addOrderToQueue(delayedOrder);
        // 202403221902 3秒后取消
        DelayedOrder delayedOrder1 = new DelayedOrder("202403221902", 3000);
        orderDelayQueue.addOrderToQueue(delayedOrder1);
        // 202403221903 5秒后取消
        delayedOrder = new DelayedOrder("202403221903", 6000);
        orderDelayQueue.addOrderToQueue(delayedOrder);

        delayedOrder = new DelayedOrder("202403221904", 8000);
        orderDelayQueue.addOrderToQueue(delayedOrder);
    }
}

请求接口,发现订单超过各自的时间之后,都超时了。当然真实场景是超时时间一致,只是订单创建时间不一致。

美团二面:如何设计一个订单超时未支付关闭订单的解决方案?我说使用ElasticJob轮训判断。-每日运维图片

基于JDK的DelayQueue实现的延迟队列解决取消超时订单的方案,相比较于定时轮训有如下优点:

  • DelayQueue基于优先级队列实现,内部使用了堆数据结构,插入和删除操作的时间复杂度为O(log n),对于大量订单的处理效率较高。
  • 相比于定期查询数据库的方式,DelayQueue将待处理的订单信息保留在内存中,减少了对数据库的访问频率,降低了IO压力。
  • DelayQueue是java.util.concurrent包下的工具类,本身就具备良好的线程安全特性,可以在多线程环境下稳定工作。
  • 但是因为DelayQueue是基于内存的,这也导致它在实现上有一定的缺点:

  • 所有待处理的订单信息都需要保留在内存中,对于大量订单,可能会造成较大的内存消耗。
  • 由于所有的超时信息都依赖于内存中的队列,如果系统崩溃或重启,未处理的订单信息可能丢失,除非有额外的持久化措施。
  • 时间轮算法

    在介绍时间轮算法实现取消超时订单功能之前,我们先来看一下什么是时间轮算法?

    时间轮算法(Time Wheel Algorithm)是一种高效处理定时任务调度的机制,广泛应用于各类系统如计时器、调度器等组件。该算法的关键理念在于将时间维度映射至物理空间,即构建一个由多个时间槽构成的循环结构,每个槽代表一个固定的时间单位(如毫秒、秒等)。

    时间轮实质上是一个具有多个槽位的环形数据结构,随着时间的推进,时间轮上的指针按照预先设定的速度(例如每秒前进一槽)顺时针旋转。每当指针移动至下一槽位时,系统会检视该槽位中挂载的所有定时任务,并逐一执行到期的任务。

    在时间轮中,每个待执行任务均与其触发时间点对应的时间槽关联。添加新任务时,系统会根据任务的期望执行时间计算出相应的槽位编号,并将任务插入该槽。对于未来执行的任务,计算所需等待的槽位数目,确保任务按时被处理。值得注意的是,时间轮设计为循环结构,意味着当指针到达最后一个槽位后会自动返回至第一个槽位,形成连续不断的循环调度。

    借助时间轮算法,定时任务的执行时间以相对固定的时间槽来表示,而非直接依赖于绝对时间。任务执行完毕后,系统会及时将其从时间轮中移除,同时,对于不再需要执行的任务,也可以在任何时候予以移除,确保整个调度系统的高效运作和实时响应。

    美团二面:如何设计一个订单超时未支付关闭订单的解决方案?我说使用ElasticJob轮训判断。-每日运维图片

    如上图为例,假设一个格子是1秒,则整个wheel能表示的时间段为8s,假设当前指针指向2,此时需要调度一个3s后执行的任务, 显然应该加入到(2+3=5)的方格中,指针再走3次就可以执行了;如果任务要在10s后执行,应该等指针走完一个round零2格再执行, 因此应放入4,同时将round(1)保存到任务中。检查到期任务应当只执行round为0的,格子上其他任务的round应减1.

    所以,我们可以使用时间轮算法去试一下延迟任务,用于实现取消超时订单。

    我们以Netty4为例,引入依赖:

    
        io.netty
        netty-all
        4.1.68.Final
    

    然后定义订单处理服务,在创建订单时定义订单超时时间,以及超时时取消订单。

    @Service
    public class OrderService {
    
        private final Map orderTimeouts = new HashMap();
        private final HashedWheelTimer timer = new HashedWheelTimer();
    
        public void createOrder(String orderId) {
            System.out.println("订单"+orderId+"在"+ LocalDateTime.now() +"创建成功.");
            // 创建订单,设置超时时间为5秒钟
            Timeout timeout = timer.newTimeout(new TimerTask() {
                @Override
                public void run(Timeout timeout) throws Exception {
                    // 超时处理逻辑,取消订单
                    cancelOrder(orderId);
                }
            }, Duration.ofSeconds(5).toMillis(), TimeUnit.MILLISECONDS);
            orderTimeouts.put(orderId, timeout);
        }
    
        public void cancelOrder(String orderId) {
            // 取消订单的逻辑
            orderTimeouts.remove(orderId);
            System.out.println(orderId+"订单超时,在"+ LocalDateTime.now() +"取消订单:" + orderId);
        }
    }

    我们定义订单创建接口,模拟订单创建:

    @RestController
    @RequestMapping("orderTimeWheel")
    public class OrderTimeWheelController {
    
        @Autowired
        private OrderService orderService;
    
        @PostMapping("/create")
        public String createOrder(String orderId) {
            orderService.createOrder(orderId);
            return "订单创建成功:" + orderId;
        }
    }

    我们分别请求接口,创建订单:

    美团二面:如何设计一个订单超时未支付关闭订单的解决方案?我说使用ElasticJob轮训判断。-每日运维图片

    可以看见,订单在5秒钟之后自动调用取消方法取消订单。

    基于时间轮实现延迟任务来取消超时订单有如下优点:

  • 时间轮算法能够高效地管理大量的定时任务,其执行时间与任务数量无关,因此非常适合处理大规模的定时任务。
  • 时间轮算法能够提供相对精确的超时控制,可以在指定的时间后执行任务或者取消任务,从而确保超时订单能够及时取消。并且时间轮算法允许灵活地管理时间间隔和超时时间,可以根据具体业务需求进行调整和优化。
  • 时间轮算法的实现相对简单,算法本身比较容易理解,且现有的实现库如Netty的HashedWheelTimer已经提供了成熟的实现,因此可以很方便地集成到现有的系统中。
  • 基于内存操作,减少一些IO压力。
  • 但是相对应的也存在一些缺点:

  • 时间轮算法需要维护一个槽的数据结构,因此会占用一定的内存和计算资源,对于一些资源受限的环境可能会存在一定的压力。同DelayQueue,在大量订单时会对内存造成较大的内存消耗。同时也会影响延迟精度。
  • 同时,如果系统崩溃或者重启,未处理的订单信息可能丢失,除非有额外的持久化措施。
  • Redis实现

    对于Redis实现延迟任务,常见的两种方案是使用有序集合(Sorted Set,通常简称为zset)和使用key过期监听。

    定时轮训有序集合

    利用有序集合的特性,即集合中的元素是有序的,每个元素都有一个分数(score)。在延迟任务的场景中,可以将任务的执行时间作为分数,将任务的唯一标识(如任务ID)作为集合中的元素。然后,定时轮询有序集合,查找分数小于当前时间的元素,这些元素即为已经到期需要执行的任务。执行完任务后,可以从有序集合中删除对应的元素。因此可以将订单的过期时间作为score,用于实现取消超时订单。

    引入Redis依赖:

    
        org.springframework.boot
        spring-boot-starter-data-redis
        2.7.0
    

    配置一下RedisTemplate:

    @Configuration
    public class RedisConfig {
    
        @Bean
        public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
            // 其余配置 如序列化等
            return new StringRedisTemplate(factory);
        }
    }

    创建订单创建以及自动取消服务:

    @EnableScheduling
    @Service
    public class OrderZSetService {
    
        @Autowired
        private RedisTemplate redisTemplate;
    
        // key: orders:timeout, value: order_id:order_expiration_time
        private static final String ORDER_TIMEOUT_SET_KEY = "orders:timeout";
    
        public void createOrder(String orderId) {
            System.out.println("订单"+orderId+"在"+ LocalDateTime.now() +"创建成功.");
            // 假设订单超时时间为5秒
            long expirationTime = 5 * 1000 + System.currentTimeMillis();
            redisTemplate.opsForZSet().add(ORDER_TIMEOUT_SET_KEY, orderId, expirationTime);
        }
    
        @Scheduled(fixedRate = 1000) // 每秒检查一次,实际频率根据业务需求调整
        public void checkAndProcessTimeoutOrders() {
            Long now = System.currentTimeMillis();
            Set range = redisTemplate.opsForZSet().rangeByScoreWithScores(ORDER_TIMEOUT_SET_KEY, 0, now);
    
            for (ZSetOperations.TypedTuple tuple : range) {
                String orderId = (String) tuple.getValue();
                if (tuple.getScore()  {
                message.getMessageProperties().setDelay(5 * 1000);
                return message;
            });
        }
    
        public void cancelOrder(String orderId) {
            // 在这里实现订单取消的实际逻辑
            System.out.println("订单" + orderId + " 在" + LocalDateTime.now() +"取消");
            // 更新订单状态、释放库存等操作...
        }
    }

    我们模拟创建订单请求:

    @RestController
    @RequestMapping("orderMq")
    public class MqOrderController {
    
        @Autowired
        private OrderMqService orderMqService;
    
        @PostMapping("/create")
        public String createOrder(String orderId) {
            orderMqService.createOrder(orderId);
            return "订单创建成功:" + orderId;
        }
    }

    美团二面:如何设计一个订单超时未支付关闭订单的解决方案?我说使用ElasticJob轮训判断。-每日运维图片

    可以看见订单过了5秒之后开始执行取消。

    使用延迟队列方案来实现订单超时取消等场景的优点:

  • 延迟队列能够在消息到达指定时间后立刻触发处理,减少不必要的轮询查询,提高了处理效率和实时性。
  • 订单超时处理是异步进行的,不会影响主线业务流程,有利于提升整体系统的响应速度和稳定性。
  • 延迟队列方案使得订单创建、支付和超时取消三个环节相互独立,有利于系统的模块化和扩展性
  • 当系统规模扩大时,可以通过增加消费者数量来应对更多的超时订单处理,实现水平扩展。
  • 但是也有一些缺点:

  • 高度依赖消息队列服务的可用性和稳定性,一旦消息队列出现故障,可能导致超时订单无法正常处理。
  • 延迟队列方案涉及更多的中间件配置和管理,增加了系统的复杂性。
  • 在分布式系统中,如果订单状态不仅在消息队列中维护,还要同步到数据库,需要额外保证消息队列处理和数据库操作的一致性。
  • 虽然大部分消息队列的延迟机制相当可靠,但仍有极小概率出现消息延迟到达或丢失的情况,需要有相应的容错和补偿机制。
  • 对于延迟队列,并非只有rabbitmq才有,RocketMQ也有延迟队列。在RocketMQ中,延迟消息的发送是通过设置消息的延迟级别来实现的。每个延迟级别都对应着一个具体的延迟时间,例如 1 表示 1 秒、2 表示 5 秒、3 表示 10 秒,以此类推。用户可以根据自己的需求选择合适的延迟级别。但是也可以看出他并没有支持的那么精确,如果想要精确的就必须使用RocketMQ的企业版,在企业版中可以自定义设置延迟时间。这里就不过多讲解,有兴趣的可以自己研究一下。

    基于RabbitMq的死信队列实现

    订单创建时,将订单信息发送到一个具有TTL的队列,当消息在队列中停留的时间超过了TTL(也就是订单的有效支付期限),消息就会变为死信。然后再配置队列,使得这些过期的死信消息被路由到一个预先设置好的死信队列。最后创建一个消费者监听这个死信队列,一旦有消息进来(即订单超时),消费者便处理这些死信,检查订单状态并执行取消操作。

    使用的rabbitmq依赖以及配置同上使用延迟队列方案。我们来看一下创建处理订单即带有TTL的队列:

    @Configuration
    public class RabbitMQConfig {
    
        /**订单队列*/
        public static final String ORDER_QUEUE = "order.queue";
        /**死信队列交换机*/
        public static final String DEAD_LETTER_EXCHANGE = "order.deadLetter.exchange";
        /**死信队列*/
        public static final String DEAD_LETTER_QUEUE = "order.deadLetter.queue";
        /**死信路由*/
        public static final String ROUTING_KEY = "delayed-routing-key";
    
        /**
         * 创建订单队列
         * @return
         */
        @Bean
        public Queue orderQueue() {
            Map args = new HashMap();
            args.put("x-message-ttl", 5000L); // 设置订单队列消息有效期为30秒(可以根据实际情况调整)
            args.put("type", "java.lang.Long");
            args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE);
            args.put("x-dead-letter-routing-key", ROUTING_KEY);
            return new Queue(ORDER_QUEUE, true, false, false, args);
        }
    }

    同理也是需要先创建交换机:

    美团二面:如何设计一个订单超时未支付关闭订单的解决方案?我说使用ElasticJob轮训判断。-每日运维图片

    创建订单业务类,将订单发送到订单消息队列:

    @Service
    public class OrderMqService {
    
        private final AmqpTemplate rabbitTemplate;
    
        @Autowired
        public OrderMqService(AmqpTemplate rabbitTemplate) {
            this.rabbitTemplate = rabbitTemplate;
        }
    
       public void createOrder(String orderId) {
            System.out.println("订单"+orderId+"在"+ LocalDateTime.now() +"创建成功.");
            rabbitTemplate.convertAndSend(RabbitMQConfig.ORDER_QUEUE, orderId);
        }
    
        public void cancelOrder(String orderId) {
            // 在这里实现订单取消的实际逻辑
            System.out.println("订单" + orderId + " 在" + LocalDateTime.now() +"取消");
            // 更新订单状态、释放库存等操作...
        }
    
    }

    在创建死信队列,私信队列交换机,通过订单队列路由将私信队列绑定到订单订单队列中:

    @Configuration
    public class RabbitMQConfig {
    
        /**订单队列*/
        public static final String ORDER_QUEUE = "order.queue";
        /**死信队列交换机*/
        public static final String DEAD_LETTER_EXCHANGE = "order.deadLetter.exchange";
        /**死信队列*/
        public static final String DEAD_LETTER_QUEUE = "order.deadLetter.queue";
        /**死信路由*/
        public static final String ROUTING_KEY = "delayed-routing-key";
    
        /**
         * 创建死信队列交换机
         * @return
         */
        @Bean
        public DirectExchange deadLetterExchange() {
            return new DirectExchange(DEAD_LETTER_EXCHANGE);
        }
    
        /**
         * 创建死信队列
         * @return
         */
        @Bean
        public Queue deadLetterQueue() {
            return new Queue(DEAD_LETTER_QUEUE);
        }
    
        /**
         * 将死信队列与私信交换机绑定通过路由帮订单订单队列中
         * @return
         */
        @Bean
        public Binding bindingDeadLetterQueue() {
            return BindingBuilder.bind(deadLetterQueue()).to(deadLetterExchange()).with(ROUTING_KEY);
        }
    }

    在创建一个死信队列消息监听器,用于判断订单是否超时:

    @Component
    public class DelayedQueueListener {
    
        @Autowired
        private OrderMqService orderMqService;
    
        @RabbitListener(queues = RabbitMQConfig.DEAD_LETTER_QUEUE)
        public void handleDeadLetterOrder(String orderId) {
            orderMqService.cancelOrder(orderId);
        }
    }

    然后我们在订单创建时,将订单信息发送到订单MQ中,等消息的TTL到期之后,会自动转到死信队列中。

    @Service
    public class OrderMqService {
    
        private final AmqpTemplate rabbitTemplate;
    
        @Autowired
        public OrderMqService(AmqpTemplate rabbitTemplate) {
            this.rabbitTemplate = rabbitTemplate;
        }
    
        public void createOrder(String orderId) {
            System.out.println("订单"+orderId+"在"+ LocalDateTime.now() +"创建成功.");
            rabbitTemplate.convertAndSend(RabbitMQConfig.ORDER_QUEUE, orderId);
        }
    
        public void cancelOrder(String orderId) {
            // 在这里实现订单取消的实际逻辑
            System.out.println("订单" + orderId + " 在" + LocalDateTime.now() +"取消");
            // 更新订单状态、释放库存等操作...
        }
    
    }

    我们模拟创建订单接口:

    @RestController
    @RequestMapping("orderMq")
    public class MqOrderController {
    
        @Autowired
        private OrderMqService orderMqService;
    
        @PostMapping("/create")
        public String createOrder(String orderId) {
            orderMqService.createOrder(orderId);
            return "订单创建成功:" + orderId;
        }
    }

    美团二面:如何设计一个订单超时未支付关闭订单的解决方案?我说使用ElasticJob轮训判断。-每日运维图片

    可以看见订单过了5秒之后开始执行取消。

    使用死信队列实现取消超时订单的优点:

  • 死信队列可以捕获并隔离那些在原始队列中无法正常处理的消息,比如订单超时未支付等情况。这样有助于保障主业务流程不受影响,同时可以对异常情况进行统一管理和处理。
  • 通过设置消息TTL(Time-to-Live)或最大重试次数等条件,将无法正常处理的消息转移到死信队列,可以避免消息堆积导致的资源浪费,如内存、磁盘空间等。
  • 死信队列可以作为订单生命周期中特定阶段的处理通道,如订单超时后的处理流程,从而实现业务逻辑的清晰分离和模块化。
  • 所有的死信消息都被记录在死信队列中,方便跟踪和分析订单处理过程中出现的问题,也有助于完善系统的监控报警和数据分析。
  • 死信队列的处理过程也是异步进行的,不影响主线程的执行效率,增强系统的并发处理能力和响应速度。
  • 当然他也有一些缺点:

  • 相较于专门的延迟队列,死信队列机制通常不会自动将消息在特定时间后发出,需要通过设置消息TTL(过期时间)并在过期后触发转移至死信队列。这种方式对于精确到秒级别的超时处理不够友好,可能需要配合定时任务来检查即将超时的订单。
  • 死信队列的配置相对复杂,需要设置死信交换机、绑定关系以及消息TTL等,而且在处理死信时也需要额外的逻辑判断。
  • 如果没有妥善处理死信队列的消息,比如没有监听死信队列或者处理逻辑存在缺陷,可能会导致部分死信消息未被正确处理。
  • 在分布式环境下,如果订单状态不仅在消息队列中维护,还涉及到数据库的更新,那么需要保证消息队列与数据库之间的事务一致性。
  • 总结

    订单超时自动取消是电商平台中非常重要的功能之一,通过合适的技术方案,可以实现自动化处理订单超时的逻辑,提升用户体验和系统效率。本文讨论了多种实现订单超时自动取消的技术方案,包括定时轮询、JDK的延迟队列、时间轮算法、Redis实现以及MQ消息队列中的延迟队列和死信队列。

  • 定时轮询:基于SpringBoot的Scheduled实现,通过定时任务扫描数据库中的订单。优点是实现简单直接,但缺点是会给数据库带来持续压力,处理效率受任务执行间隔影响较大,且在高并发场景下可能引发并发问题和资源浪费。
  • JDK的延迟队列(DelayQueue):基于优先级队列实现,减少数据库访问,提供高效的任务处理。优点是内部数据结构高效,线程安全。缺点是所有待处理订单需保留在内存中,可能导致内存消耗大,且无持久化机制,系统崩溃时可能丢失数据。
  • 时间轮算法:通过时间轮结构实现定时任务调度,能高效处理大量定时任务,提供精确的超时控制。优点是实现简单,执行效率高,且有成熟实现库。缺点同样是内存占用和崩溃时数据丢失的问题。
  • Redis实现:
  • 有序集合(Sorted Set):利用有序集合的特性,定时轮询查找已超时的任务。优点是查询效率高,适用于分布式环境,减少数据库压力。缺点是依赖定时任务执行频率,处理超时订单的实时性受限,且在处理事务一致性方面需要额外努力。

    Key过期监听:利用Redis键过期事件自动触发订单取消逻辑。优点是实时性好,资源消耗少,支持高并发。缺点是对Redis服务的依赖性强,极端情况下处理能力可能成为瓶颈,且键过期有一定的不确定性。

  • MQ消息队列:
  • 延迟队列(如RabbitMQ的rabbitmq_delayed_message_exchange插件):实现消息在指定延迟后送达处理队列。优点是处理高效,异步执行,易于扩展,模块化程度高。缺点是高度依赖消息队列服务,配置复杂度增加,可能涉及消息丢失或延迟风险,以及消息队列与数据库操作一致性问题。

    死信队列:通过设置队列TTL将超时订单转为死信,由监听死信队列的消费者处理。优点是能捕获并隔离异常消息,实现业务逻辑分离,资源保护良好,方便追踪和分析问题。缺点是相比延迟队列,处理超时不够精确,配置复杂,且同样存在消息处理完整性及一致性问题。

    不同方案各有优劣,实际应用中应根据系统的具体需求、资源状况以及技术栈等因素综合评估,选择最适合的方案。在许多现代大型系统中,通常会选择消息队列的延迟队列或死信队列方案,以充分利用其异步处理、资源优化和扩展性方面的优势。