最近有很多运营同事来找我说:我们的数据校对系统越来越慢了,要显示校对结果需要很长时间。你能快速优化它吗?我:好的,我先了解一下业务,以后再优化。优化背景由于这个数据校对系统最初不是我开发的,所以我了解了数据校对系统的业务。总体来说,数据校对系统的业务比较简单。用户通过商城提交订单后,会在订单微服务中生成订单信息,存储在订单数据库中。订单微服务会调用库存微服务的接口,扣除商品的库存数量,并将每次订单扣除库存的记录保存在库存数据库中。为防止用户提交订单后不扣减库存,或重复扣减库存,数据校对系统会每天检查订单提交的商品数量是否与扣减的库存数量一致,并将将校对结果信息保存到数据校对信息表中。数据校对系统的总体流程是:先查询订单记录,再查询库存扣减记录,然后对比订单和库存扣减记录,最后将校对结果信息保存到数据校对信息表中。整体流程如下。为了让大家更好的理解订单和库存数据校对系统的校对业务,我对代码进行了精简,核心业务逻辑代码如下。//检测是否有未勾选的订单checkOrders=checkOrders();while(checkOrders!=null){//查询未查询的订单信息hasNoOrders=getHasNoOrders();//查询未查询的库存记录hasNoStock=getHasNoStock();//查询数据并返回结果checkResult=checkData(hasNoOrders,hasNoStock);//将结果信息保存到数据校对信息表中saveCheckResult(checkResult);//检测是否有未对帐的订单checkOrders=checkOrders();}好了,以上就是系统优化的背景。看到这里肯定有很多朋友应该知道问题出在哪里了。让我们继续阅读。问题分析虽然很多小伙伴应该已经知道系统性能低下的问题,但是在这里,我们就来详细分析校对系统性能低下的原因。既然运营的同事都说数据校对系统越来越慢,那么我们首先要做的就是找到系统的性能瓶颈。据了解,在目前的数据对账系统中,由于订单记录和库存扣减记录的数据量巨大,查询未勾选订单信息的方法getHasNoOrders()与查询库存记录的方法getHasNoStock()具有相对可比性:正在校对。慢的。而在数据校对系统中,校对订单和库存记录的方法是在单线程中执行的,我们可以简单的画一个时序图,如下图。从图中可以看出,getHasNoOrders()方法和getHasNoStock()方法在单线程的情况下消耗了大量的时间。这两个方法本身在逻辑上是两个独立的方法,这两个方法并不是按顺序执行的。顺序依赖。这两个方法可以并行执行吗?显然是的。然后我们将getHasNoOrders()方法和getHasNoStock()方法放在两个不同的线程中,以优化系统的性能。整体流程如下。优化后,我们将getHasNoOrders()方法放在线程1执行,getHasNoStock()方法放在线程2执行,checkData()方法和saveCheckResult()方法放在线程3执行。与优化后的系统性能相比优化前的系统性能几乎翻了一番,优化效果比较明显。说到这里,大家应该知道怎么优化了吧?好了,我们继续往下看!我们有了解决问题的想法。接下来我们看看如何使用代码来实现解决我们上面分析的问题的思路。这里,我们可以开两个线程执行getHasNoOrders()方法和getHasNoStock()方法,在主线程中执行checkData()方法和saveCheckResult()方法。这里需要注意的是,主线程需要等待两个子线程执行完毕,才能执行checkData()方法和saveCheckResult()方法。为了实现这个功能,我们可以使用Thread类中的join()方法。Thread类中join()方法的具体描述,这里,具体逻辑是在主线程中调用两个子线程的join()方法实现阻塞等待,当两个子线程执行完毕并退出,调用两个子线程的join()方法的主线程会被唤醒,从而在主线程中执行checkData()方法和saveCheckResult()方法。一般代码如下。//检测是否有未勾选订单checkOrders=checkOrders();while(checkOrders!=null){Threadt1=newThread(()->{//查询未勾选订单信息hasNoOrders=getHasNoOrders();});t1.start();Threadt2=newThread(()->{//查询未查询的库存记录hasNoStock=getHasNoStock();});t2.start();//阻塞主线程,等待线程t1和线程t2执行完毕t1.join();t2.join();//校验数据并返回结果checkResult=checkData(hasNoOrders,hasNoStock);//将结果信息保存到数据校对信息表中saveCheckResult(checkResult);//校验是否有未对帐的订单checkOrders=checkOrders();}至此,基本可以解决问题。但是,还有进一步优化的空间吗?我们再往下看。进一步优化通过上面的系统优化,基本可以达到我们的优化目标,但是上面的方案有不足之处,就是在while循环中,每次都要新建两个线程分别执行ge??tHasNoOrders()方法和getHasNoStock。()方法,了解Java多线程的朋友应该知道,在Java中创建线程是一个非常耗时的操作。因此,最好能够重复使用创建的线程。说到这里,估计很多朋友都会想到使用线程池。是的,我们可以使用线程池进一步优化上面的代码。遇到新问题但是在使用线程池做进一步优化的时候,我们会遇到一个问题,就是主线程如何在子线程中等待结果数据呢?说白了:主线程怎么知道getHasNoOrders()方法和getHasNoStock()方法执行了呢?因为在前面的代码中,我们在主线程中调用了子线程的join()方法,等待子线程执行完毕。得到子线程的执行结果后,继续执行主线程。逻辑。但是如果使用线程池,线程池中的线程根本不会退出。这个时候我们不能使用线程的join()方法来等待线程执行完毕。那么,主线程如何知道子线程中的getHasNoOrders()方法和getHasNoStock()方法已经执行完毕呢?这个问题成为了一个关键的突破点。在这里,我们使用线程池进一步优化的代码如下所示。//检测是否有未对账的订单checkOrders=checkOrders();//创建线程池Executorexecutor=Executors.newFixedThreadPool(2);while(checkOrders!=null){executor.execute(()->{//查询未勾选的订单信息hasNoOrders=getHasNoOrders();});executor.execute(()->{//查询未勾选的库存记录hasNoStock=getHasNoStock();});/**如何知道getHasNoOrders()方法和getHasNoStock()方法被执行成为key**///检查数据并返回结果checkResult=checkData(hasNoOrders,hasNoStock);//将结果信息保存到数据校验信息表中saveCheckResult(checkResult);//检查是否有未对帐的订单checkOrders=checkOrders();}那么,如何解决这个问题呢?我们继续往下看。新方案相信细心的朋友可以看出来,整个业务场景是:一个线程需要等待另外两个线程的逻辑完成后再执行。在Java的并发类库中,为我们提供了一个可以在这种场景下使用的类库,即CountDownLatch类。使用CountDownLatch类优化我们程序的具体方法是:首先在程序的while()循环中创建一个CountDownLatch对象,并将计数器的值初始化为2。在hasNoOrders=getHasNoOrders之后调用latch.countDown()方法();代码和hasNoStock=getHasNoStock();将计数器的值分别减1的代码。在主线程中调用latch.await()方法,等待计数器的值变为0,继续执行。这样就可以完美的解决我们遇到的问题了。优化后的代码如下所示。//检测是否有未对账的订单checkOrders=checkOrders();//创建线程池Executorexecutor=Executors.newFixedThreadPool(2);while(checkOrders!=null){CountDownLatchlatch=newCountDownLatch(2);executor.execute(()->{//查询未查询的订单信息hasNoOrders=getHasNoOrders();latch.countDown();});executor.execute(()->{//查询未查询的库存记录hasNoStock=getHasNoStock();latch.countDown();});//等待子线程逻辑执行完成latch.await();//校验数据并返回结果checkResult=checkData(hasNoOrders,hasNoStock);//将结果信息保存到数据中校对信息表中saveCheckResult(checkResult);//检查是否有未对帐的订单checkOrders=checkOrders();}至此,我们就完成了系统的优化。总结思考这次系统性能的优化,主要是将单线程执行的数据校对业务优化为多线程执行。在平时的工作过程中,我们需要深思熟虑,找出系统性能的瓶颈,找出逻辑上不相关、没有先后顺序的业务逻辑,放到不同的线程中执行,这样可以大大提高性能系统。这次为了系统的优化,我们最终使用线程池来执行查询订单和查询库存记录等耗时操作,在主线程中等待线程池中的线程逻辑执行完毕后再执行主线程逻辑的后续业务。在这种场景下,使用Java提供的CountDownLatch类就完美了。这里再次强调一下:CountDownLatch的主要使用场景是一个线程等待多个线程执行完毕再执行。如下所示。在此,进一步提醒我们:要想学好并发编程,必须熟练掌握Java提供的并发类库。本文转载自微信公众号“冰河科技”,可通过以下二维码关注。转载本文请联系冰川科技公众号。
