使用队列增加系统的吞吐量提升稳定性

「这是我参与2022首次更文挑战的第35天,活动详情查看:2022首次更文挑战

欢迎阅读本专题内的文章,专题地址:juejin.cn/column/7053…

本文项目代码gitee地址: gitee.com/wei_rong_xi…

在前面的文章中,我们针对下单,支付的代码进行了部分优化,但是仍然存在好多问题,比如说使用循环的方式解决异常重试的问题,实际是不该这么做的。在上一篇当中引入了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-21 16:44:17 INFO  Thread-62 com.wjbgn.test.controller.TestController 下单开始时间+++++++++++++++++++++++: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锁住整个支付和下单方法,无论是在效率还是稳定性上面都已经有了质的飞越了。


欢迎阅读本专题内的文章,专题地址:juejin.cn/column/7053…

本文项目代码gitee地址: gitee.com/wei_rong_xi…

猜你喜欢

转载自juejin.im/post/7067333675599265828