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

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

前言

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

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

使用 JDK 自带的 DelayQueue:适用于小型系统或测试环境,不需要分布式支持。

使用 Redis 过期机制:适用于高性能要求的系统,需要分布式支持。

使用 Spring Schedule:适用于已经使用Spring框架的系统,希望利用其集成优势。

使用消息队列(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 delayQueue;

public OrderProcessor(DelayQueue 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 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 delayQueue, String orderId, long delayInMillis) {

delayQueue.put(new Order(orderId, delayInMillis));

}

}

1.2、优缺点

优点:

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

缺点

所有超时订单都加入到DelayQueue中,占用内存大。

没法做到分布式处理,只能在集群中的一个leader专门处理效率低

服务崩溃,数据直接丢失

不适合订单量大的场景

二、消息队列

2.1、具体实现

这里采用RabbitMQ简单实现。

1、添加依赖

xml

复制代码

com.rabbitmq

amqp-client

5.14.0

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 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、优缺点分析

优点

使用简单

支持分布式

精度高,支持任意时刻

缺点

使用限制:定时时长最大值24小时

成本高:每个订单都要新增一个定时任务,且不会马上消费,给MQ带来很大的存储成本

同一个时刻大量消息会导致消息延迟:给系统压力很大,导致消息分发延迟。

三、Redis过期监听

3.1、具体实现

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

实现步骤:

1、添加Jedis依赖

xml

复制代码

redis.clients

jedis

2.9.0

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、优缺点分析

优点

高性能:

Redis 是一个内存数据库,读写速度非常快,适用于需要快速响应的场景。

利用Redis的过期机制可以实现高效的过期通知。

简单易用:

Redis 提供了简单的API来设置键值的过期时间,如 EXPIRE 或 PEXPIRE 命令。

使用 PUBLISH 和 SUBSCRIBE 机制可以方便地监听过期事件。

可扩展性强:

Redis 支持集群部署,可以通过水平扩展来应对更高的负载。

可以轻松地在多个节点之间复制数据,保证数据的一致性和可用性。

可靠性:

Redis 提供了持久化机制(AOF 或 RDB),可以保证数据的安全性。

过期事件可以通过 PUBLISH 发布到特定频道,由多个订阅者监听,确保不会错过任何过期通知。

灵活性:

可以根据业务需求灵活配置过期时间。

可以通过不同的频道来区分不同类型的过期事件,便于管理和处理。

缺点

过期时间不精确:

Redis 的过期机制是基于定时任务来检查和清理过期键的,因此过期时间并不是精确的。

实际上,键可能会在设定的过期时间之后几秒钟才真正过期,这取决于Redis的内部调度机制。

内存消耗:

Redis 是内存数据库,存储大量订单数据可能会导致较高的内存消耗。

需要合理规划内存使用,特别是在高并发环境下。

过期事件处理延迟:

如果有大量的订单同时过期,Redis 的内部调度机制可能会导致过期事件的处理出现延迟。

这种延迟可能会导致业务逻辑出现问题,特别是对于实时性要求很高的场景。

单点故障风险:

单个Redis实例可能会成为系统的单点故障,需要通过集群和主从复制等方式来提高系统的可用性。

如果Redis实例宕机,可能会导致过期事件丢失,从而影响业务逻辑。

并发处理复杂:

如果需要在多个订阅者之间分发过期事件,需要考虑如何保证事件处理的一致性和幂等性。

需要设计合理的并发处理机制来避免数据一致性问题。

四、任务调度

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、优缺点分析

优点

易于实现:

Spring 提供了简洁的@Scheduled注解,使得定时任务的实现变得非常简单。

不需要额外的中间件或服务,只需要在Spring应用中定义任务即可。

高度集成:

由于Spring Boot框架的广泛使用,将定时任务集成到现有的Spring应用中非常方便。

可以直接访问Spring容器中的Bean,方便与其他服务交互。

灵活的调度选项:

支持多种调度方式,包括基于固定延时(fixedDelay)、基于固定频率(fixedRate)和基于Cron表达式的调度(cron)。

可以根据业务需求选择最适合的调度方式。

易于维护和管理:

由于任务是在Spring容器内运行的,可以利用Spring的依赖注入和其他特性来简化任务的开发和维护。

可以通过Spring的配置文件或环境变量动态调整任务的调度参数。

支持并发处理:

可以通过配置线程池来支持并发执行定时任务,提高处理效率。

可以根据业务需求调整线程池大小,以适应不同的负载情况。

日志记录和异常处理:

可以利用Spring的AOP(面向切面编程)来增强定时任务的功能,例如记录日志和处理异常。

可以在任务中添加日志输出,方便调试和监控任务执行情况。

缺点

单点故障:

如果整个Spring应用或执行定时任务的服务器出现故障,定时任务将无法执行。

需要额外的容错机制来确保任务的高可用性。

调度精度受限:

Spring定时任务的调度精度受线程池调度机制的影响,可能会有一定的延迟。

特别是在高负载情况下,任务的实际执行时间可能会与预定时间有所偏差。

资源消耗:

如果定时任务非常频繁,可能会消耗较多的CPU和内存资源。

需要合理配置任务的执行频率,以免影响应用的整体性能。

并发处理复杂度:

如果需要处理大量并发任务,需要仔细设计并发处理逻辑,以防止数据竞争和一致性问题。

需要考虑事务处理和数据锁定机制,确保数据的一致性和完整性。

缺乏外部监控:

默认情况下,Spring定时任务的执行情况主要依赖于日志记录,缺乏外部监控工具的支持。

可能需要额外的监控工具或框架来监控任务的执行状态。

部署灵活性较低:

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

对于需要动态调整任务调度的情况,可能需要重新启动应用。

五、总结

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

推荐使用:

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

MQ:适用于大型分布式系统,需要高可靠性和高并发处理能力。

定时任务(如Spring Schedule):适用于已经使用Spring框架的系统,希望利用其集成优势,并且对性能和扩展性要求不是特别高的场景。

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

相关作品

手机英文上面的逗号怎么打_英文写作中标点符号怎么用?--逗号篇
约彩365彩票官方app下载安卓

手机英文上面的逗号怎么打_英文写作中标点符号怎么用?--逗号篇

📅 10-19 👀 3017
天罡星攻略之天究星·单招(难度★★)
约彩365彩票官方app下载安卓

天罡星攻略之天究星·单招(难度★★)

📅 08-17 👀 3376