性能提升了200%?。▋?yōu)化篇)
最近不少運(yùn)營(yíng)同事找到我說:咱們的數(shù)據(jù)校對(duì)系統(tǒng)越來越慢了,要過很久才會(huì)顯示出校對(duì)結(jié)果,你能不能快速優(yōu)化一下呢?我:好的,我先了解下業(yè)務(wù),后續(xù)優(yōu)化下。
優(yōu)化背景
由于這個(gè)數(shù)據(jù)校對(duì)系統(tǒng)最初不是我開發(fā)的,我了解了下數(shù)據(jù)校對(duì)系統(tǒng)的業(yè)務(wù),整體來說,數(shù)據(jù)校對(duì)系統(tǒng)的業(yè)務(wù)還是比較簡(jiǎn)單的。用戶通過商城提交訂單后,會(huì)在訂單微服務(wù)中生成訂單信息,保存在訂單數(shù)據(jù)庫(kù)中。訂單微服務(wù)會(huì)調(diào)用庫(kù)存微服務(wù)的接口,扣減商品的庫(kù)存數(shù)量,并且會(huì)將每筆訂單扣減庫(kù)存的記錄保存在庫(kù)存數(shù)據(jù)庫(kù)中。為了防止用戶提交訂單后沒有扣減庫(kù)存,或者重復(fù)扣減庫(kù)存,數(shù)據(jù)校對(duì)系統(tǒng)每天會(huì)校驗(yàn)訂單中提交的商品數(shù)量與扣減的庫(kù)存數(shù)量是否一致,并且會(huì)將校對(duì)的結(jié)果信息保存到數(shù)據(jù)校對(duì)信息表中。
數(shù)據(jù)校對(duì)系統(tǒng)的總體流程為:先查詢訂單記錄,然后在查詢庫(kù)存的扣減記錄,然后對(duì)比訂單和庫(kù)存扣減記錄,然后將校對(duì)的結(jié)果信息保存到數(shù)據(jù)校對(duì)信息表中,整體流程如下所示。
為了能夠讓大家更好的了解數(shù)據(jù)校對(duì)系統(tǒng)對(duì)于訂單和庫(kù)存的校對(duì)業(yè)務(wù),我將代碼精簡(jiǎn)了下,核心業(yè)務(wù)邏輯代碼如下所示。
- //檢測(cè)是否存在未對(duì)賬訂單
- checkOrders = checkOrders();
- while(checkOrders != null){
- //查詢未校對(duì)的訂單信息
- hasNoOrders = getHasNoOrders();
- //查詢未校對(duì)的庫(kù)存記錄
- hasNoStock = getHasNoStock();
- //校對(duì)數(shù)據(jù)并返回結(jié)果
- checkResult = checkData(hasNoOrders, hasNoStock);
- //將結(jié)果信息保存到數(shù)據(jù)校對(duì)信息表中
- saveCheckResult(checkResult);
- //檢測(cè)是否存在未對(duì)賬訂單
- checkOrders = checkOrders();
- }
好了,上述就是系統(tǒng)優(yōu)化的背景,想必看到這里,很多小伙伴應(yīng)該知道問題出在哪里了。我們繼續(xù)往下看。
問題分析
雖然很多小伙伴應(yīng)該已經(jīng)知道系統(tǒng)性能低下的問題所在了,這里,我們就一起詳細(xì)分析下校對(duì)系統(tǒng)性能低下的原因。
既然運(yùn)營(yíng)的同事說數(shù)據(jù)校對(duì)系統(tǒng)越來越慢了,我們首先要做的就是找到系統(tǒng)的性能瓶頸所在。據(jù)了解,目前的數(shù)據(jù)對(duì)賬系統(tǒng),由于訂單記錄和庫(kù)存扣減記錄數(shù)據(jù)量巨大,所以查詢未校對(duì)的訂單信息的方法getHasNoOrders()和查詢?yōu)樾?duì)的庫(kù)存記錄的方法getHasNoStock()相對(duì)來說比較慢。并且在數(shù)據(jù)校對(duì)系統(tǒng)中,校對(duì)訂單和庫(kù)存記錄的方法是單線程執(zhí)行的,我們可以簡(jiǎn)單畫一個(gè)時(shí)間抽線圖,如下所示。
由圖可以看出,以單線程的方式getHasNoOrders()方法和getHasNoStock()方法耗費(fèi)了大量的時(shí)間,這兩個(gè)方法本身在邏輯上就是兩個(gè)獨(dú)立的方法,并且這兩個(gè)方法沒有先后的執(zhí)行的順序依賴。那這兩個(gè)方法能不能并行執(zhí)行呢?很顯然是可以的。那我們把getHasNoOrders()方法和getHasNoStock()方法分別放到兩個(gè)不同的線程中,優(yōu)化下系統(tǒng)的性能,整體流程如下所示。
優(yōu)化后,我們將getHasNoOrders()方法放到線程1中執(zhí)行,getHasNoStock()方法放到線程2中執(zhí)行,checkData()方法和saveCheckResult()方法發(fā)放到線程3中執(zhí)行,優(yōu)化后的系統(tǒng)性能相比優(yōu)化前的系統(tǒng)性能幾乎提升了一倍,優(yōu)化效果相對(duì)來說還是比較明顯的。
說到這里,大家應(yīng)該應(yīng)該知道具體怎么優(yōu)化了吧?好,我們繼續(xù)往下看!
解決方案
解決問題的思路有了,接下來,我們看看如何使用代碼實(shí)現(xiàn)我們上面分析的解決問題的思路。這里,我們可以分別開啟兩個(gè)線程執(zhí)行g(shù)etHasNoOrders()方法和getHasNoStock()方法,在主線程中執(zhí)行checkData()方法和saveCheckResult()方法。這里需要注意的是:主線程需要等待兩個(gè)子線程執(zhí)行完畢之后再執(zhí)行checkData()方法和saveCheckResult()方法。 為了實(shí)現(xiàn)這個(gè)功能,我們可以使用Thread類中join()方法,有關(guān)Thread類中join()方法的具體說明,這里,具體的邏輯就是在主線程中調(diào)用兩個(gè)子線程的join()方法實(shí)現(xiàn)阻塞等待,當(dāng)兩個(gè)子線程執(zhí)行完畢退出時(shí),調(diào)用兩個(gè)子線程join()方法的主線程會(huì)被喚醒,從而執(zhí)行主線程中的checkData()方法和saveCheckResult()方法。大體代碼如下所示。
- //檢測(cè)是否存在未對(duì)賬訂單
- checkOrders = checkOrders();
- while(checkOrders != null){
- Thread t1 = new Thread(()->{
- //查詢未校對(duì)的訂單信息
- hasNoOrders = getHasNoOrders();
- });
- t1.start();
- Thread t2 = new Thread(()->{
- //查詢未校對(duì)的庫(kù)存記錄
- hasNoStock = getHasNoStock();
- });
- t2.start();
- //阻塞主線程,等待線程t1和線程t2執(zhí)行完畢
- t1.join();
- t2.join();
- //校對(duì)數(shù)據(jù)并返回結(jié)果
- checkResult = checkData(hasNoOrders, hasNoStock);
- //將結(jié)果信息保存到數(shù)據(jù)校對(duì)信息表中
- saveCheckResult(checkResult);
- //檢測(cè)是否存在未對(duì)賬訂單
- checkOrders = checkOrders();
- }
至此,我們基本上能夠解決問題了。但是,還有沒有進(jìn)一步優(yōu)化的空間呢?我們進(jìn)一步往下看。
進(jìn)一步優(yōu)化
通過上面對(duì)系統(tǒng)優(yōu)化,基本能夠達(dá)成我們的優(yōu)化目標(biāo),但是上面的解決方案存在著不足的地方,那就是在while循環(huán)里每次都要新建兩個(gè)線程分別執(zhí)行g(shù)etHasNoOrders()方法和getHasNoStock()方法,了解Java多線程的小伙伴們應(yīng)該都知道,在Java中創(chuàng)建線程可是個(gè)非常耗時(shí)的操作。所以,最好是能夠?qū)?chuàng)建出來的線程反復(fù)使用。這里,估計(jì)很多小伙伴都會(huì)想到使用線程池,沒錯(cuò),我們可以使用線程池進(jìn)一步優(yōu)化上面的代碼。
遇到新的問題
不過在使用線程池進(jìn)一步優(yōu)化時(shí),我們會(huì)遇到一個(gè)問題,就是主線程如何等待子線程中的結(jié)果數(shù)據(jù)呢?說直白點(diǎn)就是:主線程如何知道子線程中的getHasNoOrders()方法和getHasNoStock()方法執(zhí)行完了? 由于在之前的代碼中我們是在主線程中調(diào)用子線程的join()方法等待子線程執(zhí)行完畢,獲取到子線程執(zhí)行的結(jié)果后,繼續(xù)執(zhí)行主線程的邏輯。但是如果使用了線程池的話,線程池中的線程根本不會(huì)退出,此時(shí),我們無法使用線程的join()方法等待線程執(zhí)行完畢。
所以,主線程如何知道子線程中的getHasNoOrders()方法和getHasNoStock()方法執(zhí)行完了? 這個(gè)問題就成了關(guān)鍵的突破點(diǎn)。這里,我們使用線程池進(jìn)一步優(yōu)化的代碼如下所示。
- //檢測(cè)是否存在未對(duì)賬訂單
- checkOrders = checkOrders();
- //創(chuàng)建線程池
- Executor executor = Executors.newFixedThreadPool(2);
- while(checkOrders != null){
- executor.execute(()->{
- //查詢未校對(duì)的訂單信息
- hasNoOrders = getHasNoOrders();
- });
- executor.execute(()->{
- //查詢未校對(duì)的庫(kù)存記錄
- hasNoStock = getHasNoStock();
- });
- /**如何知道子線程中的getHasNoOrders()方法和getHasNoStock()方法執(zhí)行完了成為關(guān)鍵**/
- //校對(duì)數(shù)據(jù)并返回結(jié)果
- checkResult = checkData(hasNoOrders, hasNoStock);
- //將結(jié)果信息保存到數(shù)據(jù)校對(duì)信息表中
- saveCheckResult(checkResult);
- //檢測(cè)是否存在未對(duì)賬訂單
- checkOrders = checkOrders();
- }
那么,如何解決這個(gè)問題呢?我們繼續(xù)往下看。
新的解決方案
相信細(xì)心的小伙伴們能夠看出,整個(gè)業(yè)務(wù)的場(chǎng)景就是:一個(gè)線程需要等待其他兩個(gè)線程的邏輯執(zhí)行完畢后再執(zhí)行。在Java的并發(fā)類庫(kù)中,為我們提供了一個(gè)能夠在這種場(chǎng)景下使用的類庫(kù),那就是CountDownLatch類。
使用CountDownLatch類優(yōu)化我們程序的具體做法就是:在程序的while()循環(huán)中首先創(chuàng)建一個(gè)CountDownLatch對(duì)象,計(jì)數(shù)器的值初始化為2。分別在hasNoOrders = getHasNoOrders();代碼和hasNoStock = getHasNoStock();代碼的后面調(diào)用latch.countDown()方法使得計(jì)數(shù)器的值分別減1。在主線程中調(diào)用latch.await()方法,等待計(jì)數(shù)器的值變?yōu)?,繼續(xù)往下執(zhí)行。這樣,就能夠完美解決我們遇到的問題了。優(yōu)化后的代碼如下所示。
- //檢測(cè)是否存在未對(duì)賬訂單
- checkOrders = checkOrders();
- //創(chuàng)建線程池
- Executor executor = Executors.newFixedThreadPool(2);
- while(checkOrders != null){
- CountDownLatch latch = new CountDownLatch(2);
- executor.execute(()->{
- //查詢未校對(duì)的訂單信息
- hasNoOrders = getHasNoOrders();
- latch.countDown();
- });
- executor.execute(()->{
- //查詢未校對(duì)的庫(kù)存記錄
- hasNoStock = getHasNoStock();
- latch.countDown();
- });
- //等待子線程的邏輯執(zhí)行完畢
- latch.await();
- //校對(duì)數(shù)據(jù)并返回結(jié)果
- checkResult = checkData(hasNoOrders, hasNoStock);
- //將結(jié)果信息保存到數(shù)據(jù)校對(duì)信息表中
- saveCheckResult(checkResult);
- //檢測(cè)是否存在未對(duì)賬訂單
- checkOrders = checkOrders();
- }
至此,我們就完成了系統(tǒng)的優(yōu)化工作。
總結(jié)與思考
這次系統(tǒng)性能的優(yōu)化,主要是將單線程執(zhí)行的數(shù)據(jù)校對(duì)業(yè)務(wù),優(yōu)化成使用多線程執(zhí)行。在平時(shí)的工作過程中,我們需要認(rèn)真思考,找到系統(tǒng)性能瓶頸所在,找出在邏輯上不相干,并且沒有先后順序的業(yè)務(wù)邏輯,將其放到不同的線程中執(zhí)行,能夠大大提供系統(tǒng)的性能。
這次,對(duì)于系統(tǒng)的優(yōu)化,我們最終使用線程池來執(zhí)行比較耗時(shí)的查詢訂單與查詢庫(kù)存記錄的操作,并且在主線程中等待線程池中的線程邏輯執(zhí)行完畢后再執(zhí)行主線程的后續(xù)業(yè)務(wù)邏輯。這種場(chǎng)景,使用Java中提供的CountDownLatch類再合適不過了。這里,再?gòu)?qiáng)調(diào)一下:CountDownLatch主要的使用場(chǎng)景就是一個(gè)線程等待多個(gè)線程執(zhí)行完畢后再執(zhí)行。如下圖所示。
這里,也進(jìn)一步提醒了我們:如果想學(xué)好并發(fā)編程,熟練的掌握J(rèn)ava中提供的并發(fā)類庫(kù)是我們必須要做到的。
本文轉(zhuǎn)載自微信公眾號(hào)「冰河技術(shù)」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系冰河技術(shù)公眾號(hào)。