在前面的文章中,我们针对下单,支付的代码进行了部分优化,但是仍然存在好多问题,比如说使用循环的方式解决异常重试的问题,实际是不该这么做的。在上一篇当中引入了skywalking
帮助我们查看、分析系统的一些问题,本章我们看看针对出现的问题做一些优化。
一、引入队列
为什么要引入队列,因为经过前面的两篇文章的测试,我发现即使似乎500并发,当我的电脑cpu资源紧张的时候仍然后导致接口超时,最终导致不是订单少了,就是用户支付失败,这肯定是不能忍受的,解决的办法很简单,就是增加系统的吞吐量
,而队列就是很好地实现方式
。
1.1 下单功能改造
这里的队列不是消息队列,我们目前先试用jdk提供的队列去做优化。这里为什么要使用队列呢?前面我们使用try、catch,再次调用接口请求的形式,肯定不是一个好的实现方式,这属于出了问题再去打补丁的方式。我们应该在问题出现前就将它规避掉。
如下代码使用一个LinkedBlockingQueue去存储请求,使用一个线程去轮询消费队列中的数据:
/**
* 队列
*/
private static final ConcurrentLinkedQueue<OrderDTO> queue = new ConcurrentLinkedQueue<>();
/**
* 线程存活状态,用于查看当前是否有线程运行
*/
private volatile static Boolean threadState = false;
@Override
public Result saveOrder(OrderDTO orderDTO) {
queue.offer(orderDTO);
if (!threadState) {
threadState = true;
new Thread(() -> {
while (!queue.isEmpty()) {
// 下单实现
Result result = this.saveOrderImpl(queue.poll());
CompletableFuture.runAsync(() ->
HttpUtil.get("http://localhost:8010/test/orderCallBack?orderId="
+ JSONObject.parseObject(JSONObject.toJSONString(result.getData())).getLong("id")
)
);
}
threadState = false;
}).start();
}
return Result.success("下单成功");
}
/**
* description: 下单具体实现
* @param orderDTO
* @return: com.wjbgn.stater.dto.Result
* @author: weirx
* @time: 2022/2/21 16:31
*/
public Result saveOrderImpl(OrderDTO orderDTO) {
GoodsDTO goodsDTO = new GoodsDTO(orderDTO.getGoodsId(), orderDTO.getGoodsNum());
// 扣减库存
Result result = goodsClient.inventoryDeducting(goodsDTO);
if (result.getCode() != CommonReturnEnum.SUCCESS.getCode()) {
return Result.failed("扣除商品库存失败");
}
goodsDTO = JSONObject.toJavaObject(JSONObject.parseObject(JSONObject.toJSONString(result.getData())), GoodsDTO.class);
//计算订单总金额
double amount = goodsDTO.getGoodsPrice() * orderDTO.getGoodsNum();
orderDTO.setOrderAmount(amount);
orderDTO.setCreateUser(1L);
orderDTO.setOrderStatus(OrderStatusEnum.NO_PAY);
OrderDO orderDO = OrderDoConvert.dtoToDo(orderDTO);
// 保存订单主表
boolean save = this.save(orderDO);
if (!save) {
return Result.failed("生成主订单失败");
}
// 处理订单详情数据
OrderDetailDO orderDetailDO = new OrderDetailDO();
orderDetailDO.setCreateUser(1L);
orderDetailDO.setOrderId(orderDO.getId());
orderDetailDO.setGoodsId(orderDTO.getGoodsId());
orderDetailDO.setGoodsNum(orderDTO.getGoodsNum());
orderDetailDO.setGoodsUnitPrice(goodsDTO.getGoodsPrice());
// 保存订单详情
boolean detail = orderDetailService.save(orderDetailDO);
if (!detail) {
return Result.failed("订单详情保存失败");
}
return Result.success(orderDO);
}
但是此处存在一些问题,现在的下单变成了异步,那么在发送下单请求的服务test是无法获取到订单完成后的订单id,也就无法继续支付,所以我们在test提供一个回调方法,让我们这边处理完订单后,去回调这个接口,通知它去支付。
提供一个回调接口:
@GetMapping("/orderCallBack")
public void orderCallBack(@RequestParam Long orderId) {
tradingService.pay(orderId);
log.info("完成时间:{},订单id: {}", LocalDateTime.now(), orderId);
}
而且上面的方式其实使用的单线程轮询队列,这就使得我们的过程变成了一个单线程的处理程序,大量的请求堆积在队列中,会对内存的占用多一些。
总结:当服务器压力较大时,我们可以采取队列的方式,将请求缓存在队列当中,然后去轮询队列进行处理。需要注意的是确保jvm的堆是足够的。此方式变成异步,需要回调方式通知。
1.2 支付功能改造
下单功能修改成队列后,实际对于支付功能的并发比以前小了很多了,因为是通过回调方式进行支付的,所以此处我们可以删掉之前做的不好的循环异常处理,如下:
@Override
public Result trading(TradingDO tradingDO) {
OrderDTO orderDTO = this.updateOrder(tradingDO);
if (ObjectUtil.isNull(orderDTO)) {
log.info("支付失败,修改订单状态失败,订单id :{}", tradingDO.getOrderId());
return Result.failed("支付失败,修改订单状态失败");
}
// 扣减用户账户金额
Result result = userAccountClient.accountDeducting(new UserAccountDTO(orderDTO.getUserId(), orderDTO.getOrderAmount()));
if (result.getCode() != CommonReturnEnum.SUCCESS.getCode()) {
log.info("用户账户扣款失败,订单id :{}", tradingDO.getOrderId());
return Result.failed("用户账户扣款失败");
}
// 生成支付订单
tradingDO.setCreateUser(1L);
tradingDO.setTradingAmount(orderDTO.getOrderAmount());
tradingDO.setTradingStatus(TradingStatusEnum.SUCCESS);
tradingDO.setUserId(orderDTO.getUserId());
boolean save = this.save(tradingDO);
if (!save) {
//修改用户订单状态 - 支付失败
orderClient.update(new OrderDTO(tradingDO.getOrderId(), OrderStatusEnum.PAY_FAILED.toString()));
//TODO 回滚用户的账户金额
} else {
//修改用户订单状态 - 订单完成
orderClient.update(new OrderDTO(tradingDO.getOrderId(), OrderStatusEnum.FINNISH.toString()));
}
return Result.success("支付成功");
}
/**
* description: 验证并更新订单状态
* @param tradingDO
* @return: com.wjbgn.trading.client.dto.OrderDTO
* @author: weirx
* @time: 2022/2/21 16:34
*/
OrderDTO updateOrder(TradingDO tradingDO) {
// 获取订单信息
Result info = orderClient.info(tradingDO.getOrderId());
if (ObjectUtil.isEmpty(info.getData())) {
return null;
}
OrderDTO orderDTO = JSONObject.parseObject(JSONObject.toJSONString(info.getData())).toJavaObject(OrderDTO.class);
//已完成订单不能再次支付
if (orderDTO.getOrderStatus().equals(OrderStatusEnum.FINNISH.getCode())) {
return null;
}
//修改用户订单状态 - 支付中
Result update = orderClient.update(new OrderDTO(tradingDO.getOrderId(), OrderStatusEnum.PAYING.toString()));
if (update.getCode() != CommonReturnEnum.SUCCESS.getCode()) {
return null;
}
return orderDTO;
}
1.3 测试修改后效果
2022-02-21 16:25:24 INFO Thread-2062 com.wjbgn.test.controller.TestController 下单开始时间+++++++++++++++++++++++:2022-02-21T16:25:24.370
2022-02-21 16:25:24 INFO Thread-1565 com.wjbgn.test.controller.TestController 下单开始时间+++++++++++++++++++++++:2022-02-21T16:25:24.370
.... ....
2022-02-21 16:25:36 INFO http-nio-8010-exec-7 com.wjbgn.test.controller.TestController 支付完成时间/:2022-02-21T16:25:36.219,订单id: 499
2022-02-21 16:25:36 INFO http-nio-8010-exec-9 com.wjbgn.test.controller.TestController 支付完成时间/:2022-02-21T16:25:36.219,订单id: 500
如上所示,时间大概在12
秒左右。比前面的文章当中略慢一些,但是目前的订单支付非常稳定,不会出现超时的异常,并且500
并发绝对不是我们的极限,我们可以继续增加,取决于堆内存的大小了。
针对交易服务不做修改,那么我们看看最终请求的耗时大概是11秒
左右,整体比上次慢了一些,但是500个并发的效果非常稳定,再也没出现过并发导致连接超时的问题。
增加测试力度,使用1000个并发,我们看下结果:
时间+++++++++++++++++++++++:2022-02-21T16:44:17.787
2022-02-21 16:44:17 INFO Thread-56 com.wjbgn.test.controller.TestController 下单开始时间+++++++++++++++++++++++:2022-02-21T16:44:17.787
.... ....
2022-02-21 16:44:41 INFO http-nio-8010-exec-9 com.wjbgn.test.controller.TestController 支付完成时间/:2022-02-21T16:44:41.261,订单id: 999
2022-02-21 16:44:41 INFO http-nio-8010-exec-6 com.wjbgn.test.controller.TestController 支付完成时间/:2022-02-21T16:44:41.303,订单id: 1000
如上所示,大概是24秒左右,但是没有任何一个订单出现异常情况。这对比我们一开始使用ReentrantLock
锁住整个支付和下单方法,无论是在效率还是稳定性上面都已经有了质的飞越了。