前言
我们在使用软件进行订单支付时,系统的后台在面对高频率的支付请求时,尤其是在商品秒杀,所有商品几乎在一瞬间就被抢没时,后台是如何做到正确的完成全部的订单支付的呢?如何实现顶大中商品库存的正确扣除的呢?本篇文章将会创建项目来实现该需求。
订单的状态会经历一下阶段:
新建(待支付)->已支付->待配送->已送达
想要实现支付时的正确性时必须做到锁库存。
支付时具有以下步骤:
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;
}
至此该功能实现完毕,由于实现时的原子性操作,所以该接口在高并发频繁调用的情况下也不会出错。