当前位置:首页 > 科技  > 软件

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

来源: 责编: 时间:2024-03-28 09:27:44 101观看
导读订单超时未支付自动取消是一个典型的电商和在线交易业务场景,在该场景下,用户在购物平台上下单后,系统通常会为用户提供一段有限的时间来完成支付。如果用户在这个指定的时间窗口内没有成功完成支付,系统将自动执行订单取

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

当然类似的业务场景还有:tGP28资讯网——每日最新资讯28at.com

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

图片图片tGP28资讯网——每日最新资讯28at.com

本文主要介绍以下几种主流方案。tGP28资讯网——每日最新资讯28at.com

定时轮训(SpringBoot的Scheduled实现)

定时轮训的方式都是基于定时定任务扫描订单表,按照下单时间以及状态进行过滤,之后在进行判断是否在有效期内,如果不在,则取消订单。tGP28资讯网——每日最新资讯28at.com

如以下,我们使用SpringBoot中的定时任务实现:tGP28资讯网——每日最新资讯28at.com

我们先创建定时任务的配置,设置任务每隔5秒执行一次。tGP28资讯网——每日最新资讯28at.com

@Configuration@EnableSchedulingpublic 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中实现扫描订单以及判断订单是否需要取消:tGP28资讯网——每日最新资讯28at.com

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

运行结果如下:tGP28资讯网——每日最新资讯28at.com

图片图片tGP28资讯网——每日最新资讯28at.com

基于JDK的DelayQueue实现的延迟队列解决取消超时订单的方案,相比较于定时轮训有如下优点:tGP28资讯网——每日最新资讯28at.com

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

但是因为DelayQueue是基于内存的,这也导致它在实现上有一定的缺点:tGP28资讯网——每日最新资讯28at.com

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

时间轮算法

在介绍时间轮算法实现取消超时订单功能之前,我们先来看一下什么是时间轮算法?tGP28资讯网——每日最新资讯28at.com

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

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

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

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

图片图片tGP28资讯网——每日最新资讯28at.com

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

所以,我们可以使用时间轮算法去试一下延迟任务,用于实现取消超时订单。tGP28资讯网——每日最新资讯28at.com

我们以Netty4为例,引入依赖:tGP28资讯网——每日最新资讯28at.com

<dependency>    <groupId>io.netty</groupId>    <artifactId>netty-all</artifactId>    <version>4.1.68.Final</version></dependency>

然后定义订单处理服务,在创建订单时定义订单超时时间,以及超时时取消订单。tGP28资讯网——每日最新资讯28at.com

@Servicepublic class OrderService {    private final Map<String, Timeout> 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);    }}

我们定义订单创建接口,模拟订单创建:tGP28资讯网——每日最新资讯28at.com

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

我们分别请求接口,创建订单:tGP28资讯网——每日最新资讯28at.com

图片图片tGP28资讯网——每日最新资讯28at.com

可以看见,订单在5秒钟之后自动调用取消方法取消订单。tGP28资讯网——每日最新资讯28at.com

基于时间轮实现延迟任务来取消超时订单有如下优点:tGP28资讯网——每日最新资讯28at.com

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

但是相对应的也存在一些缺点:tGP28资讯网——每日最新资讯28at.com

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

Redis实现

对于Redis实现延迟任务,常见的两种方案是使用有序集合(Sorted Set,通常简称为zset)和使用key过期监听。tGP28资讯网——每日最新资讯28at.com

定时轮训有序集合

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

引入Redis依赖:tGP28资讯网——每日最新资讯28at.com

<dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-data-redis</artifactId>    <version>2.7.0</version></dependency>

配置一下RedisTemplate:tGP28资讯网——每日最新资讯28at.com

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

创建订单创建以及自动取消服务:tGP28资讯网——每日最新资讯28at.com

@EnableScheduling@Servicepublic class OrderZSetService {    @Autowired    private RedisTemplate<String, String> 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<ZSetOperations.TypedTuple<String>> range = redisTemplate.opsForZSet().rangeByScoreWithScores(ORDER_TIMEOUT_SET_KEY, 0, now);        for (ZSetOperations.TypedTuple<String> tuple : range) {            String orderId = (String) tuple.getValue();            if (tuple.getScore() <= now) {                // 处理超时订单                cancelOrder(orderId);                // 从有序集合中移除已处理的超时订单                redisTemplate.opsForZSet().remove(ORDER_TIMEOUT_SET_KEY, orderId);            }        }    }    private void cancelOrder(String orderId) {        // 在这里实现订单取消的实际逻辑        System.out.println("订单 " + orderId + " 在" + LocalDateTime.now() +"取消");        // 更新订单状态、释放库存等操作...    }}

注意:因本例中基于@Scheduled实现定时轮训,所以需要使用@EnableScheduling开启Scheduled功能。具体请参考:玩转SpringBoot:SpringBoot的几种定时任务实现方式tGP28资讯网——每日最新资讯28at.com

我们定义订单创建接口,模拟订单创建:tGP28资讯网——每日最新资讯28at.com

图片图片tGP28资讯网——每日最新资讯28at.com

可以看到订单5秒钟后自动取消。tGP28资讯网——每日最新资讯28at.com

使用Redis有序集合实现取消超时订单有一些优点:tGP28资讯网——每日最新资讯28at.com

  1. 有序集合可以根据分数(过期时间)快速定位到需要处理的超时订单,避免了对全部订单的全表扫描,提高了查询效率。
  2. 在分布式环境中,Redis作为缓存和中间件,可以很容易地实现在多节点间共享超时订单信息,有利于分布式系统中统一管理超时订单。
  3. 利用Redis内存数据结构,不需要频繁读写数据库,降低了数据库的压力,同时也节约了数据库资源。

但是也有一些缺点:tGP28资讯网——每日最新资讯28at.com

  1. 定时任务的执行频率决定了处理超时订单的精确程度,频率太低可能导致部分订单未能及时取消,频率太高则可能浪费系统资源。
  2. 在涉及事务处理的情况下,可能需要额外的手段来保证与数据库之间的数据一致性,防止因Redis处理超时订单后,数据库层面的更新失败导致的数据不一致问题。
  3. 在处理超时订单过程中,若出现异常,需要配套的重试机制。

使用Redis key过期监听

利用Redis的key过期监听功能。当设置一个key的过期时间时,可以设置一个回调函数,当key过期时,Redis会自动调用这个回调函数。即利用Redis的Keyspace Notifications功能,当一个键(Key)过期时,Redis会向已订阅了相关频道的客户端发送一个通知。tGP28资讯网——每日最新资讯28at.com

使用Redis的key的过期监听功能之前我们需要启用Redis Keyspace Notifications,在Redis配置文件(redis.conf)中启用Key Space Notifications,即打开如下配置:tGP28资讯网——每日最新资讯28at.com

notify-keyspace-events Ex

notify-keyspace-events设置为Ex,表示启用所有类型的键空间通知,包括过期事件。具体配置方法可能因Redis的版本和环境而有所不同,请根据实际情况进行配置。tGP28资讯网——每日最新资讯28at.com

然后我们就可以使用代码实现,首先实现MessageListener接口实现一个监听器来监听Redis的key过期事件。当订单的key过期时,将触发监听器中的逻辑,执行取消订单的操作。tGP28资讯网——每日最新资讯28at.com

@Componentpublic class OrderExpirationListener implements MessageListener {    @Autowired    private OrderExpirationService orderService;    @Override    public void onMessage(Message message, byte[] pattern) {        String orderId = message.toString();        // 调用服务取消订单        orderService.cancelOrder(orderId);    }}

然后配置Redis key过期事件监听器,并将其注册到Redis连接工厂中。这样,监听器将会在Redis的key过期事件发生时被调用。tGP28资讯网——每日最新资讯28at.com

@Configurationpublic class RedisConfig {    @Bean    public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {        // 其余配置 如序列化等        return new StringRedisTemplate(factory);    }    @Bean    public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,                                                   OrderExpirationListener listener) {        RedisMessageListenerContainer container = new RedisMessageListenerContainer();        container.setConnectionFactory(connectionFactory);        container.addMessageListener(listener, new ChannelTopic("__keyevent@0__:expired")); // 监听所有数据库的key过期事件        return container;    }}

__keyevent@0__:expired是Redis的系统通道,用于监听所有数据库中的key过期事件。如果需要监听特定数据库的key过期事件,则可以修改对应的数据库号。例如,__keyevent@1__:expired表示监听第一个数据库的key过期事件。tGP28资讯网——每日最新资讯28at.com

然后我们就可以实现具体的订单创建服务以及订单取消的逻辑了。这里我们模拟一下:tGP28资讯网——每日最新资讯28at.com

@Componentpublic class OrderExpirationService {    @Autowired    private RedisTemplate<String, String> redisTemplate;    public void createOrder(String orderId) {        System.out.println("订单"+orderId+"在"+ LocalDateTime.now() +"创建成功.");        // 假设订单超时时间为5秒        long expirationTime = 5;        redisTemplate.opsForValue().set(orderId, "orderData", expirationTime, TimeUnit.SECONDS);    }    public void cancelOrder(String orderId) {        // 在这里实现订单取消的实际逻辑        System.out.println("订单 " + orderId + " 在" + LocalDateTime.now() +"取消");        // 更新订单状态、释放库存等操作...    }}
@RestController@RequestMapping("orderRedis")public class RedisOrderController {    @Autowired    private OrderExpirationService orderService;    @PostMapping("/create")    public String createOrder(String orderId) {        orderService.createOrder(orderId);        return "订单创建成功:" + orderId;    }}

我们创建4个订单,模拟5秒钟后的订单取消tGP28资讯网——每日最新资讯28at.com

图片图片tGP28资讯网——每日最新资讯28at.com

使用Redis的key过期监听事件,实现取消超时订单有以下优点:tGP28资讯网——每日最新资讯28at.com

  1. Redis键过期事件能在键过期时立即触发监听器,因此可以在订单超时的瞬间准确执行取消操作,大大提高了时效性。
  2. 相比定期轮询数据库查询超时订单的方式,Redis键过期事件是被动触发,节省了CPU和网络资源,减少了无效查询。
  3. Redis的键过期事件处理机制天然支持高并发场景,只要Redis集群足够强大,可以轻松处理大量订单的过期处理。
  4. 资源占用小,只需要维护Redis中少量的键,相对于数据库存储所有订单信息并做定时任务查询,内存和磁盘资源占用较少。

但是也存在一些缺点:tGP28资讯网——每日最新资讯28at.com

  1. 整个方案依赖于Redis服务的稳定性和性能,如果Redis服务出现问题,可能会影响订单超时处理。
  2. 在高并发场景下,Redis过期事件产生的速率可能非常高,如果处理不当,监听器本身的处理能力可能成为瓶颈,导致消息堆积,这时需要考虑消息队列或者其他缓冲机制。
  3. Redis的键过期并不是严格意义上的实时,而是基于定期检查机制,极端情况下可能存在一定的延迟。尽管在实践中这种延迟很小,但对于极高精度要求的场景,可能需要额外关注。

MQ消息队列

使用消息队列实现取消超时订单的常见方法是利用延迟队列以及死信队列。比如RabbitMq,在介绍实现方式之前,我们先来了解一下RabbitMq的延迟队列以及死信队列。tGP28资讯网——每日最新资讯28at.com

  1. 延迟队列:RabbitMQ本身并不直接支持延迟队列,但可以通过安装rabbitmq_delayed_message_exchange插件来实现延迟消息的功能。当启用这个插件后,你可以创建一个类型为x-delayed-message的交换机。在发送消息时,可以设置消息头中的x-delay字段,表示消息应该在多久之后才开始被路由到绑定的目标队列。这样,当一个订单创建时,可以将包含订单ID和过期时间的消息发送到延迟交换机,并设置相应的延迟时间。当延迟时间结束时,消息将被发送到处理超时订单的队列,随后由消费者进行订单状态检查和取消操作。

我的RabbitMq是部署在docker中的,所以顺带提议一下关于安装rabbitmq_delayed_message_exchange插件,我们需要在 Releases · rabbitmq/rabbitmq-delayed-message-exchange (github.com)下载.ez结尾的插件,然后使用docker cp命令将其拷贝到rabbitmq容器内:tGP28资讯网——每日最新资讯28at.com

docker cp <本地路径>/rabbitmq_delayed_message_exchange-3.13.0.ez <容器ID>:/plugins

然后我们进入容器后启动插件:tGP28资讯网——每日最新资讯28at.com

rabbitmq-plugins enable rabbitmq_delayed_message_exchange

然后验证一下插件是否开启成功:tGP28资讯网——每日最新资讯28at.com

rabbitmq-plugins list | grep delayed

图片图片tGP28资讯网——每日最新资讯28at.com

我的rabbitmq的版本是3.13.0tGP28资讯网——每日最新资讯28at.com

  1. 死信队列死信队列是指当消息在原始队列中遇到某种情况(如消息过期、消息被拒绝等)时,会被重新路由到另一个预定义的队列中。当消息在队列中停留的时间超过TTL,该消息就会变成死信,并根据队列配置转发到死信队列。

基于RabbitMq的延迟队列

延迟队列可以直接处理延迟消息,即消息在指定的延迟时间过后才被投递给消费者。在超时取消订单的场景中,订单创建时将订单信息封装成消息,并设置消息的延迟时间,当订单超时时,消息自动被投递到处理超时订单的队列,消费者接收到消息后执行取消操作。tGP28资讯网——每日最新资讯28at.com

引入依赖:tGP28资讯网——每日最新资讯28at.com

<dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-amqp</artifactId>    <version>2.7.18</version></dependency>

配置rabbitmq的相关参数:tGP28资讯网——每日最新资讯28at.com

spring.rabbitmq.host=localhost  spring.rabbitmq.port=5672  spring.rabbitmq.password=guest  spring.rabbitmq.username=guest

配置延迟交换机,并且初始化延迟交换机、队列及绑定关系tGP28资讯网——每日最新资讯28at.com

@Configuration@EnableRabbitpublic class RabbitConfig {    public static final String ORDER_EXCHANGE = "order.delayed.exchange";    public static final String ORDER_QUEUE = "order.delayed.queue";    public static final String ROUTING_KEY = "delayed-routing-key";    @Bean    public CustomExchange delayedExchange() {        return new CustomExchange(ORDER_EXCHANGE, "x-delayed-message", true, false);    }    @Bean    public Queue delayedQueue() {        return new Queue(ORDER_QUEUE);    }    @Bean    public Binding delayedBinding(CustomExchange delayedExchange, Queue delayedQueue) {        return BindingBuilder.bind(delayedQueue).to(delayedExchange).with(ROUTING_KEY).noargs();    }}

这里交换机exchange,需要我们事先在rabbitmq中创建好,访问http://localhost:15672/在Exchanges中,添加Exchange,设置type= x-delayed-message,如图:tGP28资讯网——每日最新资讯28at.com

图片图片tGP28资讯网——每日最新资讯28at.com

在定义一个监听rabbitmq消息的监听器,当消息延迟时间到了之后,就会被该监听器见听到,在这里判断订单是否已经被支付,如果没有支付则取消。tGP28资讯网——每日最新资讯28at.com

@Componentpublic class DelayedQueueListener {    @Autowired    private OrderMqService orderMqService;    @RabbitListener(queues = RabbitConfig.ORDER_QUEUE)    public void handleDelayedOrder(String orderId) {        orderMqService.cancelOrder(orderId);    }   }

然后我们在订单创建时,将订单信息发送到MQ中,等延迟时间到了之后,如果订单还没有支付,则执行取消订单操作。tGP28资讯网——每日最新资讯28at.com

@Servicepublic 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(RabbitConfig.ORDER_EXCHANGE, RabbitConfig.ROUTING_KEY, orderId, message -> {            message.getMessageProperties().setDelay(5 * 1000);            return message;        });    }    public void cancelOrder(String orderId) {        // 在这里实现订单取消的实际逻辑        System.out.println("订单" + orderId + " 在" + LocalDateTime.now() +"取消");        // 更新订单状态、释放库存等操作...    }}

我们模拟创建订单请求:tGP28资讯网——每日最新资讯28at.com

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

图片图片tGP28资讯网——每日最新资讯28at.com

可以看见订单过了5秒之后开始执行取消。tGP28资讯网——每日最新资讯28at.com

使用延迟队列方案来实现订单超时取消等场景的优点:tGP28资讯网——每日最新资讯28at.com

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

但是也有一些缺点:tGP28资讯网——每日最新资讯28at.com

  1. 高度依赖消息队列服务的可用性和稳定性,一旦消息队列出现故障,可能导致超时订单无法正常处理。
  2. 延迟队列方案涉及更多的中间件配置和管理,增加了系统的复杂性。
  3. 在分布式系统中,如果订单状态不仅在消息队列中维护,还要同步到数据库,需要额外保证消息队列处理和数据库操作的一致性。
  4. 虽然大部分消息队列的延迟机制相当可靠,但仍有极小概率出现消息延迟到达或丢失的情况,需要有相应的容错和补偿机制。

对于延迟队列,并非只有rabbitmq才有,RocketMQ也有延迟队列。在RocketMQ中,延迟消息的发送是通过设置消息的延迟级别来实现的。每个延迟级别都对应着一个具体的延迟时间,例如 1 表示 1 秒、2 表示 5 秒、3 表示 10 秒,以此类推。用户可以根据自己的需求选择合适的延迟级别。但是也可以看出他并没有支持的那么精确,如果想要精确的就必须使用RocketMQ的企业版,在企业版中可以自定义设置延迟时间。这里就不过多讲解,有兴趣的可以自己研究一下。tGP28资讯网——每日最新资讯28at.com

基于RabbitMq的死信队列实现

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

使用的rabbitmq依赖以及配置同上使用延迟队列方案。我们来看一下创建处理订单即带有TTL的队列:tGP28资讯网——每日最新资讯28at.com

@Configurationpublic 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<String, Object> 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);    }}

同理也是需要先创建交换机:tGP28资讯网——每日最新资讯28at.com

图片图片tGP28资讯网——每日最新资讯28at.com

创建订单业务类,将订单发送到订单消息队列:tGP28资讯网——每日最新资讯28at.com

@Servicepublic 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() +"取消");        // 更新订单状态、释放库存等操作...    }}

在创建死信队列,私信队列交换机,通过订单队列路由将私信队列绑定到订单订单队列中:tGP28资讯网——每日最新资讯28at.com

@Configurationpublic 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);    }}

在创建一个死信队列消息监听器,用于判断订单是否超时:tGP28资讯网——每日最新资讯28at.com

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

然后我们在订单创建时,将订单信息发送到订单MQ中,等消息的TTL到期之后,会自动转到死信队列中。tGP28资讯网——每日最新资讯28at.com

@Servicepublic 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() +"取消");        // 更新订单状态、释放库存等操作...    }}

我们模拟创建订单接口:tGP28资讯网——每日最新资讯28at.com

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

图片图片tGP28资讯网——每日最新资讯28at.com

可以看见订单过了5秒之后开始执行取消。tGP28资讯网——每日最新资讯28at.com

使用死信队列实现取消超时订单的优点:tGP28资讯网——每日最新资讯28at.com

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

当然他也有一些缺点:tGP28资讯网——每日最新资讯28at.com

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

总结

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

  1. 定时轮询:基于SpringBoot的Scheduled实现,通过定时任务扫描数据库中的订单。优点是实现简单直接,但缺点是会给数据库带来持续压力,处理效率受任务执行间隔影响较大,且在高并发场景下可能引发并发问题和资源浪费。
  2. JDK的延迟队列(DelayQueue):基于优先级队列实现,减少数据库访问,提供高效的任务处理。优点是内部数据结构高效,线程安全。缺点是所有待处理订单需保留在内存中,可能导致内存消耗大,且无持久化机制,系统崩溃时可能丢失数据。
  3. 时间轮算法:通过时间轮结构实现定时任务调度,能高效处理大量定时任务,提供精确的超时控制。优点是实现简单,执行效率高,且有成熟实现库。缺点同样是内存占用和崩溃时数据丢失的问题。
  4. Redis实现:

有序集合(Sorted Set):利用有序集合的特性,定时轮询查找已超时的任务。优点是查询效率高,适用于分布式环境,减少数据库压力。缺点是依赖定时任务执行频率,处理超时订单的实时性受限,且在处理事务一致性方面需要额外努力。tGP28资讯网——每日最新资讯28at.com

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

  1. MQ消息队列:

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

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

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

本文链接:http://www.28at.com/showinfo-26-79986-0.html美团二面:如何设计一个订单超时未支付关闭订单的解决方案?我说使用Elastic-Job轮训判断。

声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com

上一篇: 干货必读: 测试开发既然都这么厉害了!为啥不直接转业务开发?

下一篇: 球盒模型:一切回溯穷举,皆从此法出

标签:
  • 热门焦点
  • 5月安卓手机好评榜:魅族20 Pro夺冠

    5月安卓手机好评榜:魅族20 Pro夺冠

    性能榜和性价比榜之后,我们来看最后的安卓手机好评榜,数据来源安兔兔评测,收集时间2023年5月1日至5月31日,仅限国内市场。第一名:魅族20 Pro好评率:97.50%不得不感慨魅族老品牌还
  • 一文看懂为苹果Vision Pro开发应用程序

    一文看懂为苹果Vision Pro开发应用程序

    译者 | 布加迪审校 | 重楼苹果的Vision Pro是一款混合现实(MR)头戴设备。Vision Pro结合了虚拟现实(VR)和增强现实(AR)的沉浸感。其高分辨率显示屏、先进的传感器和强大的处理能力
  • 一文掌握 Golang 模糊测试(Fuzz Testing)

    一文掌握 Golang 模糊测试(Fuzz Testing)

    模糊测试(Fuzz Testing)模糊测试(Fuzz Testing)是通过向目标系统提供非预期的输入并监视异常结果来发现软件漏洞的方法。可以用来发现应用程序、操作系统和网络协议等中的漏洞或
  • 自律,给不了Keep自由!

    自律,给不了Keep自由!

    来源 | 互联网品牌官作者 | 李大为编排 | 又耳 审核 | 谷晓辉自律能不能给用户自由暂时不好说,但大概率不能给Keep自由。近日,全球最大的在线健身平台Keep正式登陆港交所,努力
  • 阿里瓴羊One推出背后,零售企业迎数字化新解

    阿里瓴羊One推出背后,零售企业迎数字化新解

    作者:刘旷近年来随着数字经济的高速发展,各式各样的SaaS应用服务更是层出不穷,但本质上SaaS大多局限于单一业务流层面,对用户核心关切的增长问题等则没有提供更好的解法。在Saa
  • 冯提莫签约抖音公会 前“斗鱼一姐”消失在直播间

    冯提莫签约抖音公会 前“斗鱼一姐”消失在直播间

    来源:直播观察提起&ldquo;冯提莫&rdquo;这个名字,很多网友或许听过,但应该不记得她是哪位主播了。其实,作为曾经的&ldquo;斗鱼一姐&rdquo;,冯提莫在游戏直播的年代影响力不输于现
  • iQOO 11S或7月上市:搭载“鸡血版”骁龙8Gen2 史上最强5G Soc

    iQOO 11S或7月上市:搭载“鸡血版”骁龙8Gen2 史上最强5G Soc

    去年底,iQOO推出了“电竞旗舰”iQOO 11系列,作为一款性能强机,iQOO 11不仅全球首发2K 144Hz E6全感屏,搭载了第二代骁龙8平台及144Hz电竞屏,同时在快充
  • 华为举行春季智慧办公新品发布会 首次推出电子墨水屏平板

    华为举行春季智慧办公新品发布会 首次推出电子墨水屏平板

    北京时间2月27日晚,华为在巴塞罗那举行春季智慧办公新品发布会,在海外市场推出之前已经在中国市场上市的笔记本、平板、激光打印机等办公产品,并首次推出搭载
  • 电博会与软博会实现

    电博会与软博会实现"线下+云端"的双线融合

    在本次“电博会”与“软博会”双展会利好条件的加持下,既可以发挥展会拉动人流、信息流、资金流实现快速交互流动的作用,继而推动区域经济良性发展;又可以聚
Top