一、数据库表
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 '乐观锁版本号' );
二、Maven 依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<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>
</dependencies>
三、application.yml
spring: datasource: url: jdbc:mysql://localhost:3306/pay_demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver rabbitmq: host: localhost port: 5672 username: guest password: guest virtual-host: / order: timeout: 1800000 # 30分钟
四、RabbitMQ 延迟队列配置
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"; public static final String DELAY_ROUTING_KEY = "order.delay.rk"; @Bean public CustomExchange delayExchange() { Map<String, Object> args = new HashMap<>(); args.put("x-delayed-type", "direct"); return new CustomExchange(DELAY_EXCHANGE, "x-delayed-message", true, false, args); } @Bean public Queue delayQueue() { return new Queue(DELAY_QUEUE, true); } @Bean public Binding binding() { return BindingBuilder.bind(delayQueue()) .to(delayExchange()) .with(DELAY_ROUTING_KEY) .noargs(); } }
五、实体类
Order
import lombok.Data; import java.time.LocalDateTime; @Data public class Order { private Long id; private String orderNo; private Long productId; private Long userId; private Integer status; // 0待支付 1已支付 2已关闭 private LocalDateTime createTime; private LocalDateTime payTime; }
Stock
import lombok.Data; @Data public class Stock { private Long id; private Long productId; private Integer stockNum; private Integer version; }
六、Mapper 层
OrderMapper
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
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 version = #{oldVersion} " + " AND stock_num > 0") int deductStock(@Param("productId") Long productId, @Param("oldVersion") Integer oldVersion); /** * 库存回滚 */ @Update("UPDATE t_stock SET stock_num = stock_num + 1 WHERE product_id = #{productId}") int rollbackStock(@Param("productId") Long productId); }
七、延迟消息发送器
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 OrderDelayMessageSender { @Resource private RabbitTemplate rabbitTemplate; @Value("${order.timeout}") private long orderTimeout; public void sendDelayOrderMessage(String orderNo) { rabbitTemplate.convertAndSend( DelayRabbitConfig.DELAY_EXCHANGE, DelayRabbitConfig.DELAY_ROUTING_KEY, orderNo, message -> { message.getMessageProperties().setDelay((int) orderTimeout); return message; } ); } }
八、核心业务 Service
OrderService(下单 + 支付)
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 { @Resource private OrderMapper orderMapper; @Resource private StockMapper stockMapper; @Resource private OrderDelayMessageSender delaySender; /** * 创建订单 + 扣库存(正确乐观锁) */ @Transactional(rollbackFor = Exception.class) public String createOrder(Long userId, Long productId) { // 1. 查询库存与版本号 Stock stock = stockMapper.selectOne( new LambdaQueryWrapper<Stock>().eq(Stock::getProductId, productId) ); if (stock == null || stock.getStockNum() <= 0) { throw new RuntimeException("库存不足"); } // 2. 乐观锁扣库存 int updateRows = stockMapper.deductStock(productId, stock.getVersion()); if (updateRows == 0) { throw new RuntimeException("库存不足,抢购失败"); } // 3. 创建待支付订单 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); // 4. 发送30分钟延迟消息 delaySender.sendDelayOrderMessage(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); } }
九、超时关单消费者(兜底)
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 OrderTimeoutConsumer { @Resource private OrderMapper orderMapper; @Resource private 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); } }
十、启动类
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class OrderTimeoutApplication { public static void main(String[] args) { SpringApplication.run(OrderTimeoutApplication.class, args); } }
核心逻辑总结(你完全理解的版本)
-
下单
- 查库存 + version
- 用
version = 旧版本做乐观锁扣库存,真正防超卖 - 生成待支付订单
- 发送 30 分钟延迟消息
-
用户支付
- 订单状态改为
1=已支付
- 订单状态改为
-
30 分钟后消息到达
- 检查订单状态
- 已支付 → 直接跳过
- 未支付 → CAS 关单 + 回滚库存
-
库存回滚为什么不加乐观锁?
- 回滚是
+1,不会超卖 - 靠
UPDATE order ... WHERE status=0保证只执行一次 - 无并发冲突,不需要锁
- 回滚是
浙公网安备 33010602011771号