电商订单系统设计( 高并发版)

环境:SpringBoot 2.5.15 + Java8 + MySQL8 + Redis + Redisson + RabbitMQ + 原生MyBatis XML包含:数据库全表、完整配置、全部实体、常量、工具类、Mapper+XML、Service、Controller、防重令牌、分布式锁、Redis 缓存、MQ 延时关单、乐观锁防超卖、短事务、支付 / 取消 / 超时关闭


一、SQL 全量建表语句

sql

CREATE DATABASE IF NOT EXISTS shop_full DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE shop_full;

-- 商品表(乐观锁version)
CREATE TABLE product (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    product_name VARCHAR(100) NOT NULL COMMENT '商品名称',
    price DECIMAL(10,2) NOT NULL COMMENT '单价',
    stock INT NOT NULL DEFAULT 0 COMMENT '库存',
    version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 订单主表
CREATE TABLE order_info (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_no VARCHAR(32) NOT NULL COMMENT '订单号',
    user_id BIGINT NOT NULL,
    total_amount DECIMAL(10,2) NOT NULL,
    pay_amount DECIMAL(10,2) DEFAULT 0,
    order_status TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2已取消 3已退款',
    pay_time DATETIME NULL,
    cancel_time DATETIME NULL,
    expire_time DATETIME NULL,
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_order_no (order_no),
    KEY idx_user_id (user_id),
    KEY idx_order_status (order_status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 订单明细表
CREATE TABLE order_item (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_no VARCHAR(32) NOT NULL,
    product_id BIGINT NOT NULL,
    product_name VARCHAR(100) NOT NULL,
    product_price DECIMAL(10,2) NOT NULL,
    quantity INT NOT NULL,
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    KEY idx_order_no (order_no)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 支付记录表
CREATE TABLE pay_log (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_no VARCHAR(32) NOT NULL,
    pay_no VARCHAR(64) COMMENT '第三方支付流水号',
    pay_amount DECIMAL(10,2) NOT NULL,
    pay_status TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1成功 2失败',
    pay_type TINYINT DEFAULT 1 COMMENT '1微信 2支付宝',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    KEY idx_order_no (order_no)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 操作日志表
CREATE TABLE operation_log (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    content VARCHAR(255) NOT NULL,
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 测试数据
INSERT INTO product(product_name,price,stock,version) VALUES ('华为手机',3999.00,100,0);

二、pom.xml 完整依赖

xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.15</version>
        <relativePath/>
    </parent>
    <groupId>com.ecommerce</groupId>
    <artifactId>order-full-system</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!-- Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- JDBC -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <!-- MyBatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>
        <!-- Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- Redisson分布式锁 -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.15.0</version>
        </dependency>
        <!-- RabbitMQ -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <!-- 定时任务 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-task</artifactId>
        </dependency>
        <!-- AOP -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <!-- MySQL -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- 工具 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.83</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

三、application.yml 完整配置

yaml

server:
  port: 8080

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/shop_full?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowMultiQueries=true
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
  redis:
    host: localhost
    port: 6379
    password:
    database: 0
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /
  task:
    scheduling:
      core-size: 5

mybatis:
  mapper-locations: classpath:mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true

# 业务配置
order:
  expire-minute: 30
  token-expire-minute: 5

# Redis Key前缀
redis:
  key:
    order-token: "order:token:"
    product-stock: "product:stock:"
    product-info: "product:info:"

四、全局常量类

OrderConstant.java

java

运行

public class OrderConstant {
    // 订单状态
    public static final int ORDER_WAIT_PAY = 0;
    public static final int ORDER_PAY_SUCCESS = 1;
    public static final int ORDER_CANCEL = 2;
    public static final int ORDER_REFUND = 3;

    // 支付状态
    public static final int PAY_WAIT = 0;
    public static final int PAY_SUCCESS = 1;
    public static final int PAY_FAIL = 2;
}

MqConstant.java

java

运行

public class MqConstant {
    public static final String ORDER_NORMAL_QUEUE = "order.normal.queue";
    public static final String ORDER_NORMAL_EXCHANGE = "order.normal.exchange";
    public static final String ORDER_NORMAL_ROUTING_KEY = "order.normal.key";

    public static final String ORDER_DELAY_QUEUE = "order.delay.queue";
    public static final String ORDER_DELAY_EXCHANGE = "order.delay.exchange";
    public static final String ORDER_DELAY_ROUTING_KEY = "order.delay.key";
}

五、统一返回结果 Result.java

java

运行

import lombok.Data;

@Data
public class Result {
    private Integer code;
    private String msg;
    private Object data;

    public static Result success(Object data, String msg) {
        Result result = new Result();
        result.setCode(200);
        result.setMsg(msg);
        result.setData(data);
        return result;
    }

    public static Result fail(String msg) {
        Result result = new Result();
        result.setCode(500);
        result.setMsg(msg);
        return result;
    }
}

六、全部实体类

Product.java

java

运行

import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
public class Product {
    private Long id;
    private String productName;
    private BigDecimal price;
    private Integer stock;
    private Integer version;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

OrderInfo.java

java

运行

import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
public class OrderInfo {
    private Long id;
    private String orderNo;
    private Long userId;
    private BigDecimal totalAmount;
    private BigDecimal payAmount;
    private Integer orderStatus;
    private LocalDateTime payTime;
    private LocalDateTime cancelTime;
    private LocalDateTime expireTime;
    private LocalDateTime createTime;
}

OrderItem.java

java

运行

import lombok.Data;
import java.math.BigDecimal;

@Data
public class OrderItem {
    private Long id;
    private String orderNo;
    private Long productId;
    private String productName;
    private BigDecimal productPrice;
    private Integer quantity;
}

PayLog.java

java

运行

import lombok.Data;
import java.math.BigDecimal;

@Data
public class PayLog {
    private Long id;
    private String orderNo;
    private String payNo;
    private BigDecimal payAmount;
    private Integer payStatus;
    private Integer payType;
}

OperationLog.java

java

运行

import lombok.Data;
import java.time.LocalDateTime;

@Data
public class OperationLog {
    private Long id;
    private String content;
    private LocalDateTime createTime;
}

七、工具类 & 配置类

RedisUtil.java

java

运行

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.concurrent.TimeUnit;

@Service
public class RedisUtil {
    @Resource
    private RedisTemplate<String, String> stringRedisTemplate;

    public void set(String key, String value, long time, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, value, time, unit);
    }

    public String get(String key) {
        return stringRedisTemplate.opsForValue().get(key);
    }

    public Boolean delete(String key) {
        return stringRedisTemplate.delete(key);
    }

    public boolean checkAndDelete(String key) {
        String script = "if redis.call('get',KEYS[1]) then return redis.call('del',KEYS[1]) else return 0 end";
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
        Long result = stringRedisTemplate.execute(redisScript, Collections.singletonList(key));
        return result != null && result > 0;
    }
}

RedissonConfig.java

java

运行

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;

@Configuration
public class RedissonConfig {
    @Resource
    private RedisProperties redisProperties;

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        String address = "redis://" + redisProperties.getHost() + ":" + redisProperties.getPort();
        config.useSingleServer()
                .setAddress(address)
                .setPassword(redisProperties.getPassword())
                .setDatabase(redisProperties.getDatabase());
        return Redisson.create(config);
    }
}

RabbitConfig.java

java

运行

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 RabbitConfig {

    @Bean
    public Queue orderNormalQueue() {
        return new Queue(MqConstant.ORDER_NORMAL_QUEUE, true);
    }

    @Bean
    public DirectExchange orderNormalExchange() {
        return new DirectExchange(MqConstant.ORDER_NORMAL_EXCHANGE);
    }

    @Bean
    public Binding orderNormalBinding() {
        return BindingBuilder.bind(orderNormalQueue())
                .to(orderNormalExchange())
                .with(MqConstant.ORDER_NORMAL_ROUTING_KEY);
    }

    @Bean
    public Queue orderDelayQueue() {
        Map<String, Object> args = new HashMap<>();
        args.put("x-dead-letter-exchange", MqConstant.ORDER_NORMAL_EXCHANGE);
        args.put("x-dead-letter-routing-key", MqConstant.ORDER_NORMAL_ROUTING_KEY);
        return new Queue(MqConstant.ORDER_DELAY_QUEUE, true, false, false, args);
    }

    @Bean
    public DirectExchange orderDelayExchange() {
        return new DirectExchange(MqConstant.ORDER_DELAY_EXCHANGE);
    }

    @Bean
    public Binding orderDelayBinding() {
        return BindingBuilder.bind(orderDelayQueue())
                .to(orderDelayExchange())
                .with(MqConstant.ORDER_DELAY_ROUTING_KEY);
    }
}

八、Mapper 接口

ProductMapper.java

java

运行

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface ProductMapper {
    Product selectById(Long id);
    int deductStock(@Param("id") Long id, @Param("num") Integer num, @Param("version") Integer version);
    int addStock(@Param("id") Long id, @Param("num") Integer num);
}

OrderMapper.java

java

运行

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDateTime;
import java.util.List;

@Mapper
public interface OrderMapper {
    void insert(OrderInfo orderInfo);
    OrderInfo selectByOrderNo(String orderNo);
    int updateOrderStatus(@Param("orderNo") String orderNo,
                          @Param("status") Integer status,
                          @Param("oldStatus") Integer oldStatus,
                          @Param("payTime") LocalDateTime payTime);
    int updateOrderCancel(@Param("orderNo") String orderNo, @Param("cancelTime") LocalDateTime cancelTime);
    List<String> listExpireWaitPayOrder();
}

OrderItemMapper.java

java

运行

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface OrderItemMapper {
    void insert(OrderItem orderItem);
    OrderItem selectByOrderNo(String orderNo);
}

PayLogMapper.java

java

运行

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;

@Mapper
public interface PayLogMapper {
    void insert(PayLog payLog);
    void updatePaySuccess(@Param("orderNo") String orderNo,
                          @Param("payNo") String payNo,
                          @Param("payType") Integer payType,
                          @Param("payAmount") BigDecimal payAmount);
}

OperationLogMapper.java

java

运行

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface OperationLogMapper {
    void insert(OperationLog log);
}

九、Mapper XML 文件

ProductMapper.xml

xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ecommerce.mapper.ProductMapper">
    <select id="selectById" resultType="com.ecommerce.entity.Product">
        select * from product where id = #{id}
    </select>
    <update id="deductStock">
        UPDATE product
        SET stock = stock - #{num}, version = version + 1
        WHERE id = #{id} AND version = #{version} AND stock >= #{num}
    </update>
    <update id="addStock">
        UPDATE product SET stock = stock + #{num} WHERE id = #{id}
    </update>
</mapper>

OrderMapper.xml

xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ecommerce.mapper.OrderMapper">
    <insert id="insert">
        INSERT INTO order_info(order_no,user_id,total_amount,order_status,expire_time)
        VALUES(#{orderNo},#{userId},#{totalAmount},#{orderStatus},#{expireTime})
    </insert>
    <select id="selectByOrderNo" resultType="com.ecommerce.entity.OrderInfo">
        select * from order_info where order_no = #{orderNo}
    </select>
    <update id="updateOrderStatus">
        UPDATE order_info SET order_status=#{status},pay_time=#{payTime}
        WHERE order_no=#{orderNo} AND order_status=#{oldStatus}
    </update>
    <update id="updateOrderCancel">
        UPDATE order_info SET order_status=2,cancel_time=#{cancelTime} WHERE order_no=#{orderNo}
    </update>
    <select id="listExpireWaitPayOrder" resultType="java.lang.String">
        SELECT order_no FROM order_info
        WHERE order_status = 0 AND expire_time < NOW() LIMIT 100
    </select>
</mapper>

OrderItemMapper.xml

xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ecommerce.mapper.OrderItemMapper">
    <insert id="insert">
        INSERT INTO order_item(order_no,product_id,product_name,product_price,quantity)
        VALUES(#{orderNo},#{productId},#{productName},#{productPrice},#{quantity})
    </insert>
    <select id="selectByOrderNo" resultType="com.ecommerce.entity.OrderItem">
        select * from order_item where order_no = #{orderNo}
    </select>
</mapper>

PayLogMapper.xml

xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ecommerce.mapper.PayLogMapper">
    <insert id="insert">
        INSERT INTO pay_log(order_no,pay_amount,pay_status)
        VALUES(#{orderNo},#{payAmount},#{payStatus})
    </insert>
    <update id="updatePaySuccess">
        UPDATE pay_log SET pay_no=#{payNo},pay_type=#{payType},pay_status=1 WHERE order_no=#{orderNo}
    </update>
</mapper>

OperationLogMapper.xml

xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ecommerce.mapper.OperationLogMapper">
    <insert id="insert">
        INSERT INTO operation_log(content) VALUES(#{content})
    </insert>
</mapper>

十、Service 层全量代码

OperationLogService.java

java

运行

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;

@Service
public class OperationLogService {
    @Resource
    private OperationLogMapper operationLogMapper;

    @Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
    public void recordLog(String content) {
        OperationLog log = new OperationLog();
        log.setContent(content);
        operationLogMapper.insert(log);
    }
}

OrderMqProducer.java

java

运行

import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;

@Service
public class OrderMqProducer {
    @Resource
    private RabbitTemplate rabbitTemplate;

    public void sendNormalMsg(String orderNo) {
        rabbitTemplate.convertAndSend(MqConstant.ORDER_NORMAL_EXCHANGE,
                MqConstant.ORDER_NORMAL_ROUTING_KEY,orderNo);
    }

    public void sendDelayOrderMsg(String orderNo, long delayMinute) {
        long delay = delayMinute * 60 * 1000;
        rabbitTemplate.convertAndSend(MqConstant.ORDER_DELAY_EXCHANGE,
                MqConstant.ORDER_DELAY_ROUTING_KEY, orderNo, msg -> {
                    msg.getMessageProperties().setExpiration(String.valueOf(delay));
                    return msg;
                });
    }
}

OrderMqConsumer.java

java

运行

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;

@Slf4j
@Service
public class OrderMqConsumer {
    @Resource
    private OrderService orderService;

    @RabbitListener(queues = MqConstant.ORDER_NORMAL_QUEUE)
    public void consume(String orderNo){
        try {
            orderService.autoCloseOrder(orderNo);
        }catch (Exception e){
            log.error("MQ关闭订单异常:{}",e.getMessage());
        }
    }
}

OrderService.java【核心】

java

运行

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
public class OrderService {
    @Resource
    private ProductMapper productMapper;
    @Resource
    private OrderMapper orderMapper;
    @Resource
    private OrderItemMapper orderItemMapper;
    @Resource
    private PayLogMapper payLogMapper;
    @Resource
    private OperationLogService operationLogService;
    @Resource
    private RedisUtil redisUtil;
    @Resource
    private RedissonClient redissonClient;
    @Resource
    private OrderMqProducer orderMqProducer;

    @Value("${order.expire-minute}")
    private Integer expireMinute;
    @Value("${redis.key.order-token}")
    private String orderTokenPrefix;
    @Value("${redis.key.product-info}")
    private String productInfoPrefix;
    @Value("${redis.key.product-stock}")
    private String productStockPrefix;

    public String createOrder(Long userId, Long productId, Integer quantity, String orderToken) {
        // 1. 防重令牌校验
        String tokenKey = orderTokenPrefix + userId + ":" + orderToken;
        if(!redisUtil.checkAndDelete(tokenKey)){
            throw new RuntimeException("请勿重复提交");
        }
        // 2. 商品分布式锁
        String lockKey = "lock:product:" + productId;
        RLock lock = redissonClient.getLock(lockKey);
        boolean lockOk = false;
        try {
            lockOk = lock.tryLock(0,30, TimeUnit.SECONDS);
            if(!lockOk){
                throw new RuntimeException("下单拥挤,请稍后重试");
            }
            // 3. Redis缓存查询
            String infoKey = productInfoPrefix + productId;
            String stockKey = productStockPrefix + productId;
            Product product;
            String stockStr = redisUtil.get(stockKey);
            if(StringUtils.hasText(stockStr)){
                product = JSON.parseObject(redisUtil.get(infoKey),Product.class);
                if(Integer.parseInt(stockStr) < quantity){
                    throw new RuntimeException("库存不足");
                }
            }else{
                product = productMapper.selectById(productId);
                if(product == null){
                    throw new RuntimeException("商品不存在");
                }
                redisUtil.set(infoKey,JSON.toJSONString(product),1,TimeUnit.HOURS);
                redisUtil.set(stockKey,product.getStock().toString(),1,TimeUnit.HOURS);
            }
            if(product.getStock() < quantity){
                throw new RuntimeException("库存不足");
            }
            // 4. 短事务落库
            return doCreateOrder(productId,quantity,userId,product);
        }catch (Exception e){
            throw new RuntimeException(e.getMessage());
        }finally {
            if(lockOk && lock.isHeldByCurrentThread()){
                lock.unlock();
            }
        }
    }

    @Transactional(rollbackFor = Exception.class,isolation = Isolation.READ_COMMITTED)
    public String doCreateOrder(Long productId, Integer quantity, Long userId, Product product){
        // 乐观锁扣库存
        int rows = productMapper.deductStock(productId,quantity,product.getVersion());
        if(rows <= 0){
            throw new RuntimeException("并发冲突,下单失败");
        }
        // 缓存库存扣减
        redisUtil.getOperations().increment(productStockPrefix+productId,-quantity);
        // 生成订单
        String orderNo = "ORD" + System.currentTimeMillis() + UUID.randomUUID().toString().substring(0,6);
        LocalDateTime expireTime = LocalDateTime.now().plusMinutes(expireMinute);

        OrderInfo order = new OrderInfo();
        order.setOrderNo(orderNo);
        order.setUserId(userId);
        order.setTotalAmount(product.getPrice().multiply(new BigDecimal(quantity)));
        order.setOrderStatus(OrderConstant.ORDER_WAIT_PAY);
        order.setExpireTime(expireTime);
        orderMapper.insert(order);

        OrderItem item = new OrderItem();
        item.setOrderNo(orderNo);
        item.setProductId(productId);
        item.setProductName(product.getProductName());
        item.setProductPrice(product.getPrice());
        item.setQuantity(quantity);
        orderItemMapper.insert(item);

        PayLog payLog = new PayLog();
        payLog.setOrderNo(orderNo);
        payLog.setPayAmount(order.getTotalAmount());
        payLog.setPayStatus(OrderConstant.PAY_WAIT);
        payLogMapper.insert(payLog);

        // 发送延时关单MQ
        orderMqProducer.sendDelayOrderMsg(orderNo,expireMinute);
        orderMqProducer.sendNormalMsg(orderNo);
        operationLogService.recordLog("用户"+userId+"创建订单:"+orderNo);
        return orderNo;
    }

    @Transactional(rollbackFor = Exception.class)
    public void payOrder(String orderNo,String payNo,Integer payType){
        OrderInfo order = orderMapper.selectByOrderNo(orderNo);
        if(order == null || !order.getOrderStatus().equals(OrderConstant.ORDER_WAIT_PAY)){
            throw new RuntimeException("订单状态异常");
        }
        orderMapper.updateOrderStatus(orderNo,OrderConstant.ORDER_PAY_SUCCESS,
                OrderConstant.ORDER_WAIT_PAY,LocalDateTime.now());
        payLogMapper.updatePaySuccess(orderNo,payNo,payType,order.getTotalAmount());
        operationLogService.recordLog("订单"+orderNo+"支付成功");
    }

    @Transactional(rollbackFor = Exception.class)
    public void cancelOrder(String orderNo){
        OrderInfo order = orderMapper.selectByOrderNo(orderNo);
        if(order == null || !order.getOrderStatus().equals(OrderConstant.ORDER_WAIT_PAY)){
            throw new RuntimeException("无法取消");
        }
        OrderItem item = orderItemMapper.selectByOrderNo(orderNo);
        productMapper.addStock(item.getProductId(),item.getQuantity());
        orderMapper.updateOrderCancel(orderNo,LocalDateTime.now());
        operationLogService.recordLog("订单"+orderNo+"手动取消");
    }

    @Transactional(rollbackFor = Exception.class)
    public void autoCloseOrder(String orderNo){
        OrderInfo order = orderMapper.selectByOrderNo(orderNo);
        if(order == null || !order.getOrderStatus().equals(OrderConstant.ORDER_WAIT_PAY)){
            return;
        }
        OrderItem item = orderItemMapper.selectByOrderNo(orderNo);
        productMapper.addStock(item.getProductId(),item.getQuantity());
        orderMapper.updateOrderCancel(orderNo,LocalDateTime.now());
        operationLogService.recordLog("订单"+orderNo+"超时自动关闭");
    }
}

十一、Controller 全量接口

OrderTokenController.java

java

运行

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("/api/token")
public class OrderTokenController {
    @Resource
    private RedisUtil redisUtil;
    @Value("${redis.key.order-token}")
    private String orderTokenPrefix;
    @Value("${order.token-expire-minute}")
    private Integer tokenExpireMinute;

    @GetMapping("/get")
    public Result getToken(@RequestParam Long userId){
        String token = UUID.randomUUID().toString().replace("-","");
        String key = orderTokenPrefix + userId + ":" + token;
        redisUtil.set(key,"1",tokenExpireMinute, TimeUnit.MINUTES);
        return Result.success(token,"令牌获取成功");
    }
}

OrderController.java

java

运行

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;

@RestController
@RequestMapping("/api/order")
public class OrderController {
    @Resource
    private OrderService orderService;

    @PostMapping("/create")
    public Result create(@RequestParam Long userId,
                         @RequestParam Long productId,
                         @RequestParam Integer quantity,
                         @RequestParam String orderToken){
        try {
            String orderNo = orderService.createOrder(userId,productId,quantity,orderToken);
            return Result.success(orderNo,"下单成功");
        }catch (Exception e){
            return Result.fail(e.getMessage());
        }
    }

    @PostMapping("/pay")
    public Result pay(@RequestParam String orderNo,
                      @RequestParam String payNo,
                      @RequestParam Integer payType){
        try {
            orderService.payOrder(orderNo,payNo,payType);
            return Result.success(null,"支付成功");
        }catch (Exception e){
            return Result.fail(e.getMessage());
        }
    }

    @PostMapping("/cancel")
    public Result cancel(@RequestParam String orderNo){
        try {
            orderService.cancelOrder(orderNo);
            return Result.success(null,"取消成功");
        }catch (Exception e){
            return Result.fail(e.getMessage());
        }
    }
}

十二、启动类

java

运行

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class,args);
    }
}

十三、测试流程

  1. 启动 MySQL、Redis、RabbitMQ
  2. 执行上方 SQL 建表 + 初始化商品
  3. 启动项目
  4. 接口调用顺序:

    plaintext

    1. 获取下单令牌:GET /api/token/get?userId=1
    2. 创建订单:POST /api/order/create?userId=1&productId=1&quantity=1&orderToken=xxx
    3. 模拟支付:POST /api/order/pay?orderNo=xxx&payNo=PAY666&payType=2
    4. 手动取消:POST /api/order/cancel?orderNo=xxx
    

十四、核心面试总结(可直接背)

  1. 防重:Redis+Lua 令牌防重复提交
  2. 防超卖:Redisson 商品级分布式锁 + MySQL 乐观锁
  3. 高性能:Redis 热点缓存、短事务、异步 MQ 解耦
  4. 流量削峰:RabbitMQ 延时队列替代定时任务轮库
  5. 数据一致:下单扣库存、取消 / 超时自动回补库存
  6. 事务设计:核心 DB 操作为短事务,日志独立事务不回滚
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值