接口優(yōu)化

Web開發(fā)中,后端主要的工作就是寫接口,隨著項(xiàng)目的發(fā)展和系統(tǒng)集成,接口的性能也需要優(yōu)化。

一般導(dǎo)致接口性能問題的原因不盡相同,項(xiàng)目功能不同的接口,導(dǎo)致接口出現(xiàn)性能問題的原因可能也不一樣,要根據(jù)場(chǎng)景來分享,即具體情況具體分析。

哪些問題會(huì)引起接口性能問題?

慢查詢(基于mysql)

分頁

所謂的深度分頁問題,涉及到mysql分頁的原理。通常情況下,mysql的分頁是這樣寫的:

select name,code from student limit 100,20

含義當(dāng)然就是從student表里查100到120這20條數(shù)據(jù),mysql會(huì)把前120條數(shù)據(jù)都查出來,拋棄前100條,返回20條。當(dāng)分頁所以深度不大的時(shí)候當(dāng)然沒問題,隨著分頁的深入,sql可能會(huì)變成這樣:

select name,code from student limit 1000000,20

這個(gè)時(shí)候,mysql會(huì)查出來1000020條數(shù)據(jù),拋棄1000000條,如此大的數(shù)據(jù)量,速度一定快不起來。

那如何解決呢?一般情況下,最好的方式是增加一個(gè)條件:

select name,code from student where id>1000000  limit 20

這樣,mysql會(huì)走主鍵索引,直接連接到1000000處,然后查出來20條數(shù)據(jù)。

但是這個(gè)方式需要接口的調(diào)用方配合改造,把上次查詢出來的最大id以參數(shù)的方式傳給接口提供方,會(huì)有溝通成本(調(diào)用方:老子不改!)。

未加索引

在平時(shí)項(xiàng)目中比較常見的問題:就是在 sql 語句中 where 條件的關(guān)鍵字段,或者 order by 后面的排序字段,漏加索引。

當(dāng)然項(xiàng)目初期體量比較小,表中的數(shù)據(jù)量小,加不加索引 sql 查詢性能差別不大,沒啥影響。

隨后,如果業(yè)務(wù)發(fā)展起來了,表中數(shù)據(jù)量也越來越多,此時(shí)就不得不加索引了。

show create table xxxx(表名)

查看某張表的索引。

具體加索引的語句網(wǎng)上太多了,不再贅述。

不過順便提一嘴,加索引之前,需要考慮一下這個(gè)索引是不是有必要加,如果加索引的字段區(qū)分度非常低,那即使加了索引也不會(huì)生效。

另外,加索引的alter操作,可能引起鎖表,執(zhí)行sql的時(shí)候一定要在低峰期(血淚史?。。。。?/p>

索引失效

這個(gè)是慢查詢最不好分析的情況,雖然mysql提供了explain來評(píng)估某個(gè)sql的查詢性能,其中就有使用的索引。

但是為啥索引會(huì)失效呢?

mysql卻不會(huì)告訴咱,需要咱自己分析。

大體上,可能引起索引失效的原因有這幾個(gè)(可能不完全):

在已經(jīng)能夠確認(rèn)索引有的情況下,接下來需要關(guān)注它是否生效了?

首先我們可以使用 mysql 的 explain 命令來查看 sql 的執(zhí)行計(jì)劃,它會(huì)顯示索引的使用情況。

explain select * from `t_order` where Fdeal_id=1001;

通過 refkey、key_len 這幾列可以知道索引使用情況,執(zhí)行計(jì)劃包含列的含義如下圖所示:

explain 執(zhí)行計(jì)劃中包含關(guān)鍵的信息如下:

  • select_type: 查詢類型
  • table: 表名或者別名
  • partitions: 匹配的分區(qū)
  • type: 訪問類型
  • possible_keys: 可能用到的索引
  • key: 實(shí)際用到的索引
  • key_len: 索引長(zhǎng)度
  • ref: 與索引比較的列
  • rows: 估算的行數(shù)
  • filtered: 按表?xiàng)l件篩選的行百分比

下面列舉了常見索引失效的原因:

  • 不滿足最左前綴原則
  • 使用了 select *
  • 使用索引列時(shí)進(jìn)行計(jì)算
  • 范圍索引沒有放后面
  • 字符類型沒有加引號(hào)
  • 索引列上使用了函數(shù)
  • like 查詢左側(cè)有%
  • 等等

如果區(qū)分性很差,這個(gè)索引根本就沒必要加。區(qū)分性很差是什么意思呢,舉幾個(gè)例子,比如:

  • 某個(gè)字段只可能有3個(gè)值,那這個(gè)字段的索引區(qū)分度就很低。
  • 再比如,某個(gè)字段大量為空,只有少量有值;
  • 再比如,某個(gè)字段值非常集中,90%都是1,剩下10%可能是2,3,4....

進(jìn)一步的,那如果不符合上面所有的索引失效的情況,但是mysql還是不使用對(duì)應(yīng)的索引,是為啥呢?

這個(gè)跟mysql的sql優(yōu)化有關(guān),mysql會(huì)在sql優(yōu)化的時(shí)候自己選擇合適的索引,很可能是mysql自己的選擇算法算出來使用這個(gè)索引不會(huì)提升性能,所以就放棄了。

這種情況,可以使用force index 關(guān)鍵字強(qiáng)制使用索引(建議修改前先實(shí)驗(yàn)一下,是不是真的會(huì)提升查詢效率):

select name,code from student force index(XXXXXX) where name = '天才' 

其中xxxx是索引名。

join過多 or 子查詢過多

我把join過多 和子查詢過多放在一起說了。

一般來說,不建議使用子查詢,可以把子查詢改成join來優(yōu)化。

同時(shí),join關(guān)聯(lián)的表也不宜過多,一般來說2-3張表還是合適的。

具體關(guān)聯(lián)幾張表比較安全是需要具體問題具體分析的,如果各個(gè)表的數(shù)據(jù)量都很少,幾百條幾千條,那么關(guān)聯(lián)的表的可以適當(dāng)多一些,反之則需要少一些。

另外需要提到的是,在大多數(shù)情況下join是在內(nèi)存里做的,如果匹配的量比較小,或者join_buffer設(shè)置的比較大,速度也不會(huì)很慢。

但是,當(dāng)join的數(shù)據(jù)量比較大的時(shí)候,mysql會(huì)采用在硬盤上創(chuàng)建臨時(shí)表的方式進(jìn)行多張表的關(guān)聯(lián)匹配,這種顯然效率就極低,本來磁盤的IO就不快,還要關(guān)聯(lián)。

一般遇到這種情況的時(shí)候就建議從代碼層面進(jìn)行拆分,在業(yè)務(wù)層先查詢一張表的數(shù)據(jù),然后以關(guān)聯(lián)字段作為條件查詢關(guān)聯(lián)表形成map,然后在業(yè)務(wù)層進(jìn)行數(shù)據(jù)的拼裝。一般來說,索引建立正確的話,會(huì)比join快很多,畢竟內(nèi)存里拼接數(shù)據(jù)要比網(wǎng)絡(luò)傳輸和硬盤IO快得多。

in的元素過多

這種問題,如果只看代碼的話不太容易排查,最好結(jié)合監(jiān)控和數(shù)據(jù)庫日志一起分析。

如果一個(gè)查詢有in,in的條件加了合適的索引,這個(gè)時(shí)候的sql還是比較慢就可以高度懷疑是in的元素過多。

一旦排查出來是這個(gè)問題,解決起來也比較容易,不過是把元素分個(gè)組,每組查一次。想再快的話,可以再引入多線程。

進(jìn)一步的,如果in的元素量大到一定程度還是快不起來,這種最好還是有個(gè)限制

select id from student where id in (1,2,3 ...... 1000) limit 200

當(dāng)然了,最好是在代碼層面做個(gè)限制

if (ids.size() > 200) {
    throw new Exception("單次查詢數(shù)據(jù)量不能超過200");
}

單純的數(shù)據(jù)量過大

這種問題,單純代碼的修修補(bǔ)補(bǔ)一般就解決不了了,需要變動(dòng)整個(gè)的數(shù)據(jù)存儲(chǔ)架構(gòu)。

或者是對(duì)底層mysql分表或分庫+分表;或者就是直接變更底層數(shù)據(jù)庫,把mysql轉(zhuǎn)換成專門為處理大數(shù)據(jù)設(shè)計(jì)的數(shù)據(jù)庫。

這種工作是個(gè)系統(tǒng)工程,需要嚴(yán)密的調(diào)研、方案設(shè)計(jì)、方案評(píng)審、性能評(píng)估、開發(fā)、測(cè)試、聯(lián)調(diào),同時(shí)需要設(shè)計(jì)嚴(yán)密的數(shù)據(jù)遷移方案、回滾方案、降級(jí)措施、故障處理預(yù)案。

除了以上團(tuán)隊(duì)內(nèi)部的工作,還可能有跨系統(tǒng)溝通的工作,畢竟做了重大變更,下游系統(tǒng)的調(diào)用接口的方式有可能會(huì)需要變化。

使用遠(yuǎn)程調(diào)用RPC

在大多數(shù)時(shí)候,項(xiàng)目中往往需要在某個(gè)接口中,調(diào)用其它服務(wù)的接口。

比如商城的業(yè)務(wù)場(chǎng)景:

下單時(shí)需要調(diào)用用戶信息接口,在用戶信息查詢接口中需要返回:用戶名稱、性別、等級(jí)、頭像、積分、成長(zhǎng)值等信息,另外也需要調(diào)用商品信息接口,在用戶信息查詢接口中需要返回:商品主圖鏈接、價(jià)格、活動(dòng)等信息。而積分在積分服務(wù)中,活動(dòng)在活動(dòng)服務(wù)中。

因此,為了匯總這些數(shù)據(jù)統(tǒng)一返回,需要另外提供一個(gè)對(duì)外接口的服務(wù)。

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

可以知道遠(yuǎn)程調(diào)用接口總耗時(shí)為:450ms = 150ms + 100ms + 200ms.

很明顯這種串行遠(yuǎn)程調(diào)用接口性能是很差的,效率也非常低,遠(yuǎn)程調(diào)用接口的總耗時(shí)為調(diào)用各個(gè)遠(yuǎn)程接口耗時(shí)之和。

那么如何優(yōu)化遠(yuǎn)程調(diào)用接口的性能呢?繼續(xù)往下看。

使用緩存

我們可以考慮把數(shù)據(jù)冗余一下,把用戶信息、積分和活動(dòng)信息的數(shù)據(jù)統(tǒng)一存儲(chǔ)到一個(gè)地方,比如:redis,存的數(shù)據(jù)結(jié)構(gòu)就是用戶信息查詢接口所需要的內(nèi)容。

接下來可以通過用戶 id,直接從 redis 中查詢出來,這大大提高了效率。

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

但需要注意的是,如果使用了緩存方案,就要另外考慮數(shù)據(jù)一致性的問題。

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

重復(fù)調(diào)用接口

在同一個(gè)接口中,重復(fù)調(diào)用在我們平時(shí)開發(fā)的代碼中可以說隨處可見,但是如果沒有控制好的話,會(huì)大大影響接口的性能。

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

大多數(shù)時(shí)候,我們需要從指定的數(shù)據(jù)庫集合中,查詢出需要用到的數(shù)據(jù)。

當(dāng)有多個(gè)用戶 id 傳多來時(shí),如果每個(gè)用戶 id 都需要查一遍的話,那么就需要循環(huán)多次去查詢數(shù)據(jù)庫了。我們都知道,每查詢一次數(shù)據(jù)庫,就會(huì)進(jìn)行一次遠(yuǎn)程調(diào)用。這是非常耗時(shí)的操作。

那么,我們可以提供一個(gè)根據(jù)用戶id 集合批量查詢用戶信息數(shù)據(jù)的接口,只需遠(yuǎn)程調(diào)用一次即可,就能查詢出所需要的數(shù)據(jù)了。

這里溫馨提示下:id 集合的大小需要做限制以及做入?yún)⑿r?yàn),否則也會(huì)影響查詢性能,最好一次不要請(qǐng)求太多的數(shù)據(jù)??梢愿鶕?jù)業(yè)務(wù)實(shí)際情況而定。

避免出現(xiàn)死循環(huán)

有些時(shí)候,寫代碼一不留神,循環(huán)語句就出現(xiàn)死循環(huán)了。

出現(xiàn)這種情況往往就是 condition 條件沒處理好,導(dǎo)致沒有退出循環(huán),從而導(dǎo)致死循環(huán)。

出現(xiàn)死循環(huán),大概率是代碼的 bug 導(dǎo)致的,不過這種情況很容易被測(cè)出來。

但是,可能還有一種比較隱秘的死循環(huán)代碼,當(dāng)用正常數(shù)據(jù)時(shí),測(cè)不出問題,一旦出現(xiàn)有異常數(shù)據(jù),才會(huì)復(fù)現(xiàn)死循環(huán)的問題。

避免無限遞歸

一些導(dǎo)致無限遞歸的場(chǎng)景以及影響接口性能程度這里就不啰嗦了,總之,在寫遞歸代碼時(shí),建議設(shè)定一個(gè)遞歸的深度(假設(shè)限定為 5),然后在遞歸方法中做一定判斷,如果深度大于 5 時(shí),則自動(dòng)返回,這樣就可以避免無限遞歸了。

考慮使用異步處理

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

比如有個(gè)用戶請(qǐng)求接口中,需要做業(yè)務(wù)操作,發(fā)站內(nèi)通知,和記錄操作日志。

為了實(shí)現(xiàn)起來比較方便,通常我們會(huì)將這些邏輯放在接口中同步執(zhí)行,勢(shì)必會(huì)對(duì)接口性能造成一定的影響。

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

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

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

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

數(shù)據(jù)庫級(jí)別的鎖

使用 mysql 數(shù)據(jù)庫中鎖主要有三種級(jí)別:

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

如果并發(fā)度越高,意味著接口性能越好。所以數(shù)據(jù)庫鎖的優(yōu)化方向是:優(yōu)先使用行鎖,其次使用間隙鎖,再其次使用表鎖。

考慮是否要分庫分表

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

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

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

此時(shí)就需要考慮做分庫分表了。

其它輔助優(yōu)化接口功能

優(yōu)化接口性能問題,除了上面提到的這些常用方法之外,還需要配合使用一些輔助功能,因?yàn)樗鼈冋娴目梢詭臀覀兲嵘檎覇栴}的效率。

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

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

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

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

通過 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;

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

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

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

但這種方式需要重啟mysql服務(wù)。 很多公司每天早上都會(huì)發(fā)一封慢查詢?nèi)罩镜泥]件,開發(fā)人員根據(jù)這些信息優(yōu)化 sql。

加監(jiān)控

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

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

它提供了 監(jiān)控預(yù)警 的功能。 如果你想了解更多功能,可以訪問 Prometheus 的官網(wǎng):https://prometheus.io/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容