核心业务逻辑:
- 用户下单 → 创建待支付订单 → 扣减商品库存
- 启动超时检测:超时未支付 → 订单状态改为已关闭 → 回滚库存
- 支付成功 → 终止超时任务,不回滚库存
实现方案分两种:
- 简易版: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 延迟队列:
- 下单时发送延迟消息(延迟 30 分钟)
- 支付成功则手动拒绝消息
- 延迟时间到,消费消息 → 关单 + 回滚库存
核心逻辑(简化)
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);}
}
四、关键注意事项
- 事务一致性:关单和库存回滚必须加
@Transactional,保证原子性 - 乐观锁:扣库存用乐观锁,防止超卖
- 幂等性:重复执行定时任务 / 消息时,通过订单状态双重校验,避免重复回滚库存
- 并发安全:支付成功和超时关单并发时,先校验订单状态再操作
- 精度选择:小项目用定时任务,高并发 / 精准要求用延迟队列
需要我帮你补充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. 流程说明
-
用户下单
- 扣减库存(乐观锁防超卖)
- 生成待支付订单
- 发送 30 分钟延迟消息 到 RabbitMQ
-
正常支付
- 调用
paySuccess更新订单状态为「已支付」 - 延迟消息到期后,消费者校验状态,不执行关单 & 回滚
- 调用
-
超时未支付
- 延迟消息投递
- 消费者校验订单仍为「待支付」
- 关闭订单 + 回滚库存(事务保证原子性)
10. 关键保障点
- 精准超时:无定时任务轮询误差
- 事务一致性:关单 + 回滚库存在同一事务
- 并发安全:使用
status=0做 CAS 更新,避免重复关单 - 幂等性:重复消费不会重复回滚库存
- 防超卖:扣库存使用乐观锁