如何关闭已超时的订单 ?

背景:

        我们逛淘宝的时候,遇到想要的商品,支付的时候,会有时间的限制,我们在时间限制里面完成支付,就会出订单号。但是如果逾期了,就会关闭超时订单。下面说一下思路以及伪代码。

一:定时器实现

思路:
  1. 存储机制:在订单创建时记录超时时间字段 expire_time
  2. 定时轮询:每5秒扫描数据库查询未支付订单
  3. 索引优化:通过复合索引加速查询
  4. 批量处理:分页查询避免单次处理数据量过大

伪代码:
CREATE TABLE orders (
  order_id BIGINT PRIMARY KEY,
  status TINYINT,       -- 0未支付 1已支付
  expire_time DATETIME, -- 超时时间
  created_time DATETIME,
  updated_time DATETIME,
  INDEX idx_expire_status (expire_time, status) -- 复合索引
);
// 伪代码 Spring Boot定时任务示例
@Scheduled(fixedDelay = 5000)
public void checkExpiredOrders() {
    int pageSize = 100;
    long lastId = 0;
    
    while(true) {
        // 使用游标分页查询
        List<Order> orders = orderRepository.findExpiredOrders(
            LocalDateTime.now().minusSeconds(5),
            lastId,
            pageSize
        );
        
        if(orders.isEmpty()) break;
        
        orders.forEach(order -> {
            // 分布式锁防止并发处理
            String lockKey = "order_lock:" + order.getId();
            if(redisLock.tryLock(lockKey, 10)) {
                try {
                    if(order.getStatus() == OrderStatus.UNPAID) {
                        // 事务内处理订单
                        processExpiredOrder(order);
                    }
                } finally {
                    redisLock.unlock(lockKey);
                }
            }
        });
        
        lastId = orders.get(orders.size()-1).getId();
    }
}

// JPA查询方法
@Query("SELECT o FROM Order o WHERE o.status = 0 AND o.expireTime < :current AND o.id > :lastId ORDER BY o.id ASC")
List<Order> findExpiredOrders(@Param("current") LocalDateTime current, 
                             @Param("lastId") Long lastId,
                             Pageable pageable);

优点

1,无需中间件依赖

2,数据一致性由数据库事务保证

3,代码实现简单

缺点

1,存在最多5秒延迟

2,高频查询对数据库有压力

二:优化被动关闭订单方案:按需处理与客户端协同

思路:

在传统的订单超时处理中,服务端通常需要主动轮询或依赖延迟消息触发关闭动作。而另一种思路是:将超时判断逻辑下沉到客户端,由客户端按需触发关闭请求: 

1,数据拉取阶段 当客户端请求订单列表时,服务端返回订单原始数据,包含订单创建时间超时时间阈值以及服务端当前时间戳

2,客户端动态计算 客户端根据服务端时间与订单创建时间,实时计算剩余支付时间。若检测到超时,则将订单标记为“已过期”样式,同时在用户无感知的情况下,异步发送关闭请求到服务端

3,基本上面思路已经很完善,假设还想再优化一下,就是服务端接收到关闭的请求,二次校验,校验当前订单的状态,实际超时时间。

优点:

1,无需高频轮询或维护延迟队列,尤其适合低频访问,服务端仅在实际需要时处理关闭逻辑。减少引入中间件的成本以及时间投入。

缺点:

1,脏数据有滞留现象,若用户长期不访问客户端,超时订单可能未被及时关闭,影响准确性

2,对于客户端的依赖性很强。

三:延迟队列

思路:

1,消息生产阶段

1.1订单创建时同步写入数据库

1.2发送携带订单ID的延迟消息到RocketMQ

1.3根据业务超时时间选择最接近的延迟等级

 2.消息消费阶段

2.1消费者监听指定Topic的消息

2.2收到消息后查询订单最新状态

2.3校验订单是否仍处于未支付状态

2.4执行关单业务逻辑

3 补偿机制

3.1独立定时任务扫描未支付订单

3.2处理RocketMQ可能丢失的消息

伪代码:

生产者:

// 订单创建时发送延迟消息(伪代码)
public class OrderProducer {
    private RocketMQTemplate rocketMQTemplate;

    public void createOrder(Order order) {
        // 1. 写入数据库
        orderDao.insert(order);
        
        // 2. 发送延迟消息
        Message<String> message = MessageBuilder.withPayload(order.getOrderId())
                .setHeader(RocketMQHeaders.KEYS, order.getOrderId())
                .build();
        int delayLevel = 16;  // 对应30分钟
        rocketMQTemplate.syncSend("ORDER_TIMEOUT_TOPIC", message, 3000, delayLevel);
        
      
    }
}

消费者:

@RocketMQMessageListener(
    topic = "ORDER_TIMEOUT_TOPIC",
    consumerGroup = "ORDER_TIMEOUT_GROUP"
)
public class OrderTimeoutConsumer implements RocketMQListener<MessageExt> {
    @Override
    public void onMessage(MessageExt message) {
        String orderId = new String(message.getBody());
        
        Order order = orderDao.getById(orderId);
        if (order == null) return;
        
        // 核心校验逻辑
        if (order.getStatus() == OrderStatus.UNPAID) {
            // 执行关单操作
            orderService.closeOrder(order);
            log.info("关闭超时订单: {}", orderId);
        } else {
  
            log.warn("收到已处理订单: {}", orderId);
        }
    }
}


这里兜底一下:

虽然rocketmq有持久策略,这里使用了第一种方式兜底,帮助理解。

@Scheduled(cron = "0 0/5 * * * ?")
public void checkOrderTimeout() {
    // 扫描数据库中状态为未支付且超时的订单
    List<Order> expiredOrders = orderDao.scanExpiredOrders();
    
    expiredOrders.forEach(order -> {
        if (order.getStatus() == OrderStatus.UNPAID) {
            orderService.closeOrder(order);
        }
    });
}

优点:

1,天然分布式特性,适合高并发场景

2,借助MQ实现解耦,降低业务侵入性

缺点:

1,无法取消已发送的延迟消息,导致无效消息处理

2,消息堆积时可能产生处理延迟

3,强依赖MQ中间件稳定性

四: 超时中心(TOC)

思路:
  1. 任务提交

    1.1.业务系统向超时中心注册任务  1.2示例:订单系统提交任务:“订单ID=123,30分钟后触发,回调地址为/order/xxxx”。
  2. 任务存储

    2.1所有任务按触发时间排序存储。 2.2常用存储方式:数据库表、Redis Sorted Set、时间轮算法。
  3. 任务调度

    3.1定时扫描“即将触发”的任务.  3.2触发时,将任务发送到延迟队列或直接调用业务回调接口。
  4. 任务处理

    4.1 触发后,超时中心调用业务系统的接口通知:“订单ID=123已超时,请处理”。 4.2业务系统自行实现具体逻辑(如关闭订单)。

优点

1,针对超时任务调度场景专项设计,通过分布式架构提高吞吐。

2,基于时间轮、延迟队列等机制,可支撑千万级任务调度。

3,专人维护,质量好。

缺点

1,技术成本和维护成本高。

2,实现复杂度比较高,会涉及分布式调度,MQ,高存储(redis)等,面临分布式锁,任务分片,网络抖动,节点宕机一系列问题,并且运维复杂度增加。

### 使用 Redis 实现订单超时处理 #### 方法概述 为了有效管理订单的生命周期并及时处理超时情况,可以利用 Redis 的 TTL (Time To Live) 功能以及键空间通知机制。当创建新订单时,设置一个带有生存时间(TTL)的键来表示该订单的状态。一旦此键因超过设定的时间而自动消失,则触发相应的清理操作。 具体来说,在 Redis 中维护一个未支付订单的集合[^4]。每当生成一个新的待付款项记录时,就向这个列表里加入对应的 ID 同时赋予一定时限;如果到了规定期限顾客仍未完成交易流程,那么系统将会接收到关于该项数据被移除的消息提示从而执行进一步的动作——例如取消订单、释放已锁定的商品数量等措施以保障资源的有效分配和服务质量不受影响。 #### 示例代码 下面是一个简单的 Spring Boot 应用程序片段展示如何集成上述思路: ```java import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.stereotype.Service; @Service public class OrderService { @Autowired private RedissonClient redissonClient; public void createOrder(String orderId, long timeoutInSeconds){ RLock lock = redissonClient.getLock(orderId); try { boolean isLocked = lock.tryLock(); if(isLocked){ // 设置订单ID作为key,并指定其存活时间为timeoutInSeconds秒 redissonClient.getBucket(orderId).set("unpaid", timeoutInSeconds, TimeUnit.SECONDS); System.out.println("Created order with id " + orderId +" and set expiration time."); }else{ throw new RuntimeException("Failed to acquire lock"); } } finally { lock.unlock(); // Always unlock even when an exception occurs. } } } ``` 这段 Java 代码展示了怎样借助 Redisson 客户端库中的 `RLock` 接口获取分布式互斥锁,确保同一时刻只有一个线程能够成功建立新的订单条目及其关联属性(如状态)。之后再调用 `getBucket().set()` 方法为特定 key 设定 value 及有效期,以此模拟实际场景下的订单创建过程[^5]。 对于监听过期事件的部分,可以通过配置 Redis 来开启键空间通知支持,并编写自定义侦听器去捕获这些信号进而采取适当行动。不过需要注意的是这种方式存在一定的局限性,因为 Redis 并不提供内置的消息确认机制,这意味着如果有任何原因导致消息丢失则不会得到补救机会。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值