全新视角:带你重新认识订单失效处理

前言

随着电子商务和在线服务平台的快速发展,订单管理成为了现代软件系统中的一个重要组成部分。在这些系统中,订单的状态管理和过期处理是一个常见且重要的问题。当用户下单后,如果在一定时间内没有完成支付或其他必要操作,订单就需要被标记为过期并进行相应的处理,如释放库存、取消订单等。

因此,我总结了几种常用的实现订单过期处理的方法。接下来,让我们具体来看看几种方式的具体实现吧:

  1. 使用 JDK 自带的 DelayQueue:适用于小型系统或测试环境,不需要分布式支持。
  2. 使用 Redis 过期机制:适用于高性能要求的系统,需要分布式支持。
  3. 使用 Spring Schedule:适用于已经使用Spring框架的系统,希望利用其集成优势。
  4. 使用消息队列(MQ) :适用于需要高可靠性和高并发处理能力的大型分布式系统。

通过对比分析,我们将帮助你理解每种方法的特点,并根据实际情况作出合理的选择。希望本文能够为你提供一个全面的视角,让你在面对订单过期处理问题时有更多的思路和工具可供选择。

一、JDK自动的DelayQueue

1.1、代码实现

1、订单类实现

java 复制代码
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
​
public class Order implements Delayed {
    private String orderId;
    private long expirationTime;
​
    public Order(String orderId, long delayInMillis) {
        this.orderId = orderId;
        this.expirationTime = System.currentTimeMillis() + delayInMillis;
    }
​
    public String getOrderId() {
        return orderId;
    }
​
    @Override
    public long getDelay(TimeUnit unit) {
        long delay = expirationTime - System.currentTimeMillis();
        return unit.convert(delay, TimeUnit.MILLISECONDS);
    }
​
    @Override
    public int compareTo(Delayed o) {
        return Long.compare(this.expirationTime, ((Order) o).expirationTime);
    }
}

2、订单超时处理类实现

java 复制代码
package org.example.order.delay;
​
import java.util.concurrent.DelayQueue;
​
public class OrderProcessor implements Runnable {
    private DelayQueue<Order> delayQueue;
​
    public OrderProcessor(DelayQueue<Order> delayQueue) {
        this.delayQueue = delayQueue;
    }
​
    @Override
    public void run() {
        try {
            while (!Thread.currentThread().isInterrupted()) {
                Order order = delayQueue.take();
                handleOrderExpiry(order);
                System.out.println("订单超时:" + order.getOrderId());
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
​
    private void handleOrderExpiry(Order order) {
        // 处理订单过期逻辑
        System.out.println("处理订单超时:" + order.getOrderId());
        cancelOrder(order);
    }
​
    private void cancelOrder(Order order) {
        // 取消订单的逻辑
        System.out.println("取消订单:" + order.getOrderId());
        // 例如,更新数据库状态、发送邮件通知等
    }
}

3、主任务运行

java 复制代码
package org.example.order.delay;
​
import java.util.concurrent.DelayQueue;
​
public class Main {
    public static void main(String[] args) {
        DelayQueue<Order> delayQueue = new DelayQueue<>();
        OrderProcessor orderProcessor = new OrderProcessor(delayQueue);
        Thread processorThread = new Thread(orderProcessor);
        processorThread.start();
​
        // 添加一些订单到延迟队列
        addOrderToQueue(delayQueue, "order1", 5000); // 5秒后超时
        addOrderToQueue(delayQueue, "order2", 10000); // 10秒后超时
​
        // 为了演示目的,让主线程休眠一段时间
        try {
            Thread.sleep(15000); // 15秒
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            e.printStackTrace();
        }
​
        // 中断后台线程并结束
        processorThread.interrupt();
    }
​
    private static void addOrderToQueue(DelayQueue<Order> delayQueue, String orderId, long delayInMillis) {
        delayQueue.put(new Order(orderId, delayInMillis));
    }
}

1.2、优缺点

优点:

  1. 简单,不需要借助第三方组件,成本低

缺点

  1. 所有超时订单都加入到DelayQueue中,占用内存大。
  2. 没法做到分布式处理,只能在集群中的一个leader专门处理效率低
  3. 服务崩溃,数据直接丢失
  4. 不适合订单量大的场景

二、消息队列

2.1、具体实现

这里采用RabbitMQ简单实现。

1、添加依赖

xml 复制代码
        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>5.14.0</version>
        </dependency>

2、yaml配置

yaml 复制代码
spring:
  rabbitmq:
    host: localhost
    port: 5672
    password: guest
    username: guest
    listener:
      direct:
        acknowledge-mode: manual
      simple:
        retry:
          enabled: true #开启重试机制
          initial-interval: 1000ms #初始失败等待时长
          multiplier: 2 #重试间隔倍数值
          max-attempts: 3 #最大重试次数
​

3、初始化队列和死信交换机

java 复制代码
package org.example.order;
​
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
​
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;
​
public class QueueInitializer {
​
    private static final String ORDER_QUEUE_NAME = "order_queue";
    private static final String DLX_EXCHANGE_NAME = "dlx_exchange";
    private static final String TIMEOUT_QUEUE_NAME = "timeout_queue";
​
    public static void initializeQueues() throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
​
            // 创建带有TTL的普通队列
            Map<String, Object> args = new HashMap<>();
            args.put("x-message-ttl", 15000); // 15秒后消息过期
            args.put("x-dead-letter-exchange", DLX_EXCHANGE_NAME);
            args.put("x-dead-letter-routing-key", "timeout");
            channel.queueDeclare(ORDER_QUEUE_NAME, false, false, false, args);
​
            // 创建DLX
            channel.exchangeDeclare(DLX_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
            channel.queueDeclare(TIMEOUT_QUEUE_NAME, false, false, false, null);
            channel.queueBind(TIMEOUT_QUEUE_NAME, DLX_EXCHANGE_NAME, "timeout");
        }
    }
​
    public static void main(String[] args) throws IOException, TimeoutException {
        initializeQueues();
    }
}

4、发布消息

java 复制代码
package org.example.order;
​
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
​
import java.io.IOException;
import java.util.concurrent.TimeoutException;
​
public class MessagePublisher {
​
    private static final String ORDER_QUEUE_NAME = "order_queue";
​
    public static void publishMessage(String message) throws IOException, InterruptedException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
​
            // 发布消息
            channel.basicPublish("", ORDER_QUEUE_NAME, null, message.getBytes());
            System.out.println(" [x] Sent '" + message + "'");
        } catch (TimeoutException e) {
            throw new RuntimeException(e);
        }
    }
​
    public static void main(String[] args) throws IOException, InterruptedException {
        String message = "Order ID: 12345";
        publishMessage(message);
    }
}

5、队列监听,等待消息过期后进行消费(这里是15秒后过期)

java 复制代码
package org.example.order;
​
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
​
import java.io.IOException;
import java.util.concurrent.TimeoutException;
​
public class MessagePublisher {
​
    private static final String ORDER_QUEUE_NAME = "order_queue";
​
    public static void publishMessage(String message) throws IOException, InterruptedException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
​
            // 发布消息
            channel.basicPublish("", ORDER_QUEUE_NAME, null, message.getBytes());
            System.out.println(" [x] Sent '" + message + "'");
        } catch (TimeoutException e) {
            throw new RuntimeException(e);
        }
    }
​
    public static void main(String[] args) throws IOException, InterruptedException {
        String message = "Order ID: 12345";
        publishMessage(message);
    }
}

2.2、优缺点分析

优点

  1. 使用简单
  2. 支持分布式
  3. 精度高,支持任意时刻

缺点

  1. 使用限制:定时时长最大值24小时
  2. 成本高:每个订单都要新增一个定时任务,且不会马上消费,给MQ带来很大的存储成本
  3. 同一个时刻大量消息会导致消息延迟:给系统压力很大,导致消息分发延迟。

三、Redis过期监听

3.1、具体实现

通过过期时间和订阅过期事件来实现。

实现步骤:

1、添加Jedis依赖

xml 复制代码
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>

2、新建一个Jedis连接客户端

csharp 复制代码
package org.example.order.redis;

import redis.clients.jedis.Jedis;

public class RedisClient {
    private static Jedis jedis;

    static  {
        jedis = new Jedis("localhost"); // 替换为你的Redis服务器地址
        System.out.println("Connected to Redis server.");
    }

    public static Jedis getJedis() {
        return jedis;
    }
}

3、发布订单

java 复制代码
package org.example.order.redis;

import redis.clients.jedis.Jedis;

public class OrderExpirySetter {

    public static void setOrderExpiry(String orderId, int ttlInSeconds) {
        Jedis jedis = RedisClient.getJedis();
        try {
            // 设置订单的过期时间
            jedis.set(orderId, "active");
            jedis.expire(orderId, ttlInSeconds);

            // 发布过期事件
            jedis.publish("order-expiry-channel", orderId);
            System.out.println("发布订单成功:" + orderId);
        } finally {
            // 关闭连接
            if (jedis != null) {
                jedis.close();
            }
        }
    }

    public static void main(String[] args) {
        String orderId = "order_12345";
        int ttlInSeconds = 15; // 设置订单过期时间为15秒
        setOrderExpiry(orderId, ttlInSeconds);
    }
}

4、过期事件订阅

typescript 复制代码
package org.example.order.redis;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPubSub;

public class OrderExpiryListener {

    public static void listenForOrderExpiry() {
        Jedis jedis = RedisClient.getJedis();
        try {
            // 订阅过期事件频道
            jedis.subscribe(new JedisPubSub() {
                @Override
                public void onMessage(String channel, String message) {
                    System.out.println("Received expiry event for order ID: " + message);
                    handleOrderExpiry(message);
                }
            }, "order-expiry-channel");
        } finally {
            // 关闭连接
            if (jedis != null) {
                jedis.close();
            }
        }
    }

    private static void handleOrderExpiry(String orderId) {
        // 处理订单过期逻辑
        System.out.println("Handling expiry for order ID: " + orderId);
        // 例如,取消订单、释放库存等
    }

    public static void main(String[] args) {
        listenForOrderExpiry();
    }
}

3.2、优缺点分析

优点

  1. 高性能

    • Redis 是一个内存数据库,读写速度非常快,适用于需要快速响应的场景。
    • 利用Redis的过期机制可以实现高效的过期通知。
  2. 简单易用

    • Redis 提供了简单的API来设置键值的过期时间,如 EXPIREPEXPIRE 命令。
    • 使用 PUBLISHSUBSCRIBE 机制可以方便地监听过期事件。
  3. 可扩展性强

    • Redis 支持集群部署,可以通过水平扩展来应对更高的负载。
    • 可以轻松地在多个节点之间复制数据,保证数据的一致性和可用性。
  4. 可靠性

    • Redis 提供了持久化机制(AOF 或 RDB),可以保证数据的安全性。
    • 过期事件可以通过 PUBLISH 发布到特定频道,由多个订阅者监听,确保不会错过任何过期通知。
  5. 灵活性

    • 可以根据业务需求灵活配置过期时间。
    • 可以通过不同的频道来区分不同类型的过期事件,便于管理和处理。

缺点

  1. 过期时间不精确

    • Redis 的过期机制是基于定时任务来检查和清理过期键的,因此过期时间并不是精确的。
    • 实际上,键可能会在设定的过期时间之后几秒钟才真正过期,这取决于Redis的内部调度机制。
  2. 内存消耗

    • Redis 是内存数据库,存储大量订单数据可能会导致较高的内存消耗。
    • 需要合理规划内存使用,特别是在高并发环境下。
  3. 过期事件处理延迟

    • 如果有大量的订单同时过期,Redis 的内部调度机制可能会导致过期事件的处理出现延迟。
    • 这种延迟可能会导致业务逻辑出现问题,特别是对于实时性要求很高的场景。
  4. 单点故障风险

    • 单个Redis实例可能会成为系统的单点故障,需要通过集群和主从复制等方式来提高系统的可用性。
    • 如果Redis实例宕机,可能会导致过期事件丢失,从而影响业务逻辑。
  5. 并发处理复杂

    • 如果需要在多个订阅者之间分发过期事件,需要考虑如何保证事件处理的一致性和幂等性。
    • 需要设计合理的并发处理机制来避免数据一致性问题。

四、任务调度

4.1、具体实现

这里的任务调度我们采用的是Spring环境下单JVM层面的SpringSchedule,实现步骤很简单,如下:

1、主类开启任务

less 复制代码
@SpringBootApplication
@EnableScheduling
public class ConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }

}

2、实现一个处理定时任务类

typescript 复制代码
package com.example.consumer.task;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class OrderExpiryScheduler {


    @Scheduled(fixedDelay = 5000) // 每隔5秒执行一次
    public void checkExpiredOrders() {
        System.out.println("定时任务,实现订单实现处理");
        handleOrderExpiry("123");
    }

    private void handleOrderExpiry(String orderId) {
        // 处理订单过期逻辑
        System.out.println("Handling expiry for order ID: " + orderId);
        // 例如,取消订单、释放库存等
    }
}

4.2、优缺点分析

优点

  1. 易于实现

    • Spring 提供了简洁的@Scheduled注解,使得定时任务的实现变得非常简单。
    • 不需要额外的中间件或服务,只需要在Spring应用中定义任务即可。
  2. 高度集成

    • 由于Spring Boot框架的广泛使用,将定时任务集成到现有的Spring应用中非常方便。
    • 可以直接访问Spring容器中的Bean,方便与其他服务交互。
  3. 灵活的调度选项

    • 支持多种调度方式,包括基于固定延时(fixedDelay)、基于固定频率(fixedRate)和基于Cron表达式的调度(cron)。
    • 可以根据业务需求选择最适合的调度方式。
  4. 易于维护和管理

    • 由于任务是在Spring容器内运行的,可以利用Spring的依赖注入和其他特性来简化任务的开发和维护。
    • 可以通过Spring的配置文件或环境变量动态调整任务的调度参数。
  5. 支持并发处理

    • 可以通过配置线程池来支持并发执行定时任务,提高处理效率。
    • 可以根据业务需求调整线程池大小,以适应不同的负载情况。
  6. 日志记录和异常处理

    • 可以利用Spring的AOP(面向切面编程)来增强定时任务的功能,例如记录日志和处理异常。
    • 可以在任务中添加日志输出,方便调试和监控任务执行情况。

缺点

  1. 单点故障

    • 如果整个Spring应用或执行定时任务的服务器出现故障,定时任务将无法执行。
    • 需要额外的容错机制来确保任务的高可用性。
  2. 调度精度受限

    • Spring定时任务的调度精度受线程池调度机制的影响,可能会有一定的延迟。
    • 特别是在高负载情况下,任务的实际执行时间可能会与预定时间有所偏差。
  3. 资源消耗

    • 如果定时任务非常频繁,可能会消耗较多的CPU和内存资源。
    • 需要合理配置任务的执行频率,以免影响应用的整体性能。
  4. 并发处理复杂度

    • 如果需要处理大量并发任务,需要仔细设计并发处理逻辑,以防止数据竞争和一致性问题。
    • 需要考虑事务处理和数据锁定机制,确保数据的一致性和完整性。
  5. 缺乏外部监控

    • 默认情况下,Spring定时任务的执行情况主要依赖于日志记录,缺乏外部监控工具的支持。
    • 可能需要额外的监控工具或框架来监控任务的执行状态。
  6. 部署灵活性较低

    • 依赖于Spring应用的启动和运行环境,如果需要在不同的环境中部署定时任务,可能需要重新打包和部署整个应用。
    • 对于需要动态调整任务调度的情况,可能需要重新启动应用。

五、总结

上述几种实现方式各有优缺点,选择哪种方式取决于具体的应用场景和需求。如果对性能要求不高且不需要分布式支持,可以考虑使用 DelayQueue;如果需要高性能和分布式支持,可以选择使用 Redis 过期机制;如果已经使用了 Spring 框架,并希望利用其集成优势,可以使用 Spring Schedule 实现定时任务。在实际应用中,还需要综合考虑系统的扩展性、可靠性和维护成本等因素。

推荐使用:

在大多数情况下,采用 MQ(消息队列)定时任务 来实现订单过期处理是比较合适的选择:

  • MQ:适用于大型分布式系统,需要高可靠性和高并发处理能力。
  • 定时任务(如Spring Schedule):适用于已经使用Spring框架的系统,希望利用其集成优势,并且对性能和扩展性要求不是特别高的场景。

最终选择哪种方式,需要综合考虑系统的具体需求、技术栈、团队经验和运维能力等因素。

相关推荐
小蜗牛慢慢爬行12 分钟前
有关异步场景的 10 大 Spring Boot 面试问题
java·开发语言·网络·spring boot·后端·spring·面试
ThisIsClark1 小时前
【后端面试总结】MySQL主从复制逻辑的技术介绍
mysql·面试·职场和发展
程序猿进阶2 小时前
深入解析 Spring WebFlux:原理与应用
java·开发语言·后端·spring·面试·架构·springboot
LCG元9 小时前
【面试问题】JIT 是什么?和 JVM 什么关系?
面试·职场和发展
GISer_Jing14 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试
m0_7482455214 小时前
吉利前端、AI面试
前端·面试·职场和发展
TodoCoder15 小时前
【编程思想】CopyOnWrite是如何解决高并发场景中的读写瓶颈?
java·后端·面试
Wyang_XXX16 小时前
CSS 选择器和优先级权重计算这么简单,你还没掌握?一篇文章让你轻松通关面试!(下)
面试
liyinuo201719 小时前
嵌入式(单片机方向)面试题总结
嵌入式硬件·设计模式·面试·设计规范
代码中の快捷键20 小时前
java开发面试有2年经验
java·开发语言·面试