电商——真实情况下的订单管理

本文介绍了一个基于SpringBoot的系统,如何在商品秒杀场景中处理高并发的订单支付请求,涉及库存锁定策略、FeignClient调用、事务管理和数据库操作,确保在并发环境下支付过程的正确性。
摘要由CSDN通过智能技术生成

前言

我们在使用软件进行订单支付时,系统的后台在面对高频率的支付请求时,尤其是在商品秒杀,所有商品几乎在一瞬间就被抢没时,后台是如何做到正确的完成全部的订单支付的呢?如何实现顶大中商品库存的正确扣除的呢?本篇文章将会创建项目来实现该需求。

订单的状态会经历一下阶段:
新建(待支付)->已支付->待配送->已送达
想要实现支付时的正确性时必须做到锁库存。
支付时具有以下步骤:
1.弹出输密码的框
2.输密码
3.已付款
那么在什么时候锁库存呢?
应该在弹出输密码的框时锁住库存,如果还有库存则前往输密码,否则告诉用户商品售罄
在支付时会调取不同的支付接口,比如微信,支付宝,银联支付等
订单在新建时前端会得到返回的订单编号,在真正支付时就会订单编号信息传递给支付接口

下面来具体实现该项目,该项目中的接口方法要求在高并发的情况下也能正确不会出现错误,实现真正情况下的订单支付功能。

基本配置

创建两个SpringBoot项目,并创建子项目,创建后结构如下
product商品项目
在这里插入图片描述
对应的配置文件:yml,放在product-service中

server:
  port: 9093
# Spring
spring:
  application:
    # 应用名称
    name: product-service
  profiles:
    # 环境配置
    active: dev
  cloud:
    nacos:
      username: nacos
      password: nacos
      discovery:
        # 服务注册地址
        server-addr: 
      config:
        # 配置中心地址
        server-addr: 
        # 配置文件格式
        file-extension: yml
        # 共享配置
        shared-configs:
          - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}

order订单项目
在这里插入图片描述

server:
  port: 9094
# Spring
spring:
  application:
    # 应用名称
    name: order-service
  profiles:
    # 环境配置
    active: dev
  cloud:
    nacos:
      username: nacos
      password: nacos
      discovery:
        # 服务注册地址
        server-addr: 127.0.0.1:8848
      config:
        # 配置中心地址
        server-addr: 127.0.0.1:8848
        # 配置文件格式
        file-extension: yml
        # 共享配置
        shared-configs:
          - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}

之后采用nacos统一配置
在本地运行nacos,打开nacos文件的bin目录,进入控制台,输入命令startup.cmd -m standalone
进入网页中,创建一个配置
在这里插入图片描述
由于order项目要用到product项目,以后需要调用其中的接口方法
因此要将product引入到order中。
首先将product依赖中的module注释掉,然后再Lifecycle中clean,install。
再将module注释回来,刷新依赖,再将子项目product-client重新clean,install

<modules>
    <module>product-client</module>
    <module>product-service</module>
</modules>

安装完成后,在product-client的依赖中找到

<artifactId>product-client</artifactId>
<version>0.0.1-SNAPSHOT</version>

将其复制,导入到order父项目中的依赖中

<dependency>
    <groupId>com.example</groupId>
    <artifactId>product-client</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

再在order-client中也导入该依赖,此时不用写版本号

<dependency>
    <groupId>com.example</groupId>
    <artifactId>product-client</artifactId>
</dependency>

调用测试

在product-client中创建测试接口并在product-service中编写controller实现接口:
结构如下:
在这里插入图片描述

@FeignClient(contextId = "remoteProductService",
        value = "product-service")
public interface RemoteProductService {
    @GetMapping("/hello")
    String hello();
}

@RestController
public class ProductController implements RemoteProductService {

    @Override
    @GetMapping("/hello")
    public String hello(){
        return "hello world";
    }
}

之后在order-service中创建controller,调用接口

@RestController
@EnableFeignClients(basePackages = {"com.example.**.feign"})//这里是接口包的位置
public class OrderController {
    @Resource
    private RemoteProductService remoteProductService;//引入了依赖所有可以创建并注入
    @GetMapping("/test")
    public String test(){
        return remoteProductService.hello();//调用product项目中的方法

    }
}

启动后访问端口9094/test,发现可以实现调用,测试没问题
在这里插入图片描述

编写功能

计算订单中商品的金额

在这里插入图片描述
根据数据库的product写出mapper,entity,service,
编写price实体来接收前端数据

@Data
public class Price {
    private Integer productId;
    private Integer count;
}

在feign文件夹下的用于远程在其他项目中调用的接口中编写price接口来实现功能

@PostMapping("price")
public CommonResult<Integer> price(@RequestBody List<Price> prices){
    return CommonResult.success(productService.money(prices));
}

该接口中的所有方法将在controller中product项目中的controller中实现

@PostMapping("price")
public CommonResult<Integer> price(@RequestBody List<Price> prices){
    return CommonResult.success(productService.money(prices));
}

该controller类中又会调用service层的接口,所以在service中编写接口money

Integer money(List<Price> price);

再实现接口

@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper,Product> implements IProductService {
    @Autowired
    private ProductMapper productMapper;
    @Override
    public Integer money(List<Price> pricelist) {
        int sum = 0;
        for(Price price:pricelist){
            Product product = productMapper.selectById(price.getProductId());
            if(product==null){
                continue;
            }else {
                sum+=product.getPrice()*price.getCount();
            }
        }
        return sum;
    }
}

因此该功能的整体调用逻辑为order项目中调用product项目中用于远程调用的接口,该接口会在product项目中的controller实现。
而controller中的方法又会调用service层中的接口,通过service中的接口,会调用该接口的实现类。
因此order中调用的接口方法将会在product项目中的service层的实现类中实现。

根据订单完成支付

整体逻辑为:当后端得到前端传递的多个订单时,同时锁库存,如果还有库存,再扣减库存,向订单表插入数据,并计算出总金额。

锁库存

要完成上述功能,我们先要实现锁库存的接口,由于上面已经将product项目引入到order中。因此我们现在product项目中编写锁库存的接口,并在order项目中调用即可。
此外还需要改变一下数据库表,在数据库的商品表中再添加一行锁库存的数量,代表当前已经锁住的库存数,当锁住的数量等于剩余的数量时即代表该商品已经卖完。
锁库存说明:该方法的返回值为布尔类型,入参为订单中商品的id和数量,如果该商品处理完后还有库存则返回True并向订单表插入数据,否则返回false。
在被其他项目中调用的接口中编写锁库存接口:

@PostMapping("/lock-stock")
CommonResult<Boolean> lockStock(@RequestBody  Price price);

该接口中的所有方法将在controller中product项目中的controller中实现

@PostMapping("/lock-stock")
public CommonResult<Boolean> lockStock(@RequestBody  Price price) {
    return CommonResult.success(productService.lockStock(price));
}

再编写service中的接口和方法

public interface IProductService extends IService<Product> {
    Integer money(List<Price> price);
    Boolean lockStock(Price price);
}

该方法在具体实现时会使用数据库行锁的方式实现锁库存,该操纵也是原子的。
先会根据传来的商品id获取到该商品已经锁住的库存量。如果小于库存量则还要库存,并扣减库存并返回true。

/**
 * 用数据库行锁的方式实现锁库存
 * @param price
 * @return
 */
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean lockStock(Price price) {
    LambdaQueryWrapper<Product> queryWrapper = new LambdaQueryWrapper<Product>()
            .eq(Product::getId,price.getProductId())
            .last("for update");
    Product product = this.baseMapper.selectOne(queryWrapper);
    if(product.getLockStock()+price.getCount()<=product.getStock()){
        this.baseMapper.updateLockStock(price.getProductId(),price.getCount());
        return true;
    }
    return null;
}

完成订单交易

在order项目中的controller中编写接口来完成功能

@PostMapping("/order")
public CommonResult<OrderResultVO> order(@RequestBody List<OrderParam> orderParam){
    return CommonResult.success(orderService.order(orderParam));
}

在service中编写order接口并实现

public interface IOrderService extends IService<OrderEntity> {
    OrderResultVO order(List<OrderParam> orderParams);
}

该实现类中的逻辑较为复杂,前端会传来一个订单列表,先将这些订单一次进行锁库存,创建一个lock列表,用于存放被锁住的库存。
如果最后该列表为空了,则证明已经没有库存了。否则是有库存,再远程调用之前写的price方法计算出订单需要的金额。
然后将得到的信息插入到订单表中,记录下本次的订单。
最后构建返回体,将数据封装返回。

@Override
public OrderResultVO order(List<OrderParam> orderParams) {
    //存放要锁住的库存
    List<OrderParam> lock = new ArrayList<>();
    //将前端得到的订单集合一次进行锁库存
    for(OrderParam param:orderParams){
        Price price = new Price();
        price.setProductId(param.getProductId());
        price.setCount(param.getCount());
        //判断如果库存扣减后是否还有库存
        Boolean tryLock = productService.lockStock(price).getData();
        //如果还有库存,则将该订单要需要的库存锁住,证明该订单可以被完成
        if(tryLock){
            lock.add(param);
        }
    }
    //如果没有库存被锁,则证明已经没有库存
    if(lock.size()==0){
        throw new RuntimeException("所有商品已售完");
    }

    OrderEntity orderEntity = new OrderEntity();
    orderEntity.setOrderNo(UUID.randomUUID().toString());
    orderEntity.setStatus(OrderStatusEnum.PENDING_PAYMENT.getStatus());
    //构造rpc入参,远程调用
    List<Price> param = new ArrayList<>();
    for(OrderParam orderParam : orderParams){
        Price price = new Price();
        price.setCount(orderParam.getCount());
        price.setProductId(orderParam.getProductId());
        param.add(price);
    }

    Integer money = productService.price(param).getData();
    orderEntity.setAmount(money);
    orderEntity.setPayAmount(money);
    //插入订单表
    this.baseMapper.insert(orderEntity);

    //todo订单关联表
    List<OrderProductEntity> orderProductEntityList =new ArrayList<>();
    for(OrderParam orderParam : lock){
        OrderProductEntity orderProductEntity = new OrderProductEntity();
        orderProductEntity.setOrderId(orderEntity.getId());
        orderProductEntity.setProductId(orderParam.getProductId());
        orderProductEntity.setNum(orderParam.getCount());
        orderProductEntityList.add(orderProductEntity);
    }
   boolean flag= orderProductService.saveBatch(orderProductEntityList);
    System.out.println(flag);

    //构造返回体
    OrderResultVO orderResultVO = new OrderResultVO();
    orderResultVO.setOrderId(orderEntity.getId());
    orderResultVO.setPayMoney(orderEntity.getAmount());
    return orderResultVO;
}

至此该功能实现完毕,由于实现时的原子性操作,所以该接口在高并发频繁调用的情况下也不会出错。

  • 24
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值