5

Spring Boot中实现订单30分钟自动取消的策略

 8 months ago
source link: https://www.51cto.com/article/769140.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在电商和其他涉及到在线支付的应用中,通常需要实现一个功能:如果用户在生成订单后的一定时间内未完成支付,系统将自动取消该订单。本文将详细介绍基于Spring Boot框架实现订单30分钟内未支付自动取消的几种方案,并提供实例代码。

方案一:定时任务

利用Spring Boot中的@Scheduled注解,我们可以轻松地实现定时任务。该任务将周期性地扫描数据库,检查未支付的订单,如果订单生成30分钟未支付,则自动取消。

@Component
public class OrderCancelSchedule {

    @Autowired
    private OrderService orderService;

    @Scheduled(cron = "0 0/1 * * * ?")
    public void cancelUnpaidOrders() {
        List<Order> unpaidOrders = orderService.getUnpaidOrders();
        unpaidOrders.forEach(order -> {
            if (order.getCreationTime().plusMinutes(30).isBefore(LocalDateTime.now())) {
                orderService.cancelOrder(order.getId());
            }
        });
    }
}

1.1 Schedule注解

基于注解@Scheduled默认为单线程,开启多个任务时,任务的执行时机会受上一个任务执行时间的影响。

@EnableScheduling注解: 在配置类上使用,开启计划任务的支持(类上)。

@SpringBootApplication
@EnableScheduling
public class SpringBootWebApplication { //开启定时任务
}

@Scheduled注解:来声明这是一个任务,包括 cron,fixDelay,fixRate 等类型(方法上,需先开启计划任务的支持)。配合@EnableScheduling使用

package com.pjb.Schedule;
 
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
 
import java.text.SimpleDateFormat;
import java.util.Date;
 
/**
 * 定时任务的使用
 **/
@Component
public class Task
{
    @Scheduled(cron="0/5 * *  * * ? ")   //每5秒执行一次
    public void execute(){
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //设置日期格式
        System.out.println("欢迎访问Java小陈的博客 " + df.format(new Date()));
    }
}

1.2 cron表达式

cron表达式是一个字符串,字符串以5或6个空格隔开,分开共6或7个域,每一个域代表一个含义。

cron 表达式格式:[秒] [分] [小时] [日] [月] [周] [年]

允许填写的值

允许的通配符

, - * ? / L W

1-12 or JAN-DEC

1-7 or SUN-SAT

, - * ? / L #

empty 或 1970-2099

通配符说明:

  • * 表示所有值. 例如:在分的字段上设置 "*",表示每一分钟都会触发。
  • ? 表示不指定值。使用的场景为不需要关心当前设置这个字段的值。例如:要在每月的10号触发一个操作,但不关心是周几,所以需要周位置的那个字段设置为"?" 具体设置为 0 0 0 10 * ?
  • - 表示区间。例如 在小时上设置 "10-12",表示 10,11,12点都会触发。
  • , 表示指定多个值,例如在周字段上设置 "MON,WED,FRI" 表示周一,周三和周五触发
  • / 用于递增触发。如在秒上面设置"5/15" 表示从5秒开始,每增15秒触发(5,20,35,50)。在月字段上设置'1/3'所示每月1号开始,每隔三天触发一次。
  • L 表示最后的意思。在日字段设置上,表示当月的最后一天(依据当前月份,如果是二月还会依据是否是润年[leap]), 在周字段上表示星期六,相当于"7"或"SAT"。如果在"L"前加上数字,则表示该数据的最后一个。例如在周字段上设置"6L"这样的格式,则表示“本月最后一个星期五"
  • W 表示离指定日期的最近那个工作日(周一至周五). 例如在日字段上设置"15W",表示离每月15号最近的那个工作日触发。如果15号正好是周六,则找最近的周五(14号)触发, 如果15号是周未,则找最近的下周一(16号)触发.如果15号正好在工作日(周一至周五),则就在该天触发。如果指定格式为 "1W",它则表示每月1号往后最近的工作日触发。如果1号正是周六,则将在3号下周一触发。(注,"W"前只能设置具体的数字,不允许区间"-").
  • # 序号(表示每月的第几个周几),例如在周字段上设置"6#3"表示在每月的第三个周六.注意如果指定"#5",正好第五周没有周六,则不会触发该配置(用在母亲节和父亲节再合适不过了)

记住就可以了,需要运用可以直接上网查cron表达式。或者用在线cron工具来生成自己想要的表达式。符号的意思需要理解

方案二:延迟队列

使用消息队列(如RabbitMQ)的延迟队列功能,当订单生成时将订单ID推送到延迟队列,设置30分钟后过期,过期后消费该消息,取消订单。

580d82019df4a4804b2635ed3f1c5e0561a1dd.png
@Service
public class OrderService {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void createOrder(Order order) {
        // 保存订单至数据库
        saveOrderToDB(order);

        // 将订单ID推送至延迟队列
        rabbitTemplate.convertAndSend("orderDelayExchange", "orderDelayKey", order.getId(), message -> {
            message.getMessageProperties().setDelay(30 * 60 * 1000); // 设置延迟时间
            return message;
        });
    }
}

@Component
@RabbitListener(queues = "orderDelayQueue")
public class OrderDelayConsumer {

    @Autowired
    private OrderService orderService;

    @RabbitHandler
    public void process(String orderId) {
        // 取消订单
        orderService.cancelOrder(orderId);
    }
}

方案三:Redis过期事件

利用Redis的键过期事件功能,当订单生成时在Redis中存储一个键,设置30分钟过期,键过期时通过Redis的过期事件通知功能触发订单取消操作。

@Service
public class OrderService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    public void createOrder(Order order) {
        // 保存订单至数据库
        saveOrderToDB(order);

        // 在Redis中存储一个键,设置30分钟过期
        redisTemplate.opsForValue().set("order:" + order.getId(), order.getId(), 30, TimeUnit.MINUTES);
    }

    // 当键过期时,Redis会自动调用该方法(需要配置Redis的过期事件通知功能)
    public void onOrderKeyExpired(String orderId) {
        cancelOrder(orderId);
    }
}

补充一下配置Redis的过期事件通知功能如下: Redis的键过期通知是一种典型的发布-订阅模式。在Redis中,我们可以订阅到某些特定的事件。键过期事件就是其中之一。但需要注意,要使用这个功能,需要确保你的Redis服务器开启了相关配置。

下面是具体的步骤和示例:

  1. 首先需要确保Redis的配置文件(通常是redis.conf)中开启了键空间通知功能。你可以通过在配置文件中添加或修改如下配置实现:
notify-keyspace-events "Ex"

这里的"Ex"表示只监听键过期事件。如果需要监听其他类型的事件,可以参考Redis官方文档进行配置。

  1. 然后在Spring Boot应用中,可以使用RedisMessageListenerContainer来订阅Redis的键过期事件,并指定回调方法进行处理。示例如下:
@Configuration
public class RedisConfig {

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    @Bean
    RedisMessageListenerContainer container() {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(redisConnectionFactory);
        // 订阅所有db的过期事件
        container.addMessageListener(new MessageListener() {
            @Override
            public void onMessage(Message message, byte[] pattern) {
                String expiredKey = message.toString();
                if (expiredKey.startsWith("order:")) {
                    // 处理订单超时逻辑
                    String orderId = expiredKey.split(":")[1];
                    // 这里调用你的服务类方法,处理订单超时逻辑
                    // orderService.cancelOrder(orderId);
                }
            }
        }, new PatternTopic("__keyevent@*__:expired"));
        return container;
    }
}

在这个示例中,"__keyevent@*__:expired"是一个模式匹配的主题,它可以匹配所有数据库的键过期事件。当一个键过期时,onMessage方法会被调用,你可以在这里加入你的逻辑来处理订单的超时取消。

请注意,这里的代码只是一个基本示例。在实际使用中,你可能需要根据自己的需要对代码进行相应的调整和优化。

以上三种方案都可以实现订单在30分钟内未支付则自动取消的需求。根据实际业务需求、系统负载和其他因素,可以选择最适合自己系统的实现方案。每种方案都有其优缺点,需要根据具体情况权衡。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK