场景题:电商平台订单未支付过期如何实现自动关闭订单?

目录

1.引入

2.解决方案

2.1定时任务

2.1.1定时任务概述

2.1.2定时任务方案对比

2.1.3定时任务方案实现

[2.1.3.1Spring Scheduler(单机简单任务)](#2.1.3.1Spring Scheduler(单机简单任务))

2.1.3.2Quartz(复杂调度+集群部署)

2.1.3.3XXL-Job(分布式任务调度平台)

(1)部署调度中心

[(2)集成执行器(Spring Boot项目)](#(2)集成执行器(Spring Boot项目))

[① 引入依赖](#① 引入依赖)

[② 配置执行器(application.yml)](#② 配置执行器(application.yml))

[③ 配置执行器客户端](#③ 配置执行器客户端)

[④ 实现XXL-Job任务](#④ 实现XXL-Job任务)

[⑤ 调度中心配置任务](#⑤ 调度中心配置任务)

2.2JDK延迟队列DelayQueue

2.3redis过期监听

概述

Redis配置

服务类添加延时任务:

控制器展示

2.4Redission分布式延迟队列

2.4.1概述

2.4.2示例

①配置延迟时间

②核心服务类

③.优缺点分析

[2.5RabbitMQ 安装延迟队列](#2.5RabbitMQ 安装延迟队列)

(1).概述

(2).代码示例.代码示例)

①.配置类

②.消息生产者

③.消息消费者

④.订单服务类

(3).优缺点分析

2.6死信队列

(1).概述

(2).演示

①.配置交换机路由关系

②.生产者

③.消费者

④.订单服务

(3).分析


1.引入

日常开发中,我们经常遇到这种业务场景,如:外卖订单超 30 分钟未支付,则自动取订单;用户注册成功 15 分钟后,发短信息通知用户等等。这就延时任务处理场景。在电商,支付等系统中,一设都是先创建订单(支付单),再给用户一定的时间进行支付,如果没有按时支付的话,就需要把之前的订单(支付单)取消掉。这种类以的场景有很多,还有比如到期自动收货,超时自动退款,下单后自动发送短信等等都是类似的业务问题。

2.解决方案

2.1定时任务

2.1.1定时任务概述

定时任务本质是"按预设时间规则自动执行的代码逻辑",其核心价值在于"解放人工、保障业务定时闭环"。常见应用场景可分为5类:

  • 数据治理 :每日凌晨清理过期日志、无效订单;定期归档历史数据(如将3个月前的订单数据迁移至历史库);
  • 业务闭环类:订单创建后30分钟未支付自动关闭;会员到期前3天发送续费提醒;
  • 统计报表类:每日凌晨生成前一天的销售报表、用户活跃报表;每月生成月度经营分析报告;
  • 系统运维类:定时检查服务健康状态;定期备份数据库;清理缓存碎片;
  • 消息推送类:定时推送每日早安消息、营销活动通知;定时同步第三方数据(如同步物流信息)。

不同场景对定时任务的要求不同:简单场景只需"按时执行",复杂场景则需要考虑"高可用、无重复、可监控、可重试"。

2.1.2定时任务方案对比

后端定时任务的实现方案有很多,从简单到复杂可分为3个层级,不同方案适配不同的业务规模:

实现方案 核心优势 局限性 适用场景
Spring Scheduler(@Scheduled) 1. 零依赖,Spring Boot原生支持;2. 配置简单,注解式开发;3. 轻量高效,无额外部署成本 1. 不支持分布式部署(易重复执行);2. 无内置监控、重试机制;3. 任务依赖、动态调整能力弱 单机部署、简单定时任务(如单机服务的日志清理)
Quartz 1. 功能强大,支持复杂调度规则(如日历式调度);2. 支持持久化(任务信息存入数据库);3. 支持集群部署(解决重复执行);4. 支持任务优先级、重试 1. 配置复杂,代码侵入性较强;2. 无内置监控告警,需自行实现;3. 分布式场景下运维成本较高 单机/集群部署、复杂调度规则的任务(如按工作日执行、任务依赖)
XXL-Job(分布式任务调度平台) 1. 分布式部署无重复执行;2. 内置Web管理界面(任务配置、执行日志、监控告警);3. 支持动态调整任务、失败重试、任务依赖;4. 支持分片执行(大数据量处理) 1. 需额外部署调度中心;2. 轻度依赖中间件(MySQL);3. 简单场景略显重量级 分布式微服务架构、高可用要求高的核心业务任务(如订单超时关闭、报表生成)

选型建议: 1. 简单场景(单机):优先用 Spring Scheduler,快速落地无成本; 2. 复杂调度(单机/小规模集群):用 Quartz,兼顾功能与稳定性; 3. 分布式微服务(大规模、高可用):用 XXL-Job,解放运维与开发效率。

2.1.3定时任务方案实现

2.1.3.1Spring Scheduler(单机简单任务)

Spring Scheduler是Spring框架内置的定时任务组件,无需额外引入依赖,注解式开发即可快速实现。

①.环境准备:

在Spring Boot主类上添加 @EnableScheduling 注解,启用定时任务功能:

复制代码
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling // 启用定时任务
public class TimedTaskDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(TimedTaskDemoApplication.class, args);
    }
}

②.业务代码:

创建定时任务类,通过 @Scheduled 注解定义任务执行规则,支持3种常见时间配置:cron表达式、固定速率、固定延迟。

复制代码
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.UUID;

/**
 * Spring Scheduler 定时任务示例
 */
@Component
public class SimpleTimedTask {
    private final Logger log = LoggerFactory.getLogger(SimpleTimedTask.class);

    /**
     * 1. cron表达式:按指定时间执行(每天凌晨3点清理过期订单)
     * cron语法:秒 分 时 日 月 周 年(年可选)
     * 示例:0 0 3 * * ?  表示每天03:00:00执行
     */
    @Scheduled(cron = "0 0 3 * * ?")
    public void cleanExpiredOrder() {
        // 手动绑定requestId到MDC,便于日志追踪(参考之前MDC博客内容)
        String requestId = UUID.randomUUID().toString().replace("-", "");
        MDC.put("requestId", requestId);
        try {
            log.info("开始清理过期订单");
            // 核心业务逻辑:删除创建时间超过30分钟且未支付的订单
            // orderMapper.deleteExpiredOrder(30); 
            log.info("过期订单清理完成");
        } catch (Exception e) {
            log.error("清理过期订单失败", e);
        } finally {
            MDC.clear(); // 清除MDC,避免线程复用污染
        }
    }

    /**
     * 2. 固定速率:按固定时间间隔执行(每隔5秒打印系统时间)
     * fixedRate:以上一次任务开始时间为基准,间隔固定时间执行
     */
    @Scheduled(fixedRate = 5000)
    public void printSystemTime() {
        log.info("当前系统时间:{}", System.currentTimeMillis());
    }

    /**
     * 3. 固定延迟:按固定延迟执行(上一次任务结束后,间隔固定时间再执行)
     * fixedDelay:适合任务执行时间不固定的场景(如数据同步,执行时间可能波动)
     */
    @Scheduled(fixedDelay = 10000)
    public void syncThirdPartyData() {
        log.info("开始同步第三方物流数据");
        try {
            // 模拟同步耗时(1-3秒)
            Thread.sleep((long) (Math.random() * 2000 + 1000));
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        log.info("第三方物流数据同步完成");
    }
}

关闭订单:

通过定时任务关闭订单,是一种成本很低,实现也很容易的方案。通过简单的几行代码,

写一个定时任务,定期扫描数据库中的订单,如果时间过期,就将其状态更新为关闭即

可。

优点

实现容易,成本低,基本不依赖其他组件。

缺点

时间可能不够精确。由于定时任务扫描的间隔是固定的,所以可能造成一些订单已经过期了一段时间才被扫描到,订单关闭的时间比正常时间晚一些。增加了数据库的压力。随着订单的数量越来越多,扫描的成本也会越来越大,执行时间也会被拉长,可能导致某些应该被关闭的订单迟迟没有被关闭。

总结 :采用定时任务的方案比较适合对时间要求不是很敏感,并且数据量不太多的业务场景。

③自定义线程池(可选)

Spring Scheduler默认使用单线程执行所有定时任务,若多个任务同时触发,会导致任务阻塞(一个任务执行慢,后续任务排队)。建议自定义线程池,提高任务并发能力:

复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import java.util.concurrent.ThreadPoolExecutor;

@Configuration
@EnableAsync // 启用异步执行(可选,结合@Async使用)
public class SchedulerConfig {

    /**
     * 自定义定时任务线程池
     */
    @Bean
    public ThreadPoolTaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(5); // 核心线程数
        scheduler.setThreadNamePrefix("scheduler-thread-"); // 线程名前缀
        scheduler.setAwaitTerminationSeconds(60); // 等待任务执行完成的时间
        scheduler.setWaitForTasksToCompleteOnShutdown(true); // 关闭时等待任务完成
        scheduler.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 拒绝策略(调用者执行,避免任务丢失)
        scheduler.initialize();
        return scheduler;
    }
}
2.1.3.2Quartz(复杂调度+集群部署)

Quartz是功能强大的开源定时任务框架,支持复杂调度规则、任务持久化和集群部署,适合需要"高可靠性"和"复杂调度"的场景。

①.引入依赖

复制代码
<!-- Quartz核心依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

<!-- 数据库依赖(用于任务持久化) -->
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

②.配置Quartz(application.yml)

配置Quartz的数据源(任务信息存入数据库)、线程池、集群模式:

复制代码
spring:
  # 数据源配置(任务持久化到MySQL)
  datasource:
    url: jdbc:mysql://localhost:3306/quartz_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
  # Quartz配置
  quartz:
    job-store-type: JDBC # 任务存储方式:JDBC(持久化)
    jdbc:
      initialize-schema: NEVER # 不自动初始化表结构(建议手动执行Quartz官方SQL)
    properties:
      org:
        quartz:
          scheduler:
            instanceName: quartzScheduler # 调度器实例名
            instanceId: AUTO # 实例ID自动生成(集群模式必须)
          threadPool:
            class: org.quartz.simpl.SimpleThreadPool
            threadCount: 10 # 线程池大小
            threadPriority: 5 # 线程优先级
          jobStore:
            class: org.quartz.impl.jdbcjobstore.JobStoreTX # 事务型存储
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate # MySQL delegate
            tablePrefix: QRTZ_ # 表前缀(Quartz官方表的默认前缀)
            isClustered: true # 启用集群模式
            clusterCheckinInterval: 15000 # 集群节点心跳间隔(15秒)
            useProperties: false # 不使用属性文件存储任务参数

③.Quartz的任务需实现 Job 接口,重写 execute 方法:

复制代码
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import java.util.UUID;

/**
 * Quartz任务:生成每日销售报表
 */
public class DailySalesReportJob implements Job {
    private final Logger log = LoggerFactory.getLogger(DailySalesReportJob.class);

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        // 绑定MDC链路标识
        String requestId = UUID.randomUUID().toString().replace("-", "");
        MDC.put("requestId", requestId);
        try {
            log.info("开始生成每日销售报表");
            // 核心业务逻辑:查询前一天的销售数据,生成Excel报表并存储
            // String reportPath = salesReportService.generateDailyReport();
            // log.info("每日销售报表生成完成,路径:{}", reportPath);
        } catch (Exception e) {
            log.error("生成每日销售报表失败", e);
            // 若任务失败,可抛出异常触发重试(需配置重试策略)
            throw new JobExecutionException("报表生成失败,触发重试", e, true);
        } finally {
            MDC.clear();
        }
    }
}

④.配置任务触发器(Trigger)

通过配置类创建JobDetail和Trigger,定义任务执行规则:

复制代码
import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class QuartzJobConfig {

    /**
     * 1. 定义JobDetail(任务详情)
     */
    @Bean
    public JobDetail dailySalesReportJobDetail() {
        return JobBuilder.newJob(DailySalesReportJob.class)
                .withIdentity("dailySalesReportJob", "reportGroup") // 任务标识(名称+组名)
                .storeDurably() // 任务即使没有触发器也持久化
                .build();
    }

    /**
     * 2. 定义Trigger(触发器):每天凌晨2点执行
     */
    @Bean
    public Trigger dailySalesReportTrigger() {
        // 调度规则:每天02:00:00执行
        CronScheduleBuilder cronSchedule = CronScheduleBuilder.cronSchedule("0 0 2 * * ?")
                .withMisfireHandlingInstructionDoNothing(); // 错过执行时间时,不执行(避免重复执行)

        return TriggerBuilder.newTrigger()
                .forJob(dailySalesReportJobDetail()) // 绑定任务
                .withIdentity("dailySalesReportTrigger", "reportGroup") // 触发器标识
                .withSchedule(cronSchedule) // 绑定调度规则
                .build();
    }
}
2.1.3.3XXL-Job(分布式任务调度平台)

XXL-Job是国内开源的分布式任务调度平台,基于"调度中心+执行器"架构,提供Web管理界面、任务监控、失败重试、分片执行等功能,是分布式微服务架构的首选方案。

(1)部署调度中心

从XXL-Job官网(https://www.xuxueli.com/xxl-job/)下载源码,执行源码中 doc/db/tables_xxl_job.sql SQL文件,创建调度中心数据库; 2. 修改调度中心配置文件(xxl-job-admin/src/main/resources/application.properties),配置数据库连接; 3. 启动调度中心(Spring Boot应用),访问 http://localhost:8080/xxl-job-admin,默认账号密码:admin/123456。

(2)集成执行器(Spring Boot项目)
① 引入依赖

代码语言:javascript

AI代码解释

复制代码
<!-- XXL-Job执行器依赖 -->
<dependency>
    <groupId>com.xuxueli</groupId>
    <artifactId>xxl-job-core</artifactId>
    <version>2.4.0</version>
</dependency>
② 配置执行器(application.yml)
复制代码
xxl:
  job:
    admin:
      addresses: http://localhost:8080/xxl-job-admin # 调度中心地址
    executor:
      appname: order-service-executor # 执行器名称(需在调度中心注册)
      address: "" # 执行器地址(空则自动注册)
      ip: "" # 执行器IP(空则自动获取)
      port: 9999 # 执行器端口
      logpath: ./logs/xxl-job/ # 任务日志路径
      logretentiondays: 30 # 日志保留天数
    accessToken: "" # 调度中心与执行器的通信令牌(空则关闭)
③ 配置执行器客户端
复制代码
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class XxlJobConfig {
    private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);

    @Value("${xxl.job.admin.addresses}")
    private String adminAddresses;

    @Value("${xxl.job.executor.appname}")
    private String appname;

    @Value("${xxl.job.executor.port}")
    private int port;

    @Value("${xxl.job.executor.logpath}")
    private String logPath;

    @Value("${xxl.job.accessToken}")
    private String accessToken;

    @Bean
    public XxlJobSpringExecutor xxlJobExecutor() {
        logger.info(">>>>>>>>>>> xxl-job config init.");
        XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
        xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
        xxlJobSpringExecutor.setAppname(appname);
        xxlJobSpringExecutor.setPort(port);
        xxlJobSpringExecutor.setLogPath(logPath);
        xxlJobSpringExecutor.setAccessToken(accessToken);
        return xxlJobSpringExecutor;
    }
}
④ 实现XXL-Job任务

通过 @XxlJob 注解定义任务,任务逻辑写在注解指定的方法中:

复制代码
import com.xxl.job.core.handler.annotation.XxlJob;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

/**
 * XXL-Job任务:订单超时关闭
 */
@Component
public class OrderTimeoutCloseJob {
    private final Logger log = LoggerFactory.getLogger(OrderTimeoutCloseJob.class);

    /**
     * 任务标识:orderTimeoutCloseHandler(需在调度中心配置对应Handler名称)
     */
    @XxlJob("orderTimeoutCloseHandler")
    public void orderTimeoutClose() {
        log.info("开始执行订单超时关闭任务");
        try {
            // 核心业务逻辑:查询所有创建时间超过30分钟且未支付的订单,执行关闭操作
            // int closeCount = orderService.closeTimeoutOrder(30);
            // log.info("订单超时关闭任务执行完成,共关闭订单:{} 个", closeCount);
        } catch (Exception e) {
            log.error("订单超时关闭任务执行失败", e);
            // 抛出异常,XXL-Job会自动记录失败状态,支持手动重试或配置自动重试
            throw new RuntimeException("订单超时关闭失败", e);
        }
    }
}
⑤ 调度中心配置任务
  1. 登录XXL-Job管理界面,在"执行器管理"中添加执行器(AppName与配置文件一致); 2. 在"任务管理"中创建任务: - 任务描述:订单超时关闭; - 执行器:选择已注册的执行器; - 任务类型:BEAN模式; - JobHandler:填写注解中的任务标识(orderTimeoutCloseHandler); - 调度规则:cron表达式(如0 */1 * * * ? 表示每分钟执行一次); 3. 启动任务,即可实现分布式环境下的定时执行。

2.2JDK延迟队列DelayQueue

DelayQueue 是 JDK 提供的一个无界队列,我们可以看到,DelayQueue 队列中的元素需要实现 Delayed,它只提供了一个方法,就是获取过期时间。

用户的订单生成以后,设置过期时间比如 30 分钟,放入定义好的 DelayQueue,然

后创建一个线程,在线程中通过 while(true)不断的从 DelayQueue 中获取过期的数

据。

优点:不依赖任何第三方组件,连数据库也不需要了,实现起来也方便。

缺点:因为 DelayQueue 是一个无界队列,如果放入的订单过多,会造成 JVM OOM。DelayQueue 基于 JVM 内存,如果 JVM 重启了,那所有数据就丢失了。

总结 :DelayQueue 适用于数据量较小,且丢失也不影响主业务的场景,比如内部系

统的一些非重要通知,就算丢失,也不会有太大影响。

2.3redis过期监听

概述

redis 是一个高性能的 KV 数据库,除了用作缓存以外,其实还提供了过期监听的功能。

在 redis.conf 中,配置 notify-keyspace-events Ex 即可开启此功能。

然后在代码中继承 KeyspaceEventMessageListener,实现 onMessage 就可以监听

过期的数据量。

Redis配置

复制代码
# 开启过期事件监听
redis-cli config set notify-keyspace-events Ex

监听器实现

复制代码
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;

@Component
public class RedisExpireListener extends KeyExpirationEventMessageListener {
    
    public RedisExpireListener(RedisMessageListenerContainer listenerContainer) {
        super(listenerContainer);
    }
    
    @Override
    public void onMessage(Message message, byte[] pattern) {
        // 获取过期的key
        String expiredKey = new String(message.getBody());
        
        // 只处理业务相关的key
        if (!expiredKey.startsWith("task:")) {
            return;
        }
        
        // 异步处理,避免阻塞监听线程
        new Thread(() -> processExpiredKey(expiredKey)).start();
    }
    
    private void processExpiredKey(String expiredKey) {
        System.out.println("处理过期任务: " + expiredKey);
        // 这里写你的业务逻辑
        // 例如:取消订单、发送通知等
    }
}

服务类添加延时任务:

复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class DelayTaskService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    /**
     * 添加延时任务
     * @param taskId 任务ID
     * @param taskData 任务数据
     * @param delaySeconds 延迟秒数
     */
    public void addDelayTask(String taskId, String taskData, long delaySeconds) {
        // 1. 存储任务数据
        String dataKey = "task:data:" + taskId;
        redisTemplate.opsForValue().set(dataKey, taskData);
        
        // 2. 设置会过期的key(触发监听)
        String expireKey = "task:" + taskId;
        redisTemplate.opsForValue().set(expireKey, "pending", delaySeconds, TimeUnit.SECONDS);
        
        System.out.println("添加延时任务: " + taskId + ", " + delaySeconds + "秒后执行");
    }
}

控制器展示

复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
public class TaskController {
    
    @Autowired
    private DelayTaskService delayTaskService;
    
    @PostMapping("/addTask")
    public String addTask(@RequestParam String taskId,
                         @RequestParam String taskData,
                         @RequestParam long delaySeconds) {
        
        delayTaskService.addDelayTask(taskId, taskData, delaySeconds);
        return "任务添加成功,将在" + delaySeconds + "秒后执行";
    }
    
    @PostMapping("/orderTimeout")
    public String addOrderTimeout(@RequestParam String orderId,
                                 @RequestParam(defaultValue = "1800") long delaySeconds) {
        
        String taskId = "order:" + orderId;
        String taskData = "{\"action\":\"cancel\",\"reason\":\"timeout\"}";
        
        delayTaskService.addDelayTask(taskId, taskData, delaySeconds);
        return "订单超时任务已添加: " + orderId;
    }
}

2.4Redission分布式延迟队列

2.4.1概述

Redisson 是一个基于 redis 实现的 Java 驻内存数据网格,它不仅提供了一系列的分

布式的 Java 常用对象,还提供了许多分布式服务。Redisson 除了提供我们常用的分布式锁外,还提供了一个分布式延迟队列RDelayedQueue,他是一种基于 zset 结构实现的延迟队列,其实现类是RedissonDelayedQueue。

**优点:**使用简单,并且其实现类中大量使用 lua 脚本保证其原子性,不会有并发重复问题。

缺点:需要依赖 redis(如果这算一种缺点的话)。

总结:Redisson 是 redis 官方推荐的 JAVA 客户端,提供了很多常用的功能,使用

简单、高效,推荐大家尝试使用。

2.4.2示例

①配置延迟时间
复制代码
spring:
  redis:
    host: localhost
    port: 6379

order:
  auto-close:
    delay-minutes: 30  # 30分钟后自动关闭
②核心服务类
复制代码
import org.redisson.api.RBlockingQueue;
import org.redisson.api.RDelayedQueue;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;

@Service
public class OrderAutoCloseService {
    
    @Autowired
    private RedissonClient redissonClient;
    
    @Value("${order.auto-close.delay-minutes:30}")
    private long delayMinutes;
    
    private RDelayedQueue<String> delayedQueue;
    private RBlockingQueue<String> blockingQueue;
    
    @PostConstruct //实现依赖注入完成之后执行初始化的逻辑;
    public void init() {
        // 1. 获取阻塞队列和延迟队列
        blockingQueue = redissonClient.getBlockingQueue("order:delay:queue");
        delayedQueue = redissonClient.getDelayedQueue(blockingQueue);
        
        // 2. 启动消费者线程
        startConsumer();
    }
    
    /**
     * 添加订单到延迟队列
     */
    public void addOrderToDelayQueue(String orderId) {
        delayedQueue.offer(orderId, delayMinutes, TimeUnit.MINUTES);
        System.out.println("订单 " + orderId + " 已加入延迟队列," + delayMinutes + "分钟后检查");
    }
    
    /**
     * 启动消费者线程
     */
    private void startConsumer() {
        new Thread(() -> {
            System.out.println("订单自动关闭消费者启动...");
            while (true) {
                try {
                    // 阻塞获取到期的订单ID
                    String orderId = blockingQueue.take();
                    // 处理订单
                    processOrder(orderId);
                } catch (InterruptedException e) {
                    System.out.println("消费者线程被中断");
                    break;
                }
            }
        }, "order-auto-close-consumer").start();
    }
    
    /**
     * 处理到期的订单
     */
    private void processOrder(String orderId) {
        System.out.println("检查订单状态: " + orderId);
        
        try {
            // 1. 查询订单状态(这里模拟查询)
            String orderStatus = getOrderStatus(orderId);
            
            // 2. 如果未支付,关闭订单
            if ("UNPAID".equals(orderStatus)) {
                closeOrder(orderId);
                System.out.println("订单 " + orderId + " 已自动关闭(未支付)");
            } else {
                System.out.println("订单 " + orderId + " 状态为: " + orderStatus + ",无需处理");
            }
            
        } catch (Exception e) {
            System.err.println("处理订单失败: " + orderId + ", 错误: " + e.getMessage());
            // 可以加入重试队列
        }
    }
    
    /**
     * 查询订单状态(模拟)
     */
    private String getOrderStatus(String orderId) {
        // 这里应该调用订单服务查询实际状态
        // 模拟:80%的概率是未支付
        return Math.random() > 0.2 ? "UNPAID" : "PAID";
    }
    
    /**
     * 关闭订单(模拟)
     */
    private void closeOrder(String orderId) {
        // 这里应该调用订单服务执行关闭操作:
        // 1. 更新订单状态为已关闭
        // 2. 释放库存
        // 3. 发送通知
        // 4. 记录日志
        
        System.out.println("执行关闭订单: " + orderId);
        // 模拟业务处理时间
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
    
    /**
     * 取消延迟任务(用户已支付)
     */
    public void cancelDelayTask(String orderId) {
        // 从延迟队列中移除订单
        boolean removed = delayedQueue.remove(orderId);
        if (removed) {
            System.out.println("订单 " + orderId + " 已从延迟队列移除(用户已支付)");
        }
    }
}
③.优缺点分析

2.5RabbitMQ 安装延迟队列

延迟消息,当消息写入到 Broker 后,不会立刻被消费者消费,需要等待指定的时

长后才可被消费处理的消息,称为延时消息。

(1).概述

在订单创建之后,我们就可以把订单作为一条消息投递到 rabbitmq,并将延迟时间设

置为 30 分钟,这样,30 分钟后我们定义的 consumer 就可以消费到这条消息,然

后检查用户是否支付了这个订单。通过延迟消息,我们就可以将业务解耦,极大地简化我们的代码逻辑。

(2).代码示例

①.配置类
复制代码
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMQConfig {
    
    // 延迟交换机(需要安装rabbitmq_delayed_message_exchange插件)
    @Bean
    public CustomExchange delayedExchange() {
        Map<String, Object> args = new HashMap<>();
        args.put("x-delayed-type", "direct"); // 延迟交换机的类型
        return new CustomExchange(
            "order.delayed.exchange", // 交换机名称
            "x-delayed-message",      // 交换机类型(必须)
            true,                     // 是否持久化
            false,                    // 是否自动删除
            args
        );
    }
    
    // 延迟队列
    @Bean
    public Queue orderCloseQueue() {
        return QueueBuilder.durable("order.close.queue").build();
    }
    
    // 绑定队列到延迟交换机
    @Bean
    public Binding binding() {
        return BindingBuilder.bind(orderCloseQueue())
                .to(delayedExchange())
                .with("order.close.key")
                .noargs();
    }
}
②.消息生产者
复制代码
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderDelayProducer {
    
    @Autowired
    private AmqpTemplate amqpTemplate;
    
    /**
     * 发送订单关闭延迟消息(30分钟后)
     */
    public void sendOrderCloseMessage(String orderId) {
        // 创建消息
        OrderCloseMessage message = new OrderCloseMessage(orderId);
        
        // 设置延迟时间(30分钟 = 1800000毫秒)
        MessagePostProcessor processor = message1 -> {
            message1.getMessageProperties().setDelay(1800000);
            return message1;
        };
        
        // 发送消息
        amqpTemplate.convertAndSend(
            "order.delayed.exchange", // 交换机
            "order.close.key",        // 路由键
            message,                  // 消息体
            processor                // 延迟处理器
        );
        
        System.out.println("发送订单关闭延迟消息: " + orderId);
    }
}

@Data
@AllArgsConstructor
class OrderCloseMessage {
    private String orderId;
    private Long createTime = System.currentTimeMillis();
}
③.消息消费者
复制代码
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderCloseConsumer {
    
    @Autowired
    private OrderService orderService;
    
    @RabbitListener(queues = "order.close.queue")
    public void handleOrderClose(OrderCloseMessage message) {
        System.out.println("收到订单关闭消息: " + message.getOrderId());
        
        try {
            // 查询订单状态
            String orderId = message.getOrderId();
            String status = orderService.getOrderStatus(orderId);
            
            if ("UNPAID".equals(status)) {
                // 关闭未支付订单
                orderService.closeUnpaidOrder(orderId);
                System.out.println("订单自动关闭成功: " + orderId);
            } else {
                System.out.println("订单状态为 " + status + ",无需关闭: " + orderId);
            }
            
        } catch (Exception e) {
            System.err.println("处理订单关闭失败: " + e.getMessage());
        }
    }
}
④.订单服务类
复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderService {
    
    @Autowired
    private OrderDelayProducer orderDelayProducer;
    
    /**
     * 创建订单
     */
    public String createOrder() {
        // 1. 生成订单ID
        String orderId = "ORDER" + System.currentTimeMillis();
        
        // 2. 保存订单到数据库(简化)
        System.out.println("创建订单: " + orderId);
        
        // 3. 发送30分钟延迟消息
        orderDelayProducer.sendOrderCloseMessage(orderId);
        
        return orderId;
    }
    
    /**
     * 支付成功
     */
    public void paySuccess(String orderId) {
        System.out.println("订单支付成功: " + orderId);
        // 更新订单状态...
    }
    
    /**
     * 查询订单状态
     */
    public String getOrderStatus(String orderId) {
        // 模拟:80%概率未支付
        return Math.random() > 0.2 ? "UNPAID" : "PAID";
    }
    
    /**
     * 关闭未支付订单
     */
    public void closeUnpaidOrder(String orderId) {
        System.out.println("关闭未支付订单: " + orderId);
        // 业务逻辑:更新状态、释放库存等
    }
}

(3).优缺点分析

优点:可以使代码逻辑清晰,系统之间完全解耦,只需关注生产及消费消息即可。另外

其吞吐量极高,最多可以支撑万亿级的数据量。

缺点:相对来说 mq 是重量级的组件,引入 mq 之后,随之而来的消息丢失、幂等

性问题等都加深了系统的复杂度。

总结:通过 mq 进行系统业务解耦,以及对系统性能削峰填谷已经是当前高性能系统

的标配。

2.6死信队列

除了 RocketMQ 的延迟队列,RabbitMQ 的死信队列也可以实现消息延迟功能。

(1).概述

当 RabbitMQ 中的一条正常消息,因为过了存活时间(TTL 过期)、队列长度超限、

被消费者拒绝等原因无法被消费时,就会被当成一条死信消息,投递到死信队列。

基于这样的机制,我们可以给消息设置一个 ttl,然后故意不消费消息,等消息过期就

会进入死信队列,我们再消费死信队列即可。通过这样的方式,就可以达到同 RocketMQ 延迟消息一样的效果。

(2).演示

①.配置交换机路由关系
复制代码
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class RabbitMQConfig {
    
    // 1. 订单交换机(普通交换机)
    @Bean
    public DirectExchange orderExchange() {
        return new DirectExchange("order.exchange");
    }
    
    // 2. 死信交换机
    @Bean
    public DirectExchange orderDLXExchange() {
        return new DirectExchange("order.dlx.exchange");
    }
    
    // 3. 延迟队列(30分钟TTL,过期后转到死信队列)
    @Bean
    public Queue orderDelayQueue() {
        Map<String, Object> args = new HashMap<>();
        // 设置死信交换机
        args.put("x-dead-letter-exchange", "order.dlx.exchange");
        // 设置死信路由键
        args.put("x-dead-letter-routing-key", "order.close.key");
        // 设置TTL:30分钟(1800000毫秒)
        args.put("x-message-ttl", 1800000);
        // 队列最大长度(防止内存溢出)
        args.put("x-max-length", 10000);
        
        return new Queue("order.delay.queue", true, false, false, args);
    }
    
    // 4. 死信队列(实际消费队列)
    @Bean
    public Queue orderCloseQueue() {
        return new Queue("order.close.queue", true);
    }
    
    // 5. 绑定延迟队列到普通交换机
    @Bean
    public Binding delayBinding() {
        return BindingBuilder.bind(orderDelayQueue())
                .to(orderExchange())
                .with("order.delay.key");
    }
    
    // 6. 绑定死信队列到死信交换机
    @Bean
    public Binding closeBinding() {
        return BindingBuilder.bind(orderCloseQueue())
                .to(orderDLXExchange())
                .with("order.close.key");
    }
}
②.生产者
复制代码
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderDelayProducer {
    
    @Autowired
    private AmqpTemplate amqpTemplate;
    
    /**
     * 发送订单延迟消息
     * 消息会先进入order.delay.queue队列,30分钟后过期
     * 过期后转到order.close.queue队列被消费
     */
    public void sendOrderCloseMessage(String orderId) {
        // 创建消息内容
        String message = "订单ID:" + orderId;
        
        // 发送到延迟队列
        amqpTemplate.convertAndSend(
            "order.exchange",      // 交换机
            "order.delay.key",     // 路由键
            message               // 消息内容
        );
        
        System.out.println("发送订单延迟消息: " + orderId);
    }
}
③.消费者
复制代码
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;

@Service
public class OrderCloseConsumer {
    
    /**
     * 监听死信队列(订单关闭队列)
     * 消息在延迟队列中30分钟后,会转到这个队列
     */
    @RabbitListener(queues = "order.close.queue")
    public void handleOrderClose(String message) {
        System.out.println("收到订单关闭消息: " + message);
        
        // 提取订单ID(实际项目中应该用JSON解析)
        String orderId = message.replace("订单ID:", "");
        
        try {
            // 检查订单状态
            String status = checkOrderStatus(orderId);
            
            if ("UNPAID".equals(status)) {
                // 执行订单关闭逻辑
                closeOrder(orderId);
                System.out.println("订单自动关闭成功: " + orderId);
            } else {
                System.out.println("订单已支付,无需关闭: " + orderId);
            }
            
        } catch (Exception e) {
            System.err.println("处理订单失败: " + orderId + ", 错误: " + e.getMessage());
        }
    }
    
    private String checkOrderStatus(String orderId) {
        // 模拟查询订单状态
        // 实际应该查询数据库
        return Math.random() > 0.2 ? "UNPAID" : "PAID";
    }
    
    private void closeOrder(String orderId) {
        System.out.println("执行订单关闭: " + orderId);
        // 实际业务逻辑:
        // 1. 更新订单状态为已关闭
        // 2. 释放库存
        // 3. 发送通知
    }
}
④.订单服务
复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderService {
    
    @Autowired
    private OrderDelayProducer orderDelayProducer;
    
    /**
     * 创建订单
     */
    public String createOrder() {
        // 1. 生成订单ID
        String orderId = generateOrderId();
        
        // 2. 保存订单到数据库(简化)
        System.out.println("创建订单: " + orderId);
        
        // 3. 发送延迟消息(30分钟后检查是否支付)
        orderDelayProducer.sendOrderCloseMessage(orderId);
        
        return orderId;
    }
    
    /**
     * 支付成功
     */
    public void paySuccess(String orderId) {
        System.out.println("订单支付成功: " + orderId);
        // 更新订单状态为已支付
        // 注意:死信队列消息无法取消,需要在消费时判断
    }
    
    private String generateOrderId() {
        return "ORDER_" + System.currentTimeMillis();
    }
}

(3).分析

优点:同 RocketMQ 一样,RabbitMQ 同样可以使业务解耦,基于其集群的扩展性,

也可以实现高可用、高性能的目标。

缺点:死信队列本质还是一个队列,队列都是先进先出,如果队头的消息过期时间比较

长,就会导致后面过期的消息无法得到及时消费,造成消息阻塞。

相关推荐
LcVong17 小时前
WPF MediaPlayer获取网络视频流当前帧并展示图片完整范例
网络·wpf
bugcome_com18 小时前
WPF数据绑定入门:从传统事件到5种绑定模式
wpf
LateFrames20 小时前
我用 WPF 做了一个 “苍蝇飞舞” 的屏保
ui·wpf
wuty0071 天前
完善基于WPF开发的标尺控件(含实例代码)
wpf·wpf标尺·支持横向竖向标尺·ruler
浩浩测试一下2 天前
洪水猛兽攻击 Ddos Dos cc Drdos floods区别
安全·web安全·网络安全·系统安全·wpf·可信计算技术·安全架构
无心水2 天前
分布式环境下定时任务与SELECT FOR UPDATE的陷阱与解决方案
分布式·后端·wpf·xxl-job·quartz·定时任务·selectforupdate
xdpcxq10292 天前
Spring AOP + Guava RateLimiter 用注解实现优雅限流
spring·wpf·guava
Aevget3 天前
界面控件DevExpress WPF v25.2新版亮点:模板工具包全新升级
wpf·界面控件·devexpress·ui开发·.net 10
czhc11400756633 天前
wpf 129
wpf
码界奇点4 天前
基于eBPF技术的高性能网络防火墙系统设计与实现
开发语言·网络·毕业设计·php·wpf·go语言·源代码管理