<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          接口性能優(yōu)化的11個小技巧,這也太實用了叭!

          共 14398字,需瀏覽 29分鐘

           ·

          2021-12-17 15:45

          前言

          接口性能優(yōu)化對于從事后端開發(fā)的同學來說,肯定再熟悉不過了,因為它是一個跟開發(fā)語言無關的公共問題。

          該問題說簡單也簡單,說復雜也復雜。

          有時候,只需加個索引就能解決問題。

          有時候,需要做代碼重構。

          有時候,需要增加緩存。

          有時候,需要引入一些中間件,比如mq。

          有時候,需要需要分庫分表。

          有時候,需要拆分服務。

          等等。。。

          導致接口性能問題的原因千奇百怪,不同的項目不同的接口,原因可能也不一樣。

          本文我總結了一些行之有效的,優(yōu)化接口性能的辦法,給有需要的朋友一個參考。

          1.索引

          接口性能優(yōu)化大家第一個想到的可能是:優(yōu)化索引

          沒錯,優(yōu)化索引的成本是最小的。

          你通過查看線上日志或者監(jiān)控報告,查到某個接口用到的某條sql語句耗時比較長。

          這時你可能會有下面這些疑問:

          1. 該sql語句加索引了沒?
          2. 加的索引生效了沒?
          3. mysql選錯索引了沒?

          1.1 沒加索引

          sql語句中where條件的關鍵字段,或者order by后面的排序字段,忘了加索引,這個問題在項目中很常見。

          項目剛開始的時候,由于表中的數(shù)據(jù)量小,加不加索引sql查詢性能差別不大。

          后來,隨著業(yè)務的發(fā)展,表中數(shù)據(jù)量越來越多,就不得不加索引了。

          可以通過命令:

          show?index?from?`order`;

          能單獨查看某張表的索引情況。

          也可以通過命令:

          show?create?table?`order`;

          查看整張表的建表語句,里面同樣會顯示索引情況。

          通過ALTER TABLE命令可以添加索引:

          ALTER?TABLE?`order`?ADD?INDEX?idx_name?(name);

          也可以通過CREATE INDEX命令添加索引:

          CREATE?INDEX?idx_name?ON?`order`?(name);

          不過這里有一個需要注意的地方是:想通過命令修改索引,是不行的。

          目前在mysql中如果想要修改索引,只能先刪除索引,再重新添加新的。

          刪除索引可以用DROP INDEX命令:

          ALTER?TABLE?`order`?DROP?INDEX?idx_name;

          DROP INDEX命令也行:

          DROP?INDEX?idx_name?ON?`order`;

          1.2 索引沒生效

          通過上面的命令我們已經(jīng)能夠確認索引是有的,但它生效了沒?此時你內(nèi)心或許會冒出這樣一個疑問。

          那么,如何查看索引有沒有生效呢?

          答:可以使用explain命令,查看mysql的執(zhí)行計劃,它會顯示索引的使用情況。

          例如:

          explain?select?*?from?`order`?where?code='002';

          結果:通過這幾列可以判斷索引使用情況,執(zhí)行計劃包含列的含義如下圖所示:如果你想進一步了解explain的詳細用法,可以看看我的另一篇文章《explain | 索引優(yōu)化的這把絕世好劍,你真的會用嗎?》

          說實話,sql語句沒有走索引,排除沒有建索引之外,最大的可能性是索引失效了。

          下面說說索引失效的常見原因:如果不是上面的這些原因,則需要再進一步排查一下其他原因。

          1.3 選錯索引

          此外,你有沒有遇到過這樣一種情況:明明是同一條sql,只有入?yún)⒉煌?。有的時候走的索引a,有的時候卻走的索引b?

          沒錯,有時候mysql會選錯索引。

          必要時可以使用force index來強制查詢sql走某個索引。

          至于為什么mysql會選錯索引,后面有專門的文章介紹的,這里先留點懸念。

          2. sql優(yōu)化

          如果優(yōu)化了索引之后,也沒啥效果。

          接下來試著優(yōu)化一下sql語句,因為它的改造成本相對于java代碼來說也要小得多。

          下面給大家列舉了sql優(yōu)化的15個小技巧:由于這些技巧在我之前的文章中已經(jīng)詳細介紹過了,在這里我就不深入了。

          更詳細的內(nèi)容,可以看我的另一篇文章《聊聊sql優(yōu)化的15個小技巧》,相信看完你會有很多收獲。

          3. 遠程調(diào)用

          很多時候,我們需要在某個接口中,調(diào)用其他服務的接口。

          比如有這樣的業(yè)務場景:

          在用戶信息查詢接口中需要返回:用戶名稱、性別、等級、頭像、積分、成長值等信息。

          而用戶名稱、性別、等級、頭像在用戶服務中,積分在積分服務中,成長值在成長值服務中。為了匯總這些數(shù)據(jù)統(tǒng)一返回,需要另外提供一個對外接口服務。

          于是,用戶信息查詢接口需要調(diào)用用戶查詢接口、積分查詢接口 和 成長值查詢接口,然后匯總數(shù)據(jù)統(tǒng)一返回。

          調(diào)用過程如下圖所示:調(diào)用遠程接口總耗時 530ms = 200ms + 150ms + 180ms

          顯然這種串行調(diào)用遠程接口性能是非常不好的,調(diào)用遠程接口總的耗時為所有的遠程接口耗時之和。

          那么如何優(yōu)化遠程接口性能呢?

          3.1 并行調(diào)用

          上面說到,既然串行調(diào)用多個遠程接口性能很差,為什么不改成并行呢?

          如下圖所示:調(diào)用遠程接口總耗時 200ms = 200ms(即耗時最長的那次遠程接口調(diào)用)

          在java8之前可以通過實現(xiàn)Callable接口,獲取線程返回結果。

          java8以后通過CompleteFuture類實現(xiàn)該功能。我們這里以CompleteFuture為例:

          public?UserInfo?getUserInfo(Long?id)?throws?InterruptedException,?ExecutionException?{
          ????final?UserInfo?userInfo?=?new?UserInfo();
          ????CompletableFuture?userFuture?=?CompletableFuture.supplyAsync(()?->?{
          ????????getRemoteUserAndFill(id,?userInfo);
          ????????return?Boolean.TRUE;
          ????},?executor);

          ????CompletableFuture?bonusFuture?=?CompletableFuture.supplyAsync(()?->?{
          ????????getRemoteBonusAndFill(id,?userInfo);
          ????????return?Boolean.TRUE;
          ????},?executor);

          ????CompletableFuture?growthFuture?=?CompletableFuture.supplyAsync(()?->?{
          ????????getRemoteGrowthAndFill(id,?userInfo);
          ????????return?Boolean.TRUE;
          ????},?executor);
          ????CompletableFuture.allOf(userFuture,?bonusFuture,?growthFuture).join();

          ????userFuture.get();
          ????bonusFuture.get();
          ????growthFuture.get();

          ????return?userInfo;
          }

          溫馨提醒一下,這兩種方式別忘了使用線程池。示例中我用到了executor,表示自定義的線程池,為了防止高并發(fā)場景下,出現(xiàn)線程過多的問題。

          3.2 數(shù)據(jù)異構

          上面說到的用戶信息查詢接口需要調(diào)用用戶查詢接口、積分查詢接口 和 成長值查詢接口,然后匯總數(shù)據(jù)統(tǒng)一返回。

          那么,我們能不能把數(shù)據(jù)冗余一下,把用戶信息、積分和成長值的數(shù)據(jù)統(tǒng)一存儲到一個地方,比如:redis,存的數(shù)據(jù)結構就是用戶信息查詢接口所需要的內(nèi)容。然后通過用戶id,直接從redis中查詢數(shù)據(jù)出來,不就OK了?

          如果在高并發(fā)的場景下,為了提升接口性能,遠程接口調(diào)用大概率會被去掉,而改成保存冗余數(shù)據(jù)的數(shù)據(jù)異構方案。

          但需要注意的是,如果使用了數(shù)據(jù)異構方案,就可能會出現(xiàn)數(shù)據(jù)一致性問題。

          用戶信息、積分和成長值有更新的話,大部分情況下,會先更新到數(shù)據(jù)庫,然后同步到redis。但這種跨庫的操作,可能會導致兩邊數(shù)據(jù)不一致的情況產(chǎn)生。

          4. 重復調(diào)用

          重復調(diào)用在我們的日常工作代碼中可以說隨處可見,但如果沒有控制好,會非常影響接口的性能。

          不信,我們一起看看。

          4.1 循環(huán)查數(shù)據(jù)庫

          有時候,我們需要從指定的用戶集合中,查詢出有哪些是在數(shù)據(jù)庫中已經(jīng)存在的。

          實現(xiàn)代碼可以這樣寫:

          public?List?queryUser(List?searchList)?{
          ????if?(CollectionUtils.isEmpty(searchList))?{
          ????????return?Collections.emptyList();
          ????}

          ????List?result?=?Lists.newArrayList();
          ????searchList.forEach(user?->?result.add(userMapper.getUserById(user.getId())));
          ????return?result;
          }

          這里如果有50個用戶,則需要循環(huán)50次,去查詢數(shù)據(jù)庫。我們都知道,每查詢一次數(shù)據(jù)庫,就是一次遠程調(diào)用。

          如果查詢50次數(shù)據(jù)庫,就有50次遠程調(diào)用,這是非常耗時的操作。

          那么,我們?nèi)绾蝺?yōu)化呢?

          具體代碼如下:

          public?List?queryUser(List?searchList)?{
          ????if?(CollectionUtils.isEmpty(searchList))?{
          ????????return?Collections.emptyList();
          ????}
          ????List?ids?=?searchList.stream().map(User::getId).collect(Collectors.toList());
          ????return?userMapper.getUserByIds(ids);
          }

          提供一個根據(jù)用戶id集合批量查詢用戶的接口,只遠程調(diào)用一次,就能查詢出所有的數(shù)據(jù)。

          這里有個需要注意的地方是:id集合的大小要做限制,最好一次不要請求太多的數(shù)據(jù)。要根據(jù)實際情況而定,建議控制每次請求的記錄條數(shù)在500以內(nèi)。

          4.2 死循環(huán)

          有些小伙伴看到這個標題,可能會感到有點意外,死循環(huán)也算?

          代碼中不是應該避免死循環(huán)嗎?為啥還是會產(chǎn)生死循環(huán)?

          有時候死循環(huán)是我們自己寫的,例如下面這段代碼:

          while(true)?{
          ????if(condition)?{
          ????????break;
          ????}
          ????System.out.println("do?samething");
          }

          這里使用了while(true)的循環(huán)調(diào)用,這種寫法在CAS自旋鎖中使用比較多。

          當滿足condition等于true的時候,則自動退出該循環(huán)。

          如果condition條件非常復雜,一旦出現(xiàn)判斷不正確,或者少寫了一些邏輯判斷,就可能在某些場景下出現(xiàn)死循環(huán)的問題。

          出現(xiàn)死循環(huán),大概率是開發(fā)人員人為的bug導致的,不過這種情況很容易被測出來。

          還有一種隱藏的比較深的死循環(huán),是由于代碼寫的不太嚴謹導致的。如果用正常數(shù)據(jù),可能測不出問題,但一旦出現(xiàn)異常數(shù)據(jù),就會立即出現(xiàn)死循環(huán)。

          4.3 無限遞歸

          如果想要打印某個分類的所有父分類,可以用類似這樣的遞歸方法實現(xiàn):

          public?void?printCategory(Category?category)?{
          ??if(category?==?null?
          ??????||?category.getParentId()?==?null)?{
          ?????return;
          ??}?
          ??System.out.println("父分類名稱:"+?category.getName());
          ??Category?parent?=?categoryMapper.getCategoryById(category.getParentId());
          ??printCategory(parent);
          }

          正常情況下,這段代碼是沒有問題的。

          但如果某次有人誤操作,把某個分類的parentId指向了它自己,這樣就會出現(xiàn)無限遞歸的情況。導致接口一直不能返回數(shù)據(jù),最終會發(fā)生堆棧溢出。

          建議寫遞歸方法時,設定一個遞歸的深度,比如:分類最大等級有4級,則深度可以設置為4。然后在遞歸方法中做判斷,如果深度大于4時,則自動返回,這樣就能避免無限循環(huán)的情況。

          5. 異步處理

          有時候,我們接口性能優(yōu)化,需要重新梳理一下業(yè)務邏輯,看看是否有設計上不太合理的地方。

          比如有個用戶請求接口中,需要做業(yè)務操作,發(fā)站內(nèi)通知,和記錄操作日志。為了實現(xiàn)起來比較方便,通常我們會將這些邏輯放在接口中同步執(zhí)行,勢必會對接口性能造成一定的影響。

          接口內(nèi)部流程圖如下:這個接口表面上看起來沒有問題,但如果你仔細梳理一下業(yè)務邏輯,會發(fā)現(xiàn)只有業(yè)務操作才是核心邏輯,其他的功能都是非核心邏輯。

          在這里有個原則就是:核心邏輯可以同步執(zhí)行,同步寫庫。非核心邏輯,可以異步執(zhí)行,異步寫庫。

          上面這個例子中,發(fā)站內(nèi)通知和用戶操作日志功能,對實時性要求不高,即使晚點寫庫,用戶無非是晚點收到站內(nèi)通知,或者運營晚點看到用戶操作日志,對業(yè)務影響不大,所以完全可以異步處理。

          通常異步主要有兩種:多線程mq。

          5.1 線程池

          使用線程池改造之后,接口邏輯如下:發(fā)站內(nèi)通知和用戶操作日志功能,被提交到了兩個單獨的線程池中。

          這樣接口中重點關注的是業(yè)務操作,把其他的邏輯交給線程異步執(zhí)行,這樣改造之后,讓接口性能瞬間提升了。

          但使用線程池有個小問題就是:如果服務器重啟了,或者是需要被執(zhí)行的功能出現(xiàn)異常了,無法重試,會丟數(shù)據(jù)。

          那么這個問題該怎么辦呢?

          5.2 mq

          使用mq改造之后,接口邏輯如下:對于發(fā)站內(nèi)通知和用戶操作日志功能,在接口中并沒真正實現(xiàn),它只發(fā)送了mq消息到mq服務器。然后由mq消費者消費消息時,才真正的執(zhí)行這兩個功能。

          這樣改造之后,接口性能同樣提升了,因為發(fā)送mq消息速度是很快的,我們只需關注業(yè)務操作的代碼即可。

          6. 避免大事務

          很多小伙伴在使用spring框架開發(fā)項目時,為了方便,喜歡使用@Transactional注解提供事務功能。

          沒錯,使用@Transactional注解這種聲明式事務的方式提供事務功能,確實能少寫很多代碼,提升開發(fā)效率。

          但也容易造成大事務,引發(fā)其他的問題。

          下面用一張圖看看大事務引發(fā)的問題。從圖中能夠看出,大事務問題可能會造成接口超時,對接口的性能有直接的影響。

          我們該如何優(yōu)化大事務呢?

          1. 少用@Transactional注解
          2. 將查詢(select)方法放到事務外
          3. 事務中避免遠程調(diào)用
          4. 事務中避免一次性處理太多數(shù)據(jù)
          5. 有些功能可以非事務執(zhí)行
          6. 有些功能可以異步處理

          關于大事務問題我的另一篇文章《讓人頭痛的大事務問題到底要如何解決?》,它里面做了非常詳細的介紹,如果大家感興趣可以看看。

          7. 鎖粒度

          在某些業(yè)務場景中,為了防止多個線程并發(fā)修改某個共享數(shù)據(jù),造成數(shù)據(jù)異常。

          為了解決并發(fā)場景下,多個線程同時修改數(shù)據(jù),造成數(shù)據(jù)不一致的情況。通常情況下,我們會:加鎖。

          但如果鎖加得不好,導致鎖的粒度太粗,也會非常影響接口性能。

          7.1 synchronized

          在java中提供了synchronized關鍵字給我們的代碼加鎖。

          通常有兩種寫法:在方法上加鎖在代碼塊上加鎖。

          先看看如何在方法上加鎖:

          public?synchronized?doSave(String?fileUrl)?{
          ????mkdir();
          ????uploadFile(fileUrl);
          ????sendMessage(fileUrl);
          }

          這里加鎖的目的是為了防止并發(fā)的情況下,創(chuàng)建了相同的目錄,第二次會創(chuàng)建失敗,影響業(yè)務功能。

          但這種直接在方法上加鎖,鎖的粒度有點粗。因為doSave方法中的上傳文件和發(fā)消息方法,是不需要加鎖的。只有創(chuàng)建目錄方法,才需要加鎖。

          我們都知道文件上傳操作是非常耗時的,如果將整個方法加鎖,那么需要等到整個方法執(zhí)行完之后才能釋放鎖。顯然,這會導致該方法的性能很差,變得得不償失。

          這時,我們可以改成在代碼塊上加鎖了,具體代碼如下:

          public?void?doSave(String?path,String?fileUrl)?{
          ????synchronized(this)?{
          ??????if(!exists(path))?{
          ??????????mkdir(path);
          ???????}
          ????}
          ????uploadFile(fileUrl);
          ????sendMessage(fileUrl);
          }

          這樣改造之后,鎖的粒度一下子變小了,只有并發(fā)創(chuàng)建目錄功能才加了鎖。而創(chuàng)建目錄是一個非??斓牟僮?,即使加鎖對接口的性能影響也不大。

          最重要的是,其他的上傳文件和發(fā)送消息功能,任然可以并發(fā)執(zhí)行。

          當然,這種做在單機版的服務中,是沒有問題的。但現(xiàn)在部署的生產(chǎn)環(huán)境,為了保證服務的穩(wěn)定性,一般情況下,同一個服務會被部署在多個節(jié)點中。如果哪天掛了一個節(jié)點,其他的節(jié)點服務任然可用。

          多節(jié)點部署避免了因為某個節(jié)點掛了,導致服務不可用的情況。同時也能分攤整個系統(tǒng)的流量,避免系統(tǒng)壓力過大。

          同時它也帶來了新的問題:synchronized只能保證一個節(jié)點加鎖是有效的,但如果有多個節(jié)點如何加鎖呢?

          答:這就需要使用:分布式鎖了。目前主流的分布式鎖包括:redis分布式鎖、zookeeper分布式鎖 和 數(shù)據(jù)庫分布式鎖。

          由于zookeeper分布式鎖的性能不太好,真實業(yè)務場景用的不多,這里先不講。

          下面聊一下redis分布式鎖。

          7.2 redis分布式鎖

          在分布式系統(tǒng)中,由于redis分布式鎖相對于更簡單和高效,成為了分布式鎖的首先,被我們用到了很多實際業(yè)務場景當中。

          使用redis分布式鎖的偽代碼如下:

          public?void?doSave(String?path,String?fileUrl)?{
          ??try?{
          ????String?result?=?jedis.set(lockKey,?requestId,?"NX",?"PX",?expireTime);
          ????if?("OK".equals(result))?{
          ??????if(!exists(path))?{
          ?????????mkdir(path);
          ?????????uploadFile(fileUrl);
          ?????????sendMessage(fileUrl);
          ??????}
          ??????return?true;
          ????}
          ??}?finally{
          ??????unlock(lockKey,requestId);
          ??}??
          ??return?false;
          }

          跟之前使用synchronized關鍵字加鎖時一樣,這里鎖的范圍也太大了,換句話說就是鎖的粒度太粗,這樣會導致整個方法的執(zhí)行效率很低。

          其實只有創(chuàng)建目錄的時候,才需要加分布式鎖,其余代碼根本不用加鎖。

          于是,我們需要優(yōu)化一下代碼:

          public?void?doSave(String?path,String?fileUrl)?{
          ???if(this.tryLock())?{
          ??????mkdir(path);
          ???}
          ???uploadFile(fileUrl);
          ???sendMessage(fileUrl);
          }

          private?boolean?tryLock()?{
          ????try?{
          ????String?result?=?jedis.set(lockKey,?requestId,?"NX",?"PX",?expireTime);
          ????if?("OK".equals(result))?{
          ??????return?true;
          ????}
          ??}?finally{
          ??????unlock(lockKey,requestId);
          ??}??
          ??return?false;
          }

          上面代碼將加鎖的范圍縮小了,只有創(chuàng)建目錄時才加了鎖。這樣看似簡單的優(yōu)化之后,接口性能能提升很多。說不定,會有意外的驚喜喔。哈哈哈。

          redis分布式鎖雖說好用,但它在使用時,有很多注意的細節(jié),隱藏了很多坑,如果稍不注意很容易踩中。詳細內(nèi)容可以看看我的另一篇文章《聊聊redis分布式鎖的8大坑》

          7.3 數(shù)據(jù)庫分布式鎖

          mysql數(shù)據(jù)庫中主要有三種鎖:

          • 表鎖:加鎖快,不會出現(xiàn)死鎖。但鎖定粒度大,發(fā)生鎖沖突的概率最高,并發(fā)度最低。
          • 行鎖:加鎖慢,會出現(xiàn)死鎖。但鎖定粒度最小,發(fā)生鎖沖突的概率最低,并發(fā)度也最高。
          • 間隙鎖:開銷和加鎖時間界于表鎖和行鎖之間。它會出現(xiàn)死鎖,鎖定粒度界于表鎖和行鎖之間,并發(fā)度一般。

          并發(fā)度越高,意味著接口性能越好。

          所以數(shù)據(jù)庫鎖的優(yōu)化方向是:

          優(yōu)先使用行鎖,其次使用間隙鎖,再其次使用表鎖。

          趕緊看看,你用對了沒?

          8.分頁處理

          有時候我會調(diào)用某個接口批量查詢數(shù)據(jù),比如:通過用戶id批量查詢出用戶信息,然后給這些用戶送積分。

          但如果你一次性查詢的用戶數(shù)量太多了,比如一次查詢2000個用戶的數(shù)據(jù)。參數(shù)中傳入了2000個用戶的id,遠程調(diào)用接口,會發(fā)現(xiàn)該用戶查詢接口經(jīng)常超時。

          調(diào)用代碼如下:

          List?users?=?remoteCallUser(ids);

          眾所周知,調(diào)用接口從數(shù)據(jù)庫獲取數(shù)據(jù),是需要經(jīng)過網(wǎng)絡傳輸?shù)?。如果?shù)據(jù)量太大,無論是獲取數(shù)據(jù)的速度,還是網(wǎng)絡傳輸受限于帶寬,都會導致耗時時間比較長。

          那么,這種情況要如何優(yōu)化呢?

          答:分頁處理。

          將一次獲取所有的數(shù)據(jù)的請求,改成分多次獲取,每次只獲取一部分用戶的數(shù)據(jù),最后進行合并和匯總。

          其實,處理這個問題,要分為兩種場景:同步調(diào)用異步調(diào)用。

          8.1 同步調(diào)用

          如果在job中需要獲取2000個用戶的信息,它要求只要能正確獲取到數(shù)據(jù)就好,對獲取數(shù)據(jù)的總耗時要求不太高。

          但對每一次遠程接口調(diào)用的耗時有要求,不能大于500ms,不然會有郵件預警。

          這時,我們可以同步分頁調(diào)用批量查詢用戶信息接口。

          具體示例代碼如下:

          List>?allIds?=?Lists.partition(ids,200);

          for(List?batchIds:allIds)?{
          ???List?users?=?remoteCallUser(batchIds);
          }

          代碼中我用的googleguava工具中的Lists.partition方法,用它來做分頁簡直太好用了,不然要巴拉巴拉寫一大堆分頁的代碼。

          8.2 異步調(diào)用

          如果是在某個接口中需要獲取2000個用戶的信息,它考慮的就需要更多一些。

          除了需要考慮遠程調(diào)用接口的耗時之外,還需要考慮該接口本身的總耗時,也不能超時500ms。

          這時候用上面的同步分頁請求遠程接口,肯定是行不通的。

          那么,只能使用異步調(diào)用了。

          代碼如下:

          List>?allIds?=?Lists.partition(ids,200);

          final?List?result?=?Lists.newArrayList();
          allIds.stream().forEach((batchIds)?->?{
          ???CompletableFuture.supplyAsync(()?->?{
          ????????result.addAll(remoteCallUser(batchIds));
          ????????return?Boolean.TRUE;
          ????},?executor);
          })

          使用CompletableFuture類,多個線程異步調(diào)用遠程接口,最后匯總結果統(tǒng)一返回。

          9.加緩存

          解決接口性能問題,加緩存是一個非常高效的方法。

          但不能為了緩存而緩存,還是要看具體的業(yè)務場景。畢竟加了緩存,會導致接口的復雜度增加,它會帶來數(shù)據(jù)不一致問題。

          在有些并發(fā)量比較低的場景中,比如用戶下單,可以不用加緩存。

          還有些場景,比如在商城首頁顯示商品分類的地方,假設這里的分類是調(diào)用接口獲取到的數(shù)據(jù),但頁面暫時沒有做靜態(tài)化。

          如果查詢分類樹的接口沒有使用緩存,而直接從數(shù)據(jù)庫查詢數(shù)據(jù),性能會非常差。

          那么如何使用緩存呢?

          9.1 redis緩存

          通常情況下,我們使用最多的緩存可能是:redismemcached

          但對于java應用來說,絕大多數(shù)都是使用的redis,所以接下來我們以redis為例。

          由于在關系型數(shù)據(jù)庫,比如:mysql中,菜單是有上下級關系的。某個四級分類是某個三級分類的子分類,這個三級分類,又是某個二級分類的子分類,而這個二級分類,又是某個一級分類的子分類。

          這種存儲結構決定了,想一次性查出這個分類樹,并非是一件非常容易的事情。這就需要使用程序遞歸查詢了,如果分類多的話,這個遞歸是比較耗時的。

          所以,如果每次都直接從數(shù)據(jù)庫中查詢分類樹的數(shù)據(jù),是一個非常耗時的操作。

          這時我們可以使用緩存,大部分情況,接口都直接從緩存中獲取數(shù)據(jù)。操作redis可以使用成熟的框架,比如:jedis和redisson等。

          用jedis偽代碼如下:

          String?json?=?jedis.get(key);
          if(StringUtils.isNotEmpty(json))?{
          ???CategoryTree?categoryTree?=?JsonUtil.toObject(json);
          ???return?categoryTree;
          }
          return?queryCategoryTreeFromDb();

          先從redis中根據(jù)某個key查詢是否有菜單數(shù)據(jù),如果有則轉換成對象,直接返回。如果redis中沒有查到菜單數(shù)據(jù),則再從數(shù)據(jù)庫中查詢菜單數(shù)據(jù),有則返回。

          此外,我們還需要有個job每隔一段時間,從數(shù)據(jù)庫中查詢菜單數(shù)據(jù),更新到redis當中,這樣以后每次都能直接從redis中獲取菜單的數(shù)據(jù),而無需訪問數(shù)據(jù)庫了。這樣改造之后,能快速的提升性能。

          但這樣做性能提升不是最佳的,還有其他的方案,我們一起看看下面的內(nèi)容。

          9.2 二級緩存

          上面的方案是基于redis緩存的,雖說redis訪問速度很快。但畢竟是一個遠程調(diào)用,而且菜單樹的數(shù)據(jù)很多,在網(wǎng)絡傳輸?shù)倪^程中,是有些耗時的。

          有沒有辦法,不經(jīng)過請求遠程,就能直接獲取到數(shù)據(jù)呢?

          答:使用二級緩存,即基于內(nèi)存的緩存。

          除了自己手寫的內(nèi)存緩存之后,目前使用比較多的內(nèi)存緩存框架有:guava、Ehcache、caffine等。

          我們在這里以caffeine為例,它是spring官方推薦的。

          第一步,引入caffeine的相關jar包


          ????org.springframework.boot
          ????spring-boot-starter-cache


          ????com.github.ben-manes.caffeine
          ????caffeine
          ????2.6.0

          第二步,配置CacheManager,開啟EnableCaching

          @Configuration
          @EnableCaching
          public?class?CacheConfig?{
          ????@Bean
          ????public?CacheManager?cacheManager(){
          ????????CaffeineCacheManager?cacheManager?=?new?CaffeineCacheManager();
          ????????//Caffeine配置
          ????????Caffeine?caffeine?=?Caffeine.newBuilder()
          ????????????????//最后一次寫入后經(jīng)過固定時間過期
          ????????????????.expireAfterWrite(10,?TimeUnit.SECONDS)
          ????????????????//緩存的最大條數(shù)
          ????????????????.maximumSize(1000);
          ????????cacheManager.setCaffeine(caffeine);
          ????????return?cacheManager;
          ????}
          }

          第三步,使用Cacheable注解獲取數(shù)據(jù)

          @Service
          public?class?CategoryService?{
          ???
          ???@Cacheable(value?=?"category",?key?=?"#categoryKey")
          ???public?CategoryModel?getCategory(String?categoryKey)?{
          ??????String?json?=?jedis.get(categoryKey);
          ??????if(StringUtils.isNotEmpty(json))?{
          ?????????CategoryTree?categoryTree?=?JsonUtil.toObject(json);
          ?????????return?categoryTree;
          ??????}
          ??????return?queryCategoryTreeFromDb();
          ???}
          }

          調(diào)用categoryService.getCategory()方法時,先從caffine緩存中獲取數(shù)據(jù),如果能夠獲取到數(shù)據(jù),則直接返回該數(shù)據(jù),不進入方法體。

          如果不能獲取到數(shù)據(jù),則再從redis中查一次數(shù)據(jù)。如果查詢到了,則返回數(shù)據(jù),并且放入caffine中。

          如果還是沒有查到數(shù)據(jù),則直接從數(shù)據(jù)庫中獲取到數(shù)據(jù),然后放到caffine緩存中。

          具體流程圖如下:該方案的性能更好,但有個缺點就是,如果數(shù)據(jù)更新了,不能及時刷新緩存。此外,如果有多臺服務器節(jié)點,可能存在各個節(jié)點上數(shù)據(jù)不一樣的情況。

          由此可見,二級緩存給我們帶來性能提升的同時,也帶來了數(shù)據(jù)不一致的問題。使用二級緩存一定要結合實際的業(yè)務場景,并非所有的業(yè)務場景都適用。

          但上面我列舉的分類場景,是適合使用二級緩存的。因為它屬于用戶不敏感數(shù)據(jù),即使出現(xiàn)了稍微有點數(shù)據(jù)不一致也沒有關系,用戶有可能都沒有察覺出來。

          10. 分庫分表

          有時候,接口性能受限的不是別的,而是數(shù)據(jù)庫。

          當系統(tǒng)發(fā)展到一定的階段,用戶并發(fā)量大,會有大量的數(shù)據(jù)庫請求,需要占用大量的數(shù)據(jù)庫連接,同時會帶來磁盤IO的性能瓶頸問題。

          此外,隨著用戶數(shù)量越來越多,產(chǎn)生的數(shù)據(jù)也越來越多,一張表有可能存不下。由于數(shù)據(jù)量太大,sql語句查詢數(shù)據(jù)時,即使走了索引也會非常耗時。

          這時該怎么辦呢?

          答:需要做分庫分表

          如下圖所示:圖中將用戶庫拆分成了三個庫,每個庫都包含了四張用戶表。

          如果有用戶請求過來的時候,先根據(jù)用戶id路由到其中一個用戶庫,然后再定位到某張表。

          路由的算法挺多的:

          • 根據(jù)id取模,比如:id=7,有4張表,則7%4=3,模為3,路由到用戶表3。
          • 給id指定一個區(qū)間范圍,比如:id的值是0-10萬,則數(shù)據(jù)存在用戶表0,id的值是10-20萬,則數(shù)據(jù)存在用戶表1。
          • 一致性hash算法

          分庫分表主要有兩個方向:垂直水平

          說實話垂直方向(即業(yè)務方向)更簡單。

          在水平方向(即數(shù)據(jù)方向)上,分庫和分表的作用,其實是有區(qū)別的,不能混為一談。

          • 分庫:是為了解決數(shù)據(jù)庫連接資源不足問題,和磁盤IO的性能瓶頸問題。
          • 分表:是為了解決單表數(shù)據(jù)量太大,sql語句查詢數(shù)據(jù)時,即使走了索引也非常耗時問題。此外還可以解決消耗cpu資源問題。
          • 分庫分表:可以解決 數(shù)據(jù)庫連接資源不足、磁盤IO的性能瓶頸、檢索數(shù)據(jù)耗時 和 消耗cpu資源等問題。

          如果在有些業(yè)務場景中,用戶并發(fā)量很大,但是需要保存的數(shù)據(jù)量很少,這時可以只分庫,不分表。

          如果在有些業(yè)務場景中,用戶并發(fā)量不大,但是需要保存的數(shù)量很多,這時可以只分表,不分庫。

          如果在有些業(yè)務場景中,用戶并發(fā)量大,并且需要保存的數(shù)量也很多時,可以分庫分表。

          關于分庫分表更詳細的內(nèi)容,可以看看我另一篇文章,里面講的更深入《阿里二面:為什么分庫分表?》

          11. 輔助功能

          優(yōu)化接口性能問題,除了上面提到的這些常用方法之外,還需要配合使用一些輔助功能,因為它們真的可以幫我們提升查找問題的效率。

          11.1 開啟慢查詢?nèi)罩?/span>

          通常情況下,為了定位sql的性能瓶頸,我們需要開啟mysql的慢查詢?nèi)罩?。把超過指定時間的sql語句,單獨記錄下來,方面以后分析和定位問題。

          開啟慢查詢?nèi)罩拘枰攸c關注三個參數(shù):

          • slow_query_log 慢查詢開關
          • slow_query_log_file 慢查詢?nèi)罩敬娣诺穆窂?/section>
          • long_query_time 超過多少秒才會記錄日志

          通過mysql的set命令可以設置:

          set?global?slow_query_log='ON';?
          set?global?slow_query_log_file='/usr/local/mysql/data/slow.log';
          set?global?long_query_time=2;

          設置完之后,如果某條sql的執(zhí)行時間超過了2秒,會被自動記錄到slow.log文件中。

          當然也可以直接修改配置文件my.cnf

          [mysqld]
          slow_query_log?=?ON
          slow_query_log_file?=?/usr/local/mysql/data/slow.log
          long_query_time?=?2

          但這種方式需要重啟mysql服務。

          很多公司每天早上都會發(fā)一封慢查詢?nèi)罩镜泥]件,開發(fā)人員根據(jù)這些信息優(yōu)化sql。

          11.2 加監(jiān)控

          為了出現(xiàn)sql問題時,能夠讓我們及時發(fā)現(xiàn),我們需要對系統(tǒng)做監(jiān)控。

          目前業(yè)界使用比較多的開源監(jiān)控系統(tǒng)是:Prometheus。

          它提供了 監(jiān)控預警 的功能。

          架構圖如下:

          我們可以用它監(jiān)控如下信息:

          • 接口響應時間
          • 調(diào)用第三方服務耗時
          • 慢查詢sql耗時
          • cpu使用情況
          • 內(nèi)存使用情況
          • 磁盤使用情況
          • 數(shù)據(jù)庫使用情況

          等等。。。

          它的界面大概長這樣子:可以看到mysql當前qps,活躍線程數(shù),連接數(shù),緩存池的大小等信息。

          如果發(fā)現(xiàn)數(shù)據(jù)量連接池占用太多,對接口的性能肯定會有影響。

          這時可能是代碼中開啟了連接忘了關,或者并發(fā)量太大了導致的,需要做進一步排查和系統(tǒng)優(yōu)化。

          截圖中只是它一小部分功能,如果你想了解更多功能,可以訪問Prometheus的官網(wǎng):https://prometheus.io/

          11.3 鏈路跟蹤

          有時候某個接口涉及的邏輯很多,比如:查數(shù)據(jù)庫、查redis、遠程調(diào)用接口,發(fā)mq消息,執(zhí)行業(yè)務代碼等等。

          該接口一次請求的鏈路很長,如果逐一排查,需要花費大量的時間,這時候,我們已經(jīng)沒法用傳統(tǒng)的辦法定位問題了。

          有沒有辦法解決這問題呢?

          用分布式鏈路跟蹤系統(tǒng):skywalking。

          架構圖如下:通過skywalking定位性能問題:在skywalking中可以通過traceId(全局唯一的id),串聯(lián)一個接口請求的完整鏈路??梢钥吹秸麄€接口的耗時,調(diào)用的遠程服務的耗時,訪問數(shù)據(jù)庫或者redis的耗時等等,功能非常強大。

          之前沒有這個功能的時候,為了定位線上接口性能問題,我們還需要在代碼中加日志,手動打印出鏈路中各個環(huán)節(jié)的耗時情況,然后再逐一排查。

          如果你用過skywalking排查接口性能問題,不自覺的會愛上它的。如果你想了解更多功能,可以訪問skywalking的官網(wǎng):https://skywalking.apache.org/


          程序汪資料鏈接

          程序汪接的7個私活都在這里,經(jīng)驗整理

          Java項目分享 最新整理全集,找項目不累啦 06版

          堪稱神級的Spring Boot手冊,從基礎入門到實戰(zhàn)進階

          臥槽!字節(jié)跳動《算法中文手冊》火了,完整版 PDF 開放下載!

          臥槽!阿里大佬總結的《圖解Java》火了,完整版PDF開放下載!

          字節(jié)跳動總結的設計模式 PDF 火了,完整版開放下載!


          歡迎添加程序汪個人微信 itwang009? 進粉絲群或圍觀朋友圈

          瀏覽 39
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久久99人妻精品 | 天堂网av2018 | 性爱青青草 | 欧美色999 | 最新最近日本中文字幕不亚洲 |