当前位置: 首页 > news >正文

支付超时自动关单 + 库存回滚 Java 实现方案

核心业务逻辑:
 
  1. 用户下单 → 创建待支付订单 → 扣减商品库存
  2. 启动超时检测:超时未支付 → 订单状态改为已关闭 → 回滚库存
  3. 支付成功 → 终止超时任务,不回滚库存
 
实现方案分两种:
 
  • 简易版:Spring Schedule 定时任务轮询(适合中小流量、对超时精度要求不高场景)
  • 精准版:RabbitMQ 延迟队列(适合高并发、超时时间精准场景)
 

一、基础准备

 

1. 数据库表设计

 
sql
 
 
-- 订单表
CREATE TABLE t_order (id BIGINT PRIMARY KEY AUTO_INCREMENT,order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '订单号',product_id BIGINT NOT NULL COMMENT '商品ID',user_id BIGINT NOT NULL COMMENT '用户ID',status TINYINT NOT NULL DEFAULT 0 COMMENT '0-待支付 1-已支付 2-已关闭',create_time DATETIME NOT NULL COMMENT '创建时间',pay_time DATETIME NULL COMMENT '支付时间'
);-- 库存表
CREATE TABLE t_stock (id BIGINT PRIMARY KEY AUTO_INCREMENT,product_id BIGINT NOT NULL UNIQUE COMMENT '商品ID',stock_num INT NOT NULL DEFAULT 0 COMMENT '库存数量',version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号'
);
 
 

2. Maven 核心依赖

 
xml
 
 
<!-- Spring Boot Web -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis-Plus -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.3</version>
</dependency>
<!-- MySQL -->
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional>
</dependency>
 
 

二、核心代码实现(简易版:定时任务轮询)

 

1. 实体类

 

Order.java

 
java
 
 
import lombok.Data;
import java.time.LocalDateTime;@Data
public class Order {private Long id;private String orderNo;private Long productId;private Long userId;/** 0-待支付 1-已支付 2-已关闭 */private Integer status;private LocalDateTime createTime;private LocalDateTime payTime;
}
 
 

Stock.java

 
java
 
 
import lombok.Data;@Data
public class Stock {private Long id;private Long productId;private Integer stockNum;private Integer version;
}
 
 

2. Mapper 接口

 

OrderMapper.java

 
java
 
 
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;public interface OrderMapper extends BaseMapper<Order> {// 查询超时未支付订单(超时时间:30分钟)List<Order> selectTimeoutUnpaidOrders(@Param("timeoutMinutes") Integer timeoutMinutes);
}
 
 

StockMapper.java

 
java
 
 
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;public interface StockMapper extends BaseMapper<Stock> {// 扣减库存(乐观锁)int deductStock(@Param("productId") Long productId);// 回滚库存int rollbackStock(@Param("productId") Long productId);
}
 
 

3. Mapper XML

 

OrderMapper.xml

 
xml
 
 
<select id="selectTimeoutUnpaidOrders" resultType="com.example.entity.Order">SELECT * FROM t_orderWHERE status = 0AND create_time < DATE_SUB(NOW(), INTERVAL #{timeoutMinutes} MINUTE)
</select>
 
 

StockMapper.xml

 
xml
 
 
<!-- 扣减库存:乐观锁防止超卖 -->
<update id="deductStock">UPDATE t_stockSET stock_num = stock_num - 1, version = version + 1WHERE product_id = #{productId} AND stock_num > 0
</update><!-- 库存回滚 -->
<update id="rollbackStock">UPDATE t_stockSET stock_num = stock_num + 1WHERE product_id = #{productId}
</update>
 
 

4. 核心 Service 层

 

PayService.java(创建订单 + 扣库存)

 
java
 
 
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.UUID;@Service
public class PayService {@Resourceprivate OrderMapper orderMapper;@Resourceprivate StockMapper stockMapper;/*** 创建订单 + 扣减库存*/@Transactional(rollbackFor = Exception.class)public String createOrder(Long userId, Long productId) {// 1. 扣减库存(乐观锁)int rows = stockMapper.deductStock(productId);if (rows == 0) {throw new RuntimeException("库存不足,下单失败");}// 2. 创建待支付订单Order order = new Order();order.setOrderNo(UUID.randomUUID().toString().replace("-", ""));order.setUserId(userId);order.setProductId(productId);order.setStatus(0);order.setCreateTime(LocalDateTime.now());orderMapper.insert(order);return order.getOrderNo();}/*** 支付成功:更新订单状态*/@Transactional(rollbackFor = Exception.class)public void paySuccess(String orderNo) {Order order = orderMapper.selectOne(new LambdaQueryWrapper<Order>().eq(Order::getOrderNo, orderNo));if (order == null || order.getStatus() != 0) {throw new RuntimeException("订单状态异常");}order.setStatus(1);order.setPayTime(LocalDateTime.now());orderMapper.updateById(order);}
}
 
 

TimeoutOrderService.java(超时关单 + 库存回滚)

 
java
 
 
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.List;@Service
public class TimeoutOrderService {@Resourceprivate OrderMapper orderMapper;@Resourceprivate StockMapper stockMapper;/*** 定时任务:每分钟执行一次,关闭30分钟未支付订单* fixedRate = 60000 → 每60秒执行一次*/@Scheduled(fixedRate = 60000)public void handleTimeoutOrder() {// 1. 查询30分钟未支付的订单List<Order> timeoutOrders = orderMapper.selectTimeoutUnpaidOrders(30);if (timeoutOrders.isEmpty()) {return;}// 2. 遍历关单+回滚库存for (Order order : timeoutOrders) {closeOrderAndRollbackStock(order);}}/*** 事务:关闭订单 + 回滚库存*/@Transactional(rollbackFor = Exception.class)public void closeOrderAndRollbackStock(Order order) {// 双重校验:防止订单已支付时被误关if (order.getStatus() != 0) {return;}// 1. 关闭订单order.setStatus(2);orderMapper.updateById(order);// 2. 回滚库存stockMapper.rollbackStock(order.getProductId());}
}
 
 

5. 启动类开启定时任务

 
java
 
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;@SpringBootApplication
@EnableScheduling // 开启定时任务
public class PayApplication {public static void main(String[] args) {SpringApplication.run(PayApplication.class, args);}
}
 
 

三、进阶方案:RabbitMQ 延迟队列(精准超时)

 
定时任务有轮询延迟(比如每 1 分钟执行一次,最大误差 1 分钟),高并发 / 精准场景用RabbitMQ 延迟队列:
 
  1. 下单时发送延迟消息(延迟 30 分钟)
  2. 支付成功则手动拒绝消息
  3. 延迟时间到,消费消息 → 关单 + 回滚库存
 

核心逻辑(简化)

 
java
 
 
// 1. 下单时发送延迟消息
rabbitTemplate.convertAndSend("delay_exchange", "delay_key", orderNo, message -> {// 设置延迟30分钟message.getMessageProperties().setDelay(30 * 60 * 1000);return message;
});// 2. 延迟队列消费者
@RabbitListener(queues = "delay_queue")
public void consumeTimeoutOrder(String orderNo) {Order order = orderMapper.selectOne(lambdaQuery.eq(Order::getOrderNo, orderNo));if (order != null && order.getStatus() == 0) {timeoutOrderService.closeOrderAndRollbackStock(order);}
}
 
 

四、关键注意事项

 
  1. 事务一致性:关单和库存回滚必须加 @Transactional,保证原子性
  2. 乐观锁:扣库存用乐观锁,防止超卖
  3. 幂等性:重复执行定时任务 / 消息时,通过订单状态双重校验,避免重复回滚库存
  4. 并发安全:支付成功和超时关单并发时,先校验订单状态再操作
  5. 精度选择:小项目用定时任务,高并发 / 精准要求用延迟队列
 
需要我帮你补充RabbitMQ 延迟队列完整配置代码,或者分布式锁(Redisson)防止多节点重复执行的实现吗?
 
进阶方案:RabbitMQ 延迟队列(精准超时)那你重新写下代码,我想看下具体方案
 
 
已完成思考
 
 
 
下面给你一套可直接运行的 Spring Boot + RabbitMQ 延迟队列完整实现方案,用于支付超时自动关单 + 库存回滚。
 
方案采用 RabbitMQ 延迟消息插件(rabbitmq_delayed_message_exchange),超时时间精准、无轮询误差。
 

 

1. 环境准备

 

1.1 安装 RabbitMQ 延迟插件

 
去 RabbitMQ 官网下载对应版本的 rabbitmq_delayed_message_exchange 插件,放到 plugins 目录,执行:
 
bash
 
 
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
 
 
重启 RabbitMQ 即可。
 

1.2 Maven 依赖

 
xml
 
 
<!-- Spring Boot AMQP RabbitMQ -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId>
</dependency><!-- MyBatis-Plus -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.3.1</version>
</dependency><!-- MySQL -->
<dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope>
</dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional>
</dependency>
 
 

1.3 application.yml

 
yaml
 
 
spring:# 数据库datasource:url: jdbc:mysql://localhost:3306/pay_demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghaiusername: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Driver# RabbitMQrabbitmq:host: localhostport: 5672username: guestpassword: guestvirtual-host: /# 发送确认publisher-confirm-type: correlatedpublisher-returns: true# 自定义超时时间:30分钟
order:timeout: 1800000
 
 

 

2. 数据库表(不变)

 
sql
 
 
CREATE TABLE t_order
(id          BIGINT PRIMARY KEY AUTO_INCREMENT,order_no    VARCHAR(64)  NOT NULL UNIQUE COMMENT '订单号',product_id  BIGINT       NOT NULL COMMENT '商品ID',user_id     BIGINT       NOT NULL COMMENT '用户ID',status      TINYINT      NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2已关闭',create_time DATETIME     NOT NULL,pay_time    DATETIME     NULL
);CREATE TABLE t_stock
(id         BIGINT PRIMARY KEY AUTO_INCREMENT,product_id BIGINT NOT NULL UNIQUE,stock_num  INT    NOT NULL DEFAULT 0,version    INT    NOT NULL DEFAULT 0 COMMENT '乐观锁'
);
 
 

 

3. RabbitMQ 核心配置(延迟队列)

 

DelayRabbitConfig.java

 
java
 
 
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.CustomExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;@Configuration
public class DelayRabbitConfig {// 交换机public static final String DELAY_EXCHANGE = "order.delay.exchange";// 队列public static final String DELAY_QUEUE = "order.delay.queue";// routingKeypublic static final String DELAY_ROUTING_KEY = "order.delay.routingKey";/*** 声明延迟交换机(使用插件模式)*/@Beanpublic CustomExchange delayExchange() {Map<String, Object> args = new HashMap<>();args.put("x-delayed-type", "direct");// 类型为 x-delayed-messagereturn new CustomExchange(DELAY_EXCHANGE, "x-delayed-message", true, false, args);}/*** 延迟队列*/@Beanpublic Queue delayQueue() {return new Queue(DELAY_QUEUE, true);}/*** 绑定*/@Beanpublic Binding delayBinding() {return BindingBuilder.bind(delayQueue()).to(delayExchange()).with(DELAY_ROUTING_KEY).noargs();}
}
 
 

 

4. 实体类

 

Order.java

 
java
 
 
import lombok.Data;
import java.time.LocalDateTime;@Data
public class Order {private Long id;private String orderNo;private Long productId;private Long userId;/** 0待支付 1已支付 2已关闭 */private Integer status;private LocalDateTime createTime;private LocalDateTime payTime;
}
 
 

Stock.java

 
java
 
 
import lombok.Data;@Data
public class Stock {private Long id;private Long productId;private Integer stockNum;private Integer version;
}
 
 

 

5. Mapper 层

 

OrderMapper

 
java
 
 
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;public interface OrderMapper extends BaseMapper<Order> {@Update("UPDATE t_order SET status = 2 WHERE order_no = #{orderNo} AND status = 0")int closeOrderIfUnpaid(@Param("orderNo") String orderNo);
}
 
 

StockMapper

 
java
 
 
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;public interface StockMapper extends BaseMapper<Stock> {// 扣库存(乐观锁)@Update("UPDATE t_stock SET stock_num = stock_num - 1, version = version + 1 " +"WHERE product_id = #{productId} AND stock_num > 0")int deductStock(@Param("productId") Long productId);// 库存回滚@Update("UPDATE t_stock SET stock_num = stock_num + 1 WHERE product_id = #{productId}")int rollbackStock(@Param("productId") Long productId);
}
 
 

 

6. 核心业务逻辑

 

6.1 订单发送延迟消息工具类

 
java
 
 
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;@Component
public class OrderDelaySender {@Resourceprivate RabbitTemplate rabbitTemplate;@Value("${order.timeout}")private long orderTimeout;/*** 发送延迟消息*/public void sendDelayMessage(String orderNo) {rabbitTemplate.convertAndSend(DelayRabbitConfig.DELAY_EXCHANGE,DelayRabbitConfig.DELAY_ROUTING_KEY,orderNo,message -> {// 设置延迟时间(毫秒)message.getMessageProperties().setDelay((int) orderTimeout);return message;});}
}
 
 

6.2 下单 + 扣库存 + 发延迟消息

 
java
 
 
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.UUID;@Service
public class OrderService {@Resourceprivate OrderMapper orderMapper;@Resourceprivate StockMapper stockMapper;@Resourceprivate OrderDelaySender orderDelaySender;/*** 创建订单:扣库存 + 生成订单 + 发送延迟消息*/@Transactional(rollbackFor = Exception.class)public String createOrder(Long userId, Long productId) {// 1. 扣减库存int rows = stockMapper.deductStock(productId);if (rows == 0) {throw new RuntimeException("库存不足");}// 2. 生成订单String orderNo = UUID.randomUUID().toString().replace("-", "");Order order = new Order();order.setOrderNo(orderNo);order.setUserId(userId);order.setProductId(productId);order.setStatus(0);order.setCreateTime(LocalDateTime.now());orderMapper.insert(order);// 3. 发送延迟消息(30分钟后执行关单)orderDelaySender.sendDelayMessage(orderNo);return orderNo;}/*** 支付成功:更新订单状态*/@Transactional(rollbackFor = Exception.class)public void paySuccess(String orderNo) {Order order = orderMapper.selectOne(new LambdaQueryWrapper<Order>().eq(Order::getOrderNo, orderNo));if (order == null || !order.getStatus().equals(0)) {throw new RuntimeException("订单状态非法");}order.setStatus(1);order.setPayTime(LocalDateTime.now());orderMapper.updateById(order);}
}
 
 

 

7. 延迟消息消费者(超时关单 + 库存回滚)

 
java
 
 
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;@Service
public class OrderDelayConsumer {@Resourceprivate OrderMapper orderMapper;@Resourceprivate StockMapper stockMapper;/*** 监听延迟队列,处理超时未支付订单*/@RabbitListener(queues = DelayRabbitConfig.DELAY_QUEUE)@Transactional(rollbackFor = Exception.class)public void handleTimeoutOrder(String orderNo) {// 1. 查询订单Order order = orderMapper.selectOne(new LambdaQueryWrapper<Order>().eq(Order::getOrderNo, orderNo));// 订单不存在 或 已支付,直接返回if (order == null || !order.getStatus().equals(0)) {return;}// 2. 关闭订单(CAS 防止并发重复关单)int closeRows = orderMapper.closeOrderIfUnpaid(orderNo);if (closeRows == 0) {// 已被其他线程关闭/支付return;}// 3. 回滚库存stockMapper.rollbackStock(order.getProductId());System.out.println("订单超时关闭,库存已回滚:" + orderNo);}
}
 
 

 

8. 启动类

 
java
 
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class DelayOrderApplication {public static void main(String[] args) {SpringApplication.run(DelayOrderApplication.class, args);}
}
 
 

 

9. 流程说明

 
  1. 用户下单
     
    • 扣减库存(乐观锁防超卖)
    • 生成待支付订单
    • 发送 30 分钟延迟消息 到 RabbitMQ
     
  2. 正常支付
     
    • 调用 paySuccess 更新订单状态为「已支付」
    • 延迟消息到期后,消费者校验状态,不执行关单 & 回滚
     
  3. 超时未支付
     
    • 延迟消息投递
    • 消费者校验订单仍为「待支付」
    • 关闭订单 + 回滚库存(事务保证原子性)
     
 

 

10. 关键保障点

 
  • 精准超时:无定时任务轮询误差
  • 事务一致性:关单 + 回滚库存在同一事务
  • 并发安全:使用 status=0 做 CAS 更新,避免重复关单
  • 幂等性:重复消费不会重复回滚库存
  • 防超卖:扣库存使用乐观锁
http://www.jsqmd.com/news/1282189/

相关文章:

  • incude指令和include行为
  • OpenRocket终极指南:免费开源火箭仿真软件从零到精通
  • 蒸发浓缩与结晶厂家2026年实力榜:五大品牌深度评测与选型指南 - 品牌推荐
  • linux 判断服务是否在线 ss命令
  • BCI Competition IV 2a数据集实战指南:从数据加载到运动想象分类的完整流程
  • [数据结构]一般顺序表部分总结
  • eclipse使用alt+/很卡的解决方法
  • golang微服务框架go-micro 入门笔记2.3 micro工具之消息接收和发布
  • 拼多多SKU运营全攻略:从基础设置到推广变现
  • 物联网设备安全芯片SE050与PIC18集成方案解析
  • 47. 全排列 II
  • 山西瞪羚企业申报全解析:吃透申报要点,专业赋能企业冲刺认定 - 资讯在线
  • 这个简易警报电路能够工作吗?
  • 深圳夏令营:军博营地专业严谨 - 17328623207
  • 全国武校家长口碑排名,圣龙武术学校真实评价 - 圣龙武术朱老师
  • [LeetCode]303. Range Sum Query - Immutable ★
  • 中山夏令营:军博营地效果突出 - 17728181569
  • PAT-1041(乙级)
  • 2026重庆綦江管道疏通防坑指南:利扬师傅教你避开隐形收费 - 余生黄金回收
  • 关于MySQL多表关联时过滤条件的位置
  • python 类中的递归函数使用
  • Hibernate一对多关系
  • 斐波那契数列的实现
  • Jetson Nano视觉数据增强实战:平衡效果与计算开销的边缘优化方案
  • 2026重庆合川管道疏通避坑指南 邻里帮师傅真实测评 - 余生黄金回收
  • 深圳夏令营:军博营地典范 - 17728098551
  • 最后窗口期!2024Q3前未掌握AI办公链路的职场人,将错过晋升关键分水岭
  • 高效管理PS1游戏存档:MemcardRex完整使用指南
  • Godot着色器基础
  • 【单片机毕业设计推荐】 基于 51/STM32 单片机的智能感应台灯控制系统设计与实现,基于 51/STM32 单片机的蓝牙可控人体感应调光台灯设计(011904)