亚洲精品中文免费|亚洲日韩中文字幕制服|久久精品亚洲免费|一本之道久久免费

      
      

            <dl id="hur0q"><div id="hur0q"></div></dl>

                看看別人后端API接口性能優(yōu)化的11個方法,那叫一個優(yōu)雅

                看看別人后端API接口性能優(yōu)化的11個方法,那叫一個優(yōu)雅

                1.索引

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

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

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

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

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

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

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

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

                可以通過命令:

                show index from `order`;復(fù)制代碼

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

                也可以通過命令:

                show create table `order`;復(fù)制代碼

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

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

                ALTER TABLE `order` ADD INDEX idx_name (name);復(fù)制代碼

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

                CREATE INDEX idx_name ON `order` (name);復(fù)制代碼

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

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

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

                ALTER TABLE `order` DROP INDEX idx_name;復(fù)制代碼

                用DROP INDEX命令也行:

                DROP INDEX idx_name ON `order`;復(fù)制代碼

                1.2 索引沒生效

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

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

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

                例如:

                explain select * from `order` where code=’002′;復(fù)制代碼

                結(jié)果:

                通過這幾列可以判斷索引使用情況,執(zhí)行計劃包含列的含義如下圖所示:

                如果你想進(jìn)一步了解explain的詳細(xì)用法,可以看看我的另一篇文章《explain | 索引優(yōu)化的這把絕世好劍,你真的會用嗎?》

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

                下面說說索引失效的常見原因:

                如果不是上面的這些原因,則需要再進(jìn)一步排查一下其他原因。

                1.3 選錯索引

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

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

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

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

                2. sql優(yōu)化

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

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

                下面給大家列舉了sql優(yōu)化的15個小技巧:

                由于這些技巧在我之前的文章中已經(jīng)詳細(xì)介紹過了,在這里我就不深入了。

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

                3. 遠(yuǎn)程調(diào)用

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

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

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

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

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

                調(diào)用過程如下圖所示:

                調(diào)用遠(yuǎn)程接口總耗時 530ms = 200ms + 150ms + 180ms

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

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

                3.1 并行調(diào)用

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

                如下圖所示:

                調(diào)用遠(yuǎn)程接口總耗時 200ms = 200ms(即耗時最長的那次遠(yuǎn)程接口調(diào)用)

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

                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;}復(fù)制代碼

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

                3.2 數(shù)據(jù)異構(gòu)

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

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

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

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

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

                4. 重復(fù)調(diào)用

                重復(fù)調(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;}復(fù)制代碼

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

                如果查詢50次數(shù)據(jù)庫,就有50次遠(yuǎn)程調(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);}復(fù)制代碼

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

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

                4.2 死循環(huán)

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

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

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

                while(true) { if(condition) { break; } System.out.println(“do samething”);}復(fù)制代碼

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

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

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

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

                還有一種隱藏的比較深的死循環(huán),是由于代碼寫的不太嚴(yán)謹(jǐn)導(dǎo)致的。如果用正常數(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);}復(fù)制代碼

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

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

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

                5. 異步處理

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

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

                接口內(nèi)部流程圖如下:

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

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

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

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

                5.1 線程池

                使用線程池改造之后,接口邏輯如下:

                發(fā)站內(nèi)通知和用戶操作日志功能,被提交到了兩個單獨的線程池中。

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

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

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

                5.2 mq

                使用mq改造之后,接口邏輯如下:

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

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

                6. 避免大事務(wù)

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

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

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

                下面用一張圖看看大事務(wù)引發(fā)的問題。

                從圖中能夠看出,大事務(wù)問題可能會造成接口超時,對接口的性能有直接的影響。

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

              5. 少用@Transactional注解
              6. 將查詢(select)方法放到事務(wù)外
              7. 事務(wù)中避免遠(yuǎn)程調(diào)用
              8. 事務(wù)中避免一次性處理太多數(shù)據(jù)
              9. 有些功能可以非事務(wù)執(zhí)行
              10. 有些功能可以異步處理
              11. 關(guān)于大事務(wù)問題我的另一篇文章《讓人頭痛的大事務(wù)問題到底要如何解決?》,它里面做了非常詳細(xì)的介紹,如果大家感興趣可以看看。

                7. 鎖粒度

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

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

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

                7.1 synchronized

                在java中提供了synchronized關(guān)鍵字給我們的代碼加鎖。

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

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

                public synchronized doSave(String fileUrl) { mkdir(); uploadFile(fileUrl); sendMessage(fileUrl);}復(fù)制代碼

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

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

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

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

                public void doSave(String path,String fileUrl) { synchronized(this) { if(!exists(path)) { mkdir(path); } } uploadFile(fileUrl); sendMessage(fileUrl);}復(fù)制代碼

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

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

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

                多節(jié)點部署避免了因為某個節(jié)點掛了,導(dǎo)致服務(wù)不可用的情況。同時也能分?jǐn)傉麄€系統(tǒng)的流量,避免系統(tǒng)壓力過大。

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

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

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

                下面聊一下redis分布式鎖。

                7.2 redis分布式鎖

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

                使用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;}復(fù)制代碼

                跟之前使用synchronized關(guān)鍵字加鎖時一樣,這里鎖的范圍也太大了,換句話說就是鎖的粒度太粗,這樣會導(dǎo)致整個方法的執(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;}復(fù)制代碼

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

                redis分布式鎖雖說好用,但它在使用時,有很多注意的細(xì)節(jié),隱藏了很多坑,如果稍不注意很容易踩中。詳細(xì)內(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,遠(yuǎn)程調(diào)用接口,會發(fā)現(xiàn)該用戶查詢接口經(jīng)常超時。

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

                List users = remoteCallUser(ids);復(fù)制代碼

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

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

                答:分頁處理。

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

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

                8.1 同步調(diào)用

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

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

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

                具體示例代碼如下:

                List allIds = Lists.partition(ids,200);for(List batchIds:allIds) { List users = remoteCallUser(batchIds);}復(fù)制代碼

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

                8.2 異步調(diào)用

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

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

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

                那么,只能使用異步調(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);})復(fù)制代碼

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

                9.加緩存

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

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

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

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

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

                那么如何使用緩存呢?

                9.1 redis緩存

                通常情況下,我們使用最多的緩存可能是:redis和memcached。

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

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

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

                所以,如果每次都直接從數(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();復(fù)制代碼

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

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

                這樣改造之后,能快速的提升性能。

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

                9.2 二級緩存

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

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

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

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

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

                第一步,引入caffeine的相關(guān)jar包

                org.springframework.boot spring-boot-starter-cache com.github.ben-manes.caffeine caffeine 2.6.0復(fù)制代碼

                第二步,配置CacheManager,開啟EnableCaching

                @Configuration@EnableCachingpublic 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; }}復(fù)制代碼

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

                @Servicepublic 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(); }}復(fù)制代碼

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

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

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

                具體流程圖如下:

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

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

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

                10. 分庫分表

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

                當(dāng)系統(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è)務(wù)方向)更簡單。

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

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

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

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

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

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

                11. 輔助功能

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

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

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

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

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

                通過mysql的set命令可以設(shè)置:

                set global slow_query_log=’ON’; set global slow_query_log_file=’/usr/local/mysql/data/slow.log’;set global long_query_time=2;復(fù)制代碼

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

                當(dāng)然也可以直接修改配置文件my.cnf

                [mysqld]slow_query_log = ONslow_query_log_file = /usr/local/mysql/data/slow.loglong_query_time = 2復(fù)制代碼

                但這種方式需要重啟mysql服務(wù)。

                很多公司每天早上都會發(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)控 和 預(yù)警 的功能。

                架構(gòu)圖如下:

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

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

                等等。。。

                它的界面大概長這樣子:

                可以看到mysql當(dāng)前qps,活躍線程數(shù),連接數(shù),緩存池的大小等信息。

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

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

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

                11.3 鏈路跟蹤

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

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

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

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

                架構(gòu)圖如下:

                通過skywalking定位性能問題:

                在skywalking中可以通過traceId(全局唯一的id),串聯(lián)一個接口請求的完整鏈路。可以看到整個接口的耗時,調(diào)用的遠(yuǎn)程服務(wù)的耗時,訪問數(shù)據(jù)庫或者redis的耗時等等,功能非常強(qiáng)大。

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

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

                嘮叨嘮叨

                東哥在全網(wǎng)寫了很多圖解網(wǎng)絡(luò)和操作系統(tǒng)的系列文章,很高興收獲到很多朋友的認(rèn)可和支持,正好最近圖解網(wǎng)絡(luò)和操作系統(tǒng)的文章連載的有 20+ 篇了,也算有個體系了。

                鄭重聲明:本文內(nèi)容及圖片均整理自互聯(lián)網(wǎng),不代表本站立場,版權(quán)歸原作者所有,如有侵權(quán)請聯(lián)系管理員(admin#wlmqw.com)刪除。
                用戶投稿
                上一篇 2022年6月20日 23:30
                下一篇 2022年6月20日 23:31

                相關(guān)推薦

                • ios手游模擬器(手游模擬器ios)

                  本文主要講的是ios手游模擬器,以及和手游模擬器ios相關(guān)的知識,如果覺得本文對您有所幫助,不要忘了將本文分享給朋友。 哪個iOS模擬器能多開手游賬號?可以推薦個好用的模擬器給我嗎…

                  2022年11月27日
                • 分享4條發(fā)微商朋友圈的方法(微商朋友圈應(yīng)該怎么發(fā))

                  對于微商朋友來說,朋友圈的重要性不言而喻了。 那么微商的朋友圈到底該怎么發(fā)呢? 為什么同樣是經(jīng)營一個朋友圈,有的微商看起來逼格滿滿,實際效果也不錯;而有的卻動都不動就被屏蔽甚至拉黑…

                  2022年11月27日
                • 短視頻策劃內(nèi)容的3個要點(短視頻策劃內(nèi)容怎么做)

                  短視頻在制作時,內(nèi)容框架非常重要。如果直奔主題,然后結(jié)束,聚卓告訴你,這樣的短視頻已經(jīng)過時了。現(xiàn)在的短視頻需要框架的,但不是任何框架,它需要一種易于理解和消化的框架。而且,現(xiàn)在大多…

                  2022年11月27日
                • 美團(tuán)第三季度實現(xiàn)營收626億元,即時配送訂單量增至50億筆

                  新京報訊(記者秦勝南)11月25日,美團(tuán)發(fā)布業(yè)績公告顯示,第三季度營收為626億元,較去年同比增長28.2%,凈利潤為12.2億元。第三季度,美團(tuán)即時配送訂單數(shù)增長至50億筆。截至…

                  2022年11月27日
                • 個人怎么做抖音帶貨(個人做抖音帶貨能賺錢嗎)

                  抖音如今是大家很熟悉的短視頻平臺,不過現(xiàn)在的抖音卻不只是短視頻那么簡單,它的功能非常豐富,其中一個就是可以帶貨,相信很多小伙伴都有在抖音上買過東西,抖音如今的變現(xiàn)能力也是不容小覷的…

                  2022年11月25日
                • 《寶可夢朱紫》夢特性怎么獲得?隱藏特性獲取方法推薦

                  寶可夢朱紫里有很多寶可夢都是擁有夢特性會變強(qiáng)的寶可夢,很多玩家不知道夢特性怎么獲得,下面就給大家?guī)韺毧蓧糁熳想[藏特性獲取方法推薦,感興趣的小伙伴一起來看看吧,希望能幫助到大家。 …

                  2022年11月25日
                • 《寶可夢朱紫》奇魯莉安怎么進(jìn)化?奇魯莉安進(jìn)化方法分享

                  寶可夢朱紫中的奇魯莉安要怎么進(jìn)化呢?很多玩家都不知道,下面就給大家?guī)韺毧蓧糁熳掀骠斃虬策M(jìn)化方法分享,感興趣的小伙伴一起來看看吧,希望能幫助到大家。 奇魯莉安進(jìn)化方法分享 奇魯莉安…

                  2022年11月25日
                • 小紅書平臺的一些機(jī)制及玩法詳解(小紅書玩法有哪些)

                  關(guān)于小紅書 一:小紅書平臺的一些機(jī)制 1. 筆記內(nèi)容的CES評分機(jī)制 2. 筆記流量入口與長尾效應(yīng) 二:小紅書優(yōu)質(zhì)筆記的特點(分維度、類型分析) 1.筆記的本身架構(gòu)組成 維度 2.…

                  2022年11月25日
                • 什么是推廣cpa一篇文章帶你看懂CPA推廣渠道

                  CPA渠道 CPA指的是按照指定的行為結(jié)算,可以是搜索,可以是注冊,可以是激活,可以是搜索下載激活,可以是綁卡,實名認(rèn)證,可以是付費,可以是瀏覽等等。甲乙雙方可以根據(jù)自己的情況來定…

                  2022年11月25日
                • 百度關(guān)鍵詞快速排名的4大原理解析(百度怎么刷關(guān)鍵詞)

                  近期百度公告驚雷算法2.0,升級之快還是第一次吧,看來百度對于刷點擊行為是零容忍了。之前尹華峰SEO技術(shù)博客介紹過一篇如何使用刷點擊工具,其實市面上有很多這類SEO快速排名的軟件,…

                  2022年11月25日

                聯(lián)系我們

                聯(lián)系郵箱:admin#wlmqw.com
                工作時間:周一至周五,10:30-18:30,節(jié)假日休息