一、数据库表

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);
    }
}

核心逻辑总结(你完全理解的版本)

  1. 下单
    • 查库存 + version
    • version = 旧版本 做乐观锁扣库存,真正防超卖
    • 生成待支付订单
    • 发送 30 分钟延迟消息
     
  2. 用户支付
    • 订单状态改为 1=已支付
     
  3. 30 分钟后消息到达
    • 检查订单状态
    • 已支付 → 直接跳过
    • 未支付 → CAS 关单 + 回滚库存
  4. 库存回滚为什么不加乐观锁?
    • 回滚是 +1,不会超卖
    • UPDATE order ... WHERE status=0 保证只执行一次
    • 无并发冲突,不需要锁

 

posted on 2026-07-28 18:21  从精通到陌生  阅读(18)  评论(0)    收藏  举报