真实系统开发:订单超时未支付自动关闭业务实现

在电商平台中,订单超时未支付需要自动关闭是一个常见的业务场景。这个问题通常需要通过延时任务来处理。

以下是几种常用的实现方式及其优缺点分析:

解决方案总览

1. 定时任务

实现方式

通过定时任务定期扫描数据库中的订单表,判断是否有超时未支付的订单,如果有则将订单状态更新为关闭。实现非常简单,可以通过几行代码在业务逻辑中增加一个定时任务来完成。

@Scheduled(fixedRate = 60000) // 每分钟执行一次
public void checkAndCloseExpiredOrders() {
    List<Order> expiredOrders = orderService.findExpiredOrders(); // 查询超时订单
    for (Order order : expiredOrders) {
        orderService.closeOrder(order); // 关闭订单
    }
}

在上述代码中,我们使用了 Spring 框架的 @Scheduled 注解,每分钟执行一次查询,找出所有超时未支付的订单并逐一关闭。

优点

  • 实现容易,成本低:只需要编写少量代码,无需引入复杂的第三方组件,易于维护。
  • 灵活性高:可以根据业务需要,调整定时任务的执行频率和扫描逻辑。

缺点

  • 时间不够精确:由于定时任务的执行间隔是固定的,可能导致一些订单过期后没有立即关闭。这对于一些对时间要求严格的场景可能不够理想。
  • 增加数据库压力:随着订单数量增加,扫描数据库的成本也会增大,可能会对系统性能产生影响,尤其是在大数据量的情况下,这种方式的效率会显著下降。

应用场景

定时任务适合以下场景:

  • 对时间要求不敏感:如订单关闭延迟几分钟对用户体验没有太大影响。
  • 数据量不大的场景:例如,中小型电商平台或者订单不频繁的业务。
  • 快速原型和小规模应用:适合在快速开发和小规模项目中使用,能够快速实现延时任务功能。

对于需要更高实时性和性能的场景,可以考虑使用其他解决方案,如分布式消息队列、Redis 过期监听等。

2. JDK 延迟队列 DelayQueue

实现方式

使用 JDK 自带的 DelayQueue 队列,可以很方便地实现延时任务处理。每个订单作为一个元素放入队列中,并设置过期时间。当订单过期时,可以从队列中获取并处理过期订单。以下是一个简单的实现示例:

public class OrderDelayQueue {
    private static DelayQueue<OrderDelayed> delayQueue = new DelayQueue<>();

    public static void main(String[] args) {
        // 添加订单到延迟队列
        delayQueue.put(new OrderDelayed("order123", 30, TimeUnit.MINUTES));
        new Thread(new OrderCloseTask()).start(); // 启动关闭任务线程
    }

    static class OrderCloseTask implements Runnable {
        @Override
        public void run() {
            while (true) {
                try {
                    // 从延迟队列中获取订单
                    OrderDelayed order = delayQueue.take(); 
                    System.out.println("Closing order: " + order.getOrderId());
                    // 执行关闭订单逻辑
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

在这个例子中,OrderDelayed 是一个实现了 Delayed 接口的类,用来表示订单及其延迟时间。OrderCloseTask 是一个不断从 DelayQueue 中取出过期订单并处理的线程。

优点

  • 不依赖第三方组件:实现简单,减少了对其他外部组件的依赖,适合快速开发和部署。
  • 时间精确:相较于定时任务,它能够更精确地处理订单的到期时间,几乎不会有延迟。

缺点

  • 内存限制DelayQueue 是无界队列,所有数据存储在 JVM 内存中。如果订单数量过多,会导致 JVM 内存溢出(OOM)。
  • 数据丢失风险DelayQueue 基于 JVM 内存,JVM 重启或者发生故障时,队列中的数据将会丢失,无法恢复。

应用场景

DelayQueue 适用于以下场景:

  • 数据量较小的场景:例如内部系统的一些非关键通知、临时性任务调度等。
  • 容忍数据丢失的场景:例如一些非关键性任务,即使任务丢失也不会对业务造成重大影响。
  • 快速开发与部署:适用于小型项目或一些实验性功能的快速上线,不需要复杂的基础设施支持。

对于数据量大且需要高可靠性的场景,不建议使用 DelayQueue,可以考虑基于 Redis、消息队列等其他解决方案。

3. Redis 过期监听

实现方式

Redis 提供了一种过期监听的机制,可以用来处理类似订单超时关闭的需求。在订单生成时,将订单信息存储在 Redis 中,并设置一个过期时间。当订单过期时,Redis 会触发一个过期事件(__keyevent@*__:expired),业务系统可以监听这个事件并执行相应的逻辑,比如关闭订单。

下面是一个简单的 Redis 过期监听实现示例,基于 Spring 的 RedisMessageListenerContainer 来监听 Redis 的 key 过期事件:

@Configuration
public class RedisConfig {
    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory factory) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(factory);
        container.addMessageListener(new KeyExpiredListener(), new PatternTopic("__keyevent@*__:expired")); // 监听所有过期事件
        return container;
    }

    // 监听器实现
    public class KeyExpiredListener implements MessageListener {
        @Override
        public void onMessage(Message message, byte[] pattern) {
            String expiredKey = message.toString();  // 获取过期的 key
            System.out.println("Order expired: " + expiredKey);
            // 根据 key 执行订单关闭逻辑
        }
    }
}

优点

  • 高性能:Redis 的高性能使得在设置 key 和监听 key 过期事件时都能迅速响应。
  • 消息实时性较好:相比于传统的定时任务和其他轮询方式,Redis 过期监听的实时性较高,能够在大部分场景下实现较为及时的消息处理。

缺点

  • 延迟问题:由于 Redis 的 key 过期策略,当一个 key 过期时,Redis 不一定能立即删除它,因此我们的监听事件可能会有一定的延迟。
  • 消息丢失风险:在 Redis 5.0 之前,发布订阅机制的消息并未持久化,没有确认机制,因此如果在消费消息过程中客户端宕机,这条消息就会丢失。
  • 无持久化机制:Redis 重启或服务器故障时,未被消费的消息会丢失。

应用场景

Redis 过期监听适用于以下场景:

  • 对实时性要求适中:适用于实时性要求适中(秒级)的场景,比如提醒、非核心任务的调度等。
  • 数据丢失容忍度较高:例如一些非关键业务场景,能够接受偶尔的消息丢失。
  • 轻量级任务处理:不需要依赖复杂的第三方组件,适用于中小型项目的轻量级任务处理。

对于需要高可靠性和消息持久化的场景,不推荐使用 Redis 过期监听,可以考虑其他更可靠的方案,如分布式延迟队列或消息队列(如 RabbitMQ 或 Kafka)来解决。

4. Redisson 分布式延迟队列

实现方式

Redisson 是一个基于 Redis 的 Java 驻内存数据网格解决方案,它提供了一系列分布式的 Java 常用对象,还提供了许多分布式服务。Redisson 的分布式延迟队列(RDelayedQueue)可以基于 Redis 的 ZSet 数据结构来实现延迟任务处理,适合用于高性能和高并发的任务场景。

使用 Redisson 的延迟队列可以很简单地实现订单的超时处理。以下是一个简单的实现示例:

// 初始化 Redisson 客户端
RedissonClient redisson = Redisson.create();

// 获取分布式队列
RQueue<Order> orderQueue = redisson.getQueue("orderQueue");

// 使用延迟队列
RDelayedQueue<Order> delayedQueue = redisson.getDelayedQueue(orderQueue);

// 添加订单到延迟队列,设置30分钟后过期
delayedQueue.offer(order, 30, TimeUnit.MINUTES);

// 处理延迟队列中的订单
new Thread(() -> {
    while (true) {
        Order expiredOrder = orderQueue.poll(); // 从队列中获取已到期订单
        if (expiredOrder != null) {
            System.out.println("Closing order: " + expiredOrder.getId());
            // 执行关闭订单逻辑
        }
    }
}).start();

优点

  • 简单易用:使用 Redisson 的 API 设计非常简洁,易于集成到 Java 应用中。
  • 高性能和高并发:得益于 Redis 的高性能和 Lua 脚本的原子性操作,Redisson 延迟队列在高并发场景下表现良好。
  • 数据持久化:基于 Redis 的存储机制,Redisson 延迟队列中的数据是持久化的,可以避免数据丢失问题。
  • 避免并发问题Redisson 使用 Lua 脚本来保证操作的原子性,确保不会出现并发处理问题。

缺点

  • 依赖 Redis:需要额外配置和管理 Redis 集群。
  • 需要学习和掌握 Redisson:对于没有使用过 Redisson 的开发者,需要一定的学习成本。

应用场景

Redisson 的分布式延迟队列适用于以下场景:

  • 高性能和高并发要求的场景:例如大型电商平台的订单超时处理、支付超时提醒等。
  • 数据可靠性要求较高的场景:例如需要持久化消息或数据的业务场景,避免因服务器故障或重启导致的数据丢失。
  • 分布式系统场景:需要分布式特性和易用性的项目,比如分布式任务调度、消息传递、缓存同步等。

5. RocketMQ 延迟消息

实现方式

RocketMQ 是阿里巴巴开源的分布式消息队列系统,支持高吞吐量、低延迟和高可靠性。在处理订单超时关闭场景时,可以使用 RocketMQ 的延迟消息特性。将订单信息作为消息写入 RocketMQ 队列,同时设置一个延迟时间(例如 30 分钟)。在延迟时间到达后,消费者会接收到消息并执行相应的处理逻辑,如关闭订单。

以下是一个简单的 RocketMQ 延迟消息的实现示例:

// 创建消息生产者
DefaultMQProducer producer = new DefaultMQProducer("order_producer_group");
producer.setNamesrvAddr("localhost:9876"); // 设置 RocketMQ 的 NameServer 地址
producer.start();

// 创建消息对象,指定主题和消息体
Message msg = new Message("orderTopic", "OrderTag", orderBytes);

// 设置延迟级别,3为30分钟(延迟时间级别可以在配置文件中调整)
msg.setDelayTimeLevel(3); 

// 发送延迟消息
SendResult sendResult = producer.send(msg);
System.out.println("Send delayed message: " + sendResult);

// 关闭生产者
producer.shutdown();

优点

  • 系统解耦:消息队列的引入可以使系统模块之间相互解耦,业务逻辑更加清晰,易于维护。
  • 高吞吐量RocketMQ 具有高吞吐量,可以支持亿级别的数据量,适用于大规模订单场景。
  • 延迟消息支持灵活:延迟级别可以通过配置调整,能够灵活满足不同业务场景的需要。

缺点

  • 重量级组件RocketMQ 是一个重量级组件,引入之后需要维护 NameServerBroker 等服务,增加了系统复杂度。
  • 消息丢失风险:尽管 RocketMQ 支持高可靠性,但仍需考虑消息丢失、重复消费、幂等性处理等问题。
  • 学习和维护成本:需要团队具备一定的 RocketMQ 相关知识,且需要定期维护和监控。

应用场景

RocketMQ 延迟消息适用于以下场景:

  • 大型分布式系统:如大型电商平台、金融系统等,需要处理海量订单的超时关闭、支付超时、库存扣减等场景。
  • 需要解耦和异步处理的业务:对于需要解耦和异步处理的业务场景,例如订单处理、消息通知、用户行为分析等,使用 RocketMQ 可以显著简化系统设计。
  • 高可靠性和高可用性要求RocketMQ 提供的高可靠性和集群高可用特性,非常适合在业务场景中有较高要求的应用。

总体来说,RocketMQ 适合需要高性能、高吞吐量和业务解耦的复杂场景,但在使用时需权衡其复杂性和维护成本。

基于自身实战的解决方案

在实际项目开发过程中,我参与过的订单支付超时处理方案主要采用了 Redis 过期监听和 RocketMQ 延迟消息的组合使用。该方案兼具了性能、实时性、可靠性等多个方面的优势,且能够在面对大规模订单处理时保持稳定。

1. 方案背景

在一个零售平台的订单系统中,我们需要确保订单在生成后,如果用户未在 30 分钟内完成支付,订单会自动关闭。这需要一个延迟任务处理机制,能够精准地在超时时间到达后执行关闭操作。同时,系统应能处理高并发的订单请求,具有良好的扩展性。

2. 解决方案设计:Redis 过期监听器+RocketMQ 延迟消息

综合多种方案的优缺点,我们最终设计了以下解决方案:

  1. 订单创建时将信息写入 Redis:在用户创建订单时,将订单信息(包括订单号、过期时间等)写入 Redis,并设置过期时间为 30 分钟。

  2. Redis 过期监听器监听订单超时:利用 Redis 的过期事件通知功能,监听订单键的过期事件。当 Redis 检测到订单键过期时,发布一个过期事件到指定频道。

  3. 使用 RocketMQ 延迟消息处理订单关闭:Redis 过期事件触发后,生成一条包含订单号的延迟消息,并将其投递到 RocketMQ 中,设置一个较短的延迟时间(如 5 秒)。RocketMQ 消费者接收到消息后,执行订单关闭的业务逻辑。

  4. RocketMQ 保证消息可靠性:通过 RocketMQ 的消息确认机制和持久化能力,确保在消费者处理消息时不丢失,能够精确地执行超时订单的关闭操作。

3. 代码实现

以下是关键部分的代码实现:

1. 订单创建时将信息写入 Redis

在订单创建时,将订单信息写入 Redis,并设置过期时间为 30 分钟。使用 Redis 锁来防止订单被重复占用。

// 订单服务中的创建订单方法
public boolean createOrder(String orderId) {
    String orderKey = "order:" + orderId + ":locked";

    // 设置 Redis 锁,过期时间为 30 分钟
    Boolean lockSuccess = redisTemplate.opsForValue().setIfAbsent(orderKey, "locked", 30, TimeUnit.MINUTES);
    if (lockSuccess != null && lockSuccess) {
        // 订单创建成功并成功获取到锁
        // .....
        log.info("订单 {} 创建成功,并设置为占用状态", orderId);
        return true;
    } else {
        // 订单已被占用或不存在
        log.warn("订单 {} 创建失败,已被占用或其他异常", orderId);
        return false;
    }
}

2. Redis 过期监听器监听订单超时

使用 Redis 的过期事件通知功能,监听订单键的过期事件,并在超时后触发相关操作。可以使用 RedisMessageListenerContainer 来监听 Redis 键的过期事件。

@Slf4j
@Component
public class KeyExpiredListener implements MessageListener {

    @Autowired
    private RocketMQProducer rocketMQProducer;

    @Override
    public void onMessage(Message message, byte[] pattern) {
        // 获取过期键
        String expiredKey = message.toString();
        log.info("接收到 Redis 键过期事件,过期键: {}", expiredKey);

        // 假设 Redis 键名格式是 "order:{orderId}:locked",提取订单 ID
        String orderId = extractOrderIdFromKey(expiredKey);
        if (orderId != null) {
            log.info("订单 {} 已超时,发送关闭消息到 RocketMQ", orderId);
            // 发送订单关闭消息到 RocketMQ
            rocketMQProducer.sendMessage("orderTopic", orderId);
        } else {
            log.warn("无法从过期键中提取订单ID: {}", expiredKey);
        }
    }

    // 假设 Redis 键名格式是 "order:{orderId}:locked",提取订单 ID
    private String extractOrderIdFromKey(String key) {
        if (key != null && key.startsWith("order:") && key.endsWith(":locked")) {
            return key.substring(6, key.length() - 7); // 提取订单ID
        }
        return null;
    }
}

3. RocketMQ 消息生产者(发送订单关闭消息)

当 Redis 键过期时,我们通过 RocketMQ 发送一条消息,通知消费者执行订单关闭操作。

@Slf4j
@Component
public class RocketMQProducer {

    @Autowired
    private DefaultMQProducer producer;

    public void sendMessage(String topic, String orderId) {
        try {
            Message msg = new Message(topic, orderId.getBytes());
            // 设置延迟级别(5秒)
            msg.setDelayTimeLevel(1);  // 假设延迟级别 1 对应 5秒延迟
            producer.send(msg);
            log.info("订单关闭消息发送成功,订单ID: {}", orderId);
        } catch (Exception e) {
            log.error("订单关闭消息发送失败,订单ID: {}", orderId, e);
        }
    }
}

4. RocketMQ 消息消费者(接收并处理订单关闭)

消费者会接收到从 RocketMQ 投递过来的订单关闭消息,并在接收到消息后执行关闭操作。

@Slf4j
@Component
public class OrderCloseConsumer {

    @RocketMQMessageListener(topic = "orderTopic", consumerGroup = "orderGroup")
    public void onMessage(String orderId) {
        // 执行关闭订单的业务逻辑
        closeOrder(orderId);
    }

    public void closeOrder(String orderId) {
        // 执行订单关闭的业务操作,例如更新数据库中的订单状态
        log.info("订单 {} 已关闭", orderId);
        // 这里可以调用订单服务进行订单的状态更新或其他相关操作
    }
}

5. Redis 过期事件通知配置

为了能够监听 Redis 键的过期事件,需要配置 RedisMessageListenerContainer 来监听键的过期事件。

@Configuration
public class RedisConfig {

    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory factory) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(factory);
        container.addMessageListener(new KeyExpiredListener(), new PatternTopic("__keyevent@*__:expired"));
        return container;
    }
}

6. 启动 RocketMQ 消息生产者

如果不想让 RocketMQ 作为 Spring Boot 项目的组件来管理,可以单独启动 RocketMQ 的生产者。

@Slf4j
public class RocketMQProducerApp {

    public static void main(String[] args) throws Exception {
        // 初始化生产者
        DefaultMQProducer producer = new DefaultMQProducer("orderProducerGroup");
        producer.setNamesrvAddr("localhost:9876");
        producer.start();
        log.info("RocketMQ producer started.");

        // 创建发送消息
        String orderId = "12345"; // 示例订单ID
        Message message = new Message("orderTopic", orderId.getBytes());
        producer.send(message);
        log.info("消息发送成功,订单ID: {}", orderId);

        // 停止生产者
        producer.shutdown();
    }
}

3.1 优缺点分析

优点

  1. 高效的资源利用
    • Redis 锁:通过 Redis 锁管理订单状态,能够快速且高效地检测订单是否已被占用,避免了多次重复下单的情况。
    • Redis 过期监听:使用 Redis 的过期监听机制,避免了额外的定时任务,且具备低延迟,能够即时响应订单超时情况。
  2. 高并发处理能力
    • 由于 Redis 是内存数据库,它的读写速度非常快,能够支持高并发的请求,适合用作分布式锁和临时数据存储。
    • RocketMQ 作为消息队列,能够确保高并发下的消息传递,异步处理订单关闭操作,减少了系统的压力。
  3. 可靠性和扩展性
    • 消息队列(RocketMQ):RocketMQ 提供的消息确认机制、持久化、重试机制,保证了消息不会丢失,同时具备很好的横向扩展能力。当系统流量增加时,RocketMQ 可以通过增加消费者和生产者来扩展性能,满足高并发需求。
    • Redis 过期监听:Redis 支持高并发的数据操作和大规模数据存储,能够高效处理超时事件。
  4. 解耦和灵活性
    • Redis 和 RocketMQ 各自承担不同的职责(Redis 主要用于存储状态和监听过期事件,RocketMQ 用于异步通知和订单关闭),这样系统的各个模块之间实现了良好的解耦,便于维护和扩展。
  5. 延迟控制
    • RocketMQ 提供的延迟消息功能可以让我们精确控制订单超时后的处理时间,避免了手动定时处理的复杂性。

缺点

  1. 单点故障风险

    • Redis:虽然 Redis 高效且支持高并发,但 Redis 仍然是单点存储。如果 Redis 出现故障,可能会导致锁失效或过期事件无法监听,影响整个订单关闭流程。为了解决这个问题,通常需要部署 Redis 集群并做好高可用配置。
    • RocketMQ:尽管 RocketMQ 支持高可用和集群部署,但它的维护相对复杂,要求有较高的运维能力和资源投入。如果配置不当,可能会导致消息丢失或消费延迟。
  2. 系统复杂度增加

    • 结合 Redis 和 RocketMQ 提高了系统的复杂性,尤其是对于初学者或小规模项目来说,需要更多的技术栈支持与管理。
    • Redis 和 RocketMQ 都需要额外的运维工作,比如配置、监控、性能优化等。
  3. 延迟问题

    • 延迟消息:虽然 RocketMQ 提供延迟消息的功能,但在一些高并发场景下,消息的延迟时间可能会出现不稳定,尤其是在网络波动或 RocketMQ 消费端的处理速度较慢时。延迟消息的精确度也会受到一定的影响。
    • 由于我们设置的锁过期时间为 30 分钟,在 Redis 过期时,消息被投递到 RocketMQ,可能会有一定的延迟。因此,部分订单在超时时可能会稍微滞后地关闭。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

清河大善人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值