前言
接口性能優化 對于從事后端開發的同學來說,肯定再熟悉不過了,因為它是一個跟開發語言無關的公共問題。
該問題說簡單也簡單,說復雜也復雜。
有時候,只需加個索引就能解決問題。
有時候,需要做代碼重構。
有時候,需要增加緩存。
有時候,需要引入一些中間件,比如 mq。
有時候,需要分庫分表。
有時候,需要拆分服務。
等等。。。
導致接口性能問題的原因千奇百怪,不同的項目不同的接口,原因可能也不一樣。
本文總結了一些行之有效的優化接口性能的辦法,給有需要的朋友一個參考。
基于 Spring Boot + MyBatis Plus + Vue & Element 實現的后臺管理系統 + 用戶小程序,支持 RBAC 動態權限、多租戶、數據權限、工作流、三方登錄、支付、短信、商城等功能
- 項目地址:https://github.com/YunaiV/ruoyi-vue-pro
- 視頻教程:https://doc.iocoder.cn/video/
1.索引
接口性能優化大家第一個想到的可能是:優化索引
。
沒錯,優化索引的成本是最小的。
你通過查看線上日志或者監控報告,查到某個接口用到的某條 sql 語句耗時比較長。
這時你可能會有下面這些疑問:
- 該 sql 語句加索引了沒?
- 加的索引生效了沒?
- mysql 選錯索引了沒?
1.1 沒加索引
sql 語句中where
條件的關鍵字段,或者order by
后面的排序字段,忘了加索引,這個問題在項目中很常見。
項目剛開始的時候,由于表中的數據量小,加不加索引 sql 查詢性能差別不大。
后來,隨著業務的發展,表中數據量越來越多,就不得不加索引了。
可以通過命令:
showindexfrom`order`;
能單獨查看某張表的索引情況。
也可以通過命令:
showcreatetable`order`;
查看整張表的建表語句,里面同樣會顯示索引情況。
通過ALTER TABLE
命令可以添加索引:
ALTERTABLE`order`ADDINDEXidx_name(name);
也可以通過CREATE INDEX
命令添加索引:
CREATEINDEXidx_nameON`order`(name);
不過這里有一個需要注意的地方是:想通過命令修改索引,是不行的。
目前在 mysql 中如果想要修改索引,只能先刪除索引,再重新添加新的。
刪除索引可以用ALTER TABLE
命令:
ALTERTABLE`order`DROPINDEXidx_name;
用DROP INDEX
命令也行:
DROPINDEXidx_nameON`order`;
1.2 索引沒生效
通過上面的命令我們已經能夠確認索引是有的,但它生效了沒?此時你內心或許會冒出這樣一個疑問。
那么,如何查看索引有沒有生效呢?
答:可以使用explain
命令,查看 mysql 的執行計劃,它會顯示索引的使用情況。
例如:
explainselect*from`order`wherecode='002';
結果:
通過這幾列可以判斷索引使用情況,執行計劃包含列的含義如下圖所示:
說實話,sql語句沒有走索引,排除沒有建索引之外,最大的可能性是索引失效了。
下面說說索引失效的常見原因:
如果不是上面的這些原因,則需要再進一步排查一下其他原因。
1.3 選錯索引
此外,你有沒有遇到過這樣一種情況:明明是同一條 sql,只有入參不同而已。有的時候走的索引 a,有的時候卻走的索引 b?
沒錯,有時候 mysql 會選錯索引。
必要時可以使用force index
來強制查詢 sql 走某個索引。
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現的后臺管理系統 + 用戶小程序,支持 RBAC 動態權限、多租戶、數據權限、工作流、三方登錄、支付、短信、商城等功能
2. sql 優化
如果優化了索引之后,也沒啥效果。
接下來試著優化一下 sql 語句,因為它的改造成本相對于 java 代碼來說也要小得多。
下面給大家列舉了 sql 優化的 15 個小技巧:
3. 遠程調用
很多時候,我們需要在某個接口中,調用其他服務的接口。
比如有這樣的業務場景:
在用戶信息查詢接口中需要返回:用戶名稱、性別、等級、頭像、積分、成長值等信息。
而用戶名稱、性別、等級、頭像在用戶服務中,積分在積分服務中,成長值在成長值服務中。為了匯總這些數據統一返回,需要另外提供一個對外接口服務。
于是,用戶信息查詢接口需要調用用戶查詢接口、積分查詢接口和成長值查詢接口,然后匯總數據統一返回。
調用過程如下圖所示:
調用遠程接口總耗時 530ms = 200ms + 150ms + 180ms。
顯然這種串行調用遠程接口性能是非常不好的,調用遠程接口總的耗時為所有的遠程接口耗時之和。
那么如何優化遠程接口性能呢?
3.1 并行調用
上面說到,既然串行調用多個遠程接口性能很差,為什么不改成并行呢?
如下圖所示:
調用遠程接口總耗時 200ms = 200ms(即耗時最長的那次遠程接口調用)。
在 java8 之前可以通過實現Callable
接口,獲取線程返回結果。java8 以后通過CompleteFuture
類實現該功能。
我們這里以 CompleteFuture 為例:
publicUserInfogetUserInfo(Longid)throwsInterruptedException,ExecutionException{
finalUserInfouserInfo=newUserInfo();
CompletableFutureuserFuture=CompletableFuture.supplyAsync(()->{
getRemoteUserAndFill(id,userInfo);
returnBoolean.TRUE;
},executor);
CompletableFuturebonusFuture=CompletableFuture.supplyAsync(()->{
getRemoteBonusAndFill(id,userInfo);
returnBoolean.TRUE;
},executor);
CompletableFuturegrowthFuture=CompletableFuture.supplyAsync(()->{
getRemoteGrowthAndFill(id,userInfo);
returnBoolean.TRUE;
},executor);
CompletableFuture.allOf(userFuture,bonusFuture,growthFuture).join();
userFuture.get();
bonusFuture.get();
growthFuture.get();
returnuserInfo;
}
溫馨提醒一下,這兩種方式別忘了使用線程池。示例中我用到了 executor,表示自定義的線程池,為了防止高并發場景下,出現線程過多的問題。
3.2 數據異構
上面說到的用戶信息查詢接口需要調用用戶查詢接口、積分查詢接口和成長值查詢接口,然后匯總數據統一返回。
那么,我們能不能把數據冗余一下,把用戶信息、積分和成長值的數據統一存儲到一個地方,比如:redis,存的數據結構就是用戶信息查詢接口所需要的內容。然后通過用戶 id,直接從 redis 中查詢數據出來,不就 OK了?
如果在高并發的場景下,為了提升接口性能,遠程接口調用大概率會被去掉,而改成保存冗余數據的數據異構方案。
但需要注意的是,如果使用了數據異構方案,就可能會出現數據一致性問題。
用戶信息、積分和成長值有更新的話,大部分情況下,會先更新到數據庫,然后同步到 redis。但這種跨庫的操作,可能會導致兩邊數據不一致的情況產生。
4. 重復調用
重復調用
在我們的日常工作代碼中可以說隨處可見,但如果沒有控制好,會非常影響接口的性能。
不信,我們一起看看。
4.1 循環查數據庫
有時候,我們需要從指定的用戶集合中,查詢出有哪些是在數據庫中已經存在的。
實現代碼可以這樣寫:
publicListqueryUser(ListsearchList) {
if(CollectionUtils.isEmpty(searchList)){
returnCollections.emptyList();
}
Listresult=Lists.newArrayList();
searchList.forEach(user->result.add(userMapper.getUserById(user.getId())));
returnresult;
}
這里如果有 50 個用戶,則需要循環 50 次去查詢數據庫。我們都知道,每查詢一次數據庫,就是一次遠程調用。
如果查詢 50 次數據庫,就有 50 次遠程調用,這是非常耗時的操作。
那么,我們如何優化呢?
具體代碼如下:
publicListqueryUser(ListsearchList) {
if(CollectionUtils.isEmpty(searchList)){
returnCollections.emptyList();
}
Listids=searchList.stream().map(User::getId).collect(Collectors.toList());
returnuserMapper.getUserByIds(ids);
}
提供一個根據用戶 id 集合批量查詢用戶的接口,只遠程調用一次,就能查詢出所有的數據。
這里有個需要注意的地方是:id 集合的大小要做限制,最好一次不要請求太多的數據。要根據實際情況而定,建議控制每次請求的記錄條數在 500 以內。
4.2 死循環
有些小伙伴看到這個標題,可能會感到有點意外,死循環也算?
代碼中不是應該避免死循環嗎?為啥還是會產生死循環?
有時候死循環是我們自己寫的,例如下面這段代碼:
while(true){
if(condition){
break;
}
System.out.println("dosamething");
}
這里使用了 while(true) 的循環調用,這種寫法在CAS自旋鎖
中使用比較多。
當滿足 condition 等于 true 的時候,則自動退出該循環。
如果 condition 條件非常復雜,一旦出現判斷不正確,或者少寫了一些邏輯判斷,就可能在某些場景下出現死循環的問題。
出現死循環,大概率是開發人員人為的 bug 導致的,不過這種情況很容易被測出來。
還有一種隱藏的比較深的死循環,是由于代碼寫得不太嚴謹導致的。如果用正常數據,可能測不出問題,但一旦出現異常數據,就會立即出現死循環。
4.3 無限遞歸
如果想要打印某個分類的所有父分類,可以用類似這樣的遞歸方法實現:
publicvoidprintCategory(Categorycategory){
if(category==null
||category.getParentId()==null){
return;
}
System.out.println("父分類名稱:"+category.getName());
Categoryparent=categoryMapper.getCategoryById(category.getParentId());
printCategory(parent);
}
正常情況下,這段代碼是沒有問題的。
但如果某次有人誤操作,把某個分類的 parentId 指向了它自己,這樣就會出現無限遞歸的情況。導致接口一直不能返回數據,最終會發生堆棧溢出。
建議寫遞歸方法時,設定一個遞歸的深度,比如:分類最大等級有 4 級,則深度可以設置為 4。然后在遞歸方法中做判斷,如果深度大于 4 時,則自動返回,這樣就能避免無限循環的情況。
5. 異步處理
有時候,我們接口性能優化,需要重新梳理一下業務邏輯,看看是否有設計上不太合理的地方。
比如有個用戶請求接口中,需要做業務操作、發站內通知和記錄操作日志。為了實現起來比較方便,通常我們會將這些邏輯放在接口中同步執行,勢必會對接口性能造成一定的影響。
接口內部流程圖如下:
這個接口表面上看起來沒有問題,但如果你仔細梳理一下業務邏輯,會發現只有業務操作才是核心邏輯
,其他的功能都是非核心邏輯
。
在這里有個原則就是:核心邏輯可以同步執行,同步寫庫。非核心邏輯,可以異步執行,異步寫庫。
上面這個例子中,發站內通知和用戶操作日志功能,對實時性要求不高,即使晚點寫庫,用戶無非是晚點收到站內通知,或者運營晚點看到用戶操作日志,對業務影響不大,所以完全可以異步處理。
通常異步主要有兩種:多線程
和 mq
。
5.1 線程池
使用線程池
改造之后,接口邏輯如下:
發站內通知和用戶操作日志功能,被提交到了兩個單獨的線程池中。
這樣接口中重點關注的是業務操作,把其他的邏輯交給線程異步執行,這樣改造之后,讓接口性能瞬間提升了。
但使用線程池有個小問題就是:如果服務器重啟了,或者是需要被執行的功能出現異常了,無法重試,會丟數據。
5.2 mq
使用mq
改造之后,接口邏輯如下:
對于發站內通知和用戶操作日志功能,在接口中并沒真正實現,它只發送了 mq 消息到 mq 服務器。然后由 mq 消費者消費消息時,才真正地執行這兩個功能。
這樣改造之后,接口性能同樣提升了,因為發送 mq 消息速度是很快的,我們只需關注業務操作的代碼即可。
6. 避免大事務
很多小伙伴在使用 spring 框架開發項目時,為了方便,喜歡使用@Transactional
注解提供事務功能。
沒錯,使用 @Transactional 注解這種聲明式事務的方式提供事務功能,確實能少寫很多代碼,提升開發效率。
但也容易造成大事務,引發其他的問題。
下面用一張圖看看大事務引發的問題。
從圖中能夠看出,大事務問題可能會造成接口超時,對接口的性能有直接的影響。
我們該如何優化大事務呢?
- 少用 @Transactional 注解
- 將查詢(select)方法放到事務外
- 事務中避免遠程調用
- 事務中避免一次性處理太多數據
- 有些功能可以非事務執行
- 有些功能可以異步處理
7. 鎖粒度
在某些業務場景中,多個線程并發修改某個共享數據,會造成數據異常。
為了解決并發場景下,多個線程同時修改數據造成數據不一致的情況,通常情況下,我們會:加鎖
。
但如果鎖加得不好,導致鎖的粒度太粗,也會非常影響接口性能。
7.1 synchronized
在 java 中提供了synchronized
關鍵字給我們的代碼加鎖。
通常有兩種寫法:在方法上加鎖
和 在代碼塊上加鎖
。
先看看如何在方法上加鎖:
publicsynchronizeddoSave(StringfileUrl){
mkdir();
uploadFile(fileUrl);
sendMessage(fileUrl);
}
這里加鎖的目的是為了防止并發的情況下創建了相同的目錄,第二次會創建失敗,影響業務功能。
但這種直接在方法上加鎖,鎖的粒度有點粗。因為 doSave 方法中的上傳文件和發消息功能,是不需要加鎖的。只有創建目錄功能,才需要加鎖。
我們都知道文件上傳操作是非常耗時的,如果將整個方法加鎖,那么需要等到整個方法執行完之后才能釋放鎖。顯然,這會導致該方法的性能很差,變得得不償失。
這時,我們可以改成在代碼塊上加鎖了,具體代碼如下:
publicvoiddoSave(Stringpath,StringfileUrl){
synchronized(this){
if(!exists(path)){
mkdir(path);
}
}
uploadFile(fileUrl);
sendMessage(fileUrl);
}
這樣改造之后,鎖的粒度一下子變小了,只有并發創建目錄功能才加了鎖。而創建目錄是一個非常快的操作,即使加鎖對接口的性能影響也不大。
最重要的是,其他的上傳文件和發送消息功能,仍然可以并發執行。
當然,這樣做在單機版的服務中,是沒有問題的。但現在部署的生產環境,為了保證服務的穩定性,一般情況下,同一個服務會被部署在多個節點中。如果哪天掛了一個節點,其他的節點服務仍然可用。
多節點部署避免了因為某個節點掛了,導致服務不可用的情況。同時也能分攤整個系統的流量,避免系統壓力過大。
同時它也帶來了新的問題:synchronized 只能保證一個節點加鎖是有效的,但如果有多個節點如何加鎖呢?
答:這就需要使用:分布式鎖
了。目前主流的分布式鎖包括:redis 分布式鎖、zookeeper 分布式鎖和數據庫分布式鎖。
由于 zookeeper 分布式鎖的性能不太好,真實業務場景用的不多,這里就不講了。
下面聊一下 redis 分布式鎖。
7.2 redis 分布式鎖
在分布式系統中,由于 redis 分布式鎖相對更簡單和高效,成為了分布式鎖的首選,被我們用到了很多實際業務場景當中。
使用 redis 分布式鎖的偽代碼如下:
publicvoiddoSave(Stringpath,StringfileUrl){
try{
Stringresult=jedis.set(lockKey,requestId,"NX","PX",expireTime);
if("OK".equals(result)){
if(!exists(path)){
mkdir(path);
uploadFile(fileUrl);
sendMessage(fileUrl);
}
returntrue;
}
}finally{
unlock(lockKey,requestId);
}
returnfalse;
}
跟之前使用synchronized
關鍵字加鎖時一樣,這里鎖的范圍也太大了,換句話說就是鎖的粒度太粗,這樣會導致整個方法的執行效率很低。
其實只有創建目錄的時候,才需要加分布式鎖,其余代碼根本不用加鎖。
于是,我們需要優化一下代碼:
publicvoiddoSave(Stringpath,StringfileUrl){
if(this.tryLock()){
mkdir(path);
}
uploadFile(fileUrl);
sendMessage(fileUrl);
}
privatebooleantryLock(){
try{
Stringresult=jedis.set(lockKey,requestId,"NX","PX",expireTime);
if("OK".equals(result)){
returntrue;
}
}finally{
unlock(lockKey,requestId);
}
returnfalse;
}
上面代碼將加鎖的范圍縮小了,只有創建目錄時才加了鎖。這樣看似簡單的優化之后,接口性能能提升很多。說不定,會有意外的驚喜喔。哈哈哈。
redis 分布式鎖雖說好用,但它在使用時,有很多注意的細節,隱藏了很多坑,如果稍不注意很容易踩中。redis 分布式鎖的 8 大坑具體如下:
7.3 數據庫分布式鎖
mysql 數據庫中主要有三種鎖:
- 表鎖:加鎖快,不會出現死鎖。但鎖的粒度大,發生鎖沖突的概率最高,并發度最低。
- 行鎖:加鎖慢,會出現死鎖。但鎖的粒度最小,發生鎖沖突的概率最低,并發度也最高。
- 間隙鎖:開銷和加鎖時間界于表鎖和行鎖之間。它會出現死鎖,鎖的粒度界于表鎖和行鎖之間,并發度一般。
并發度越高,意味著接口性能越好。
所以數據庫鎖的優化方向是:優先使用行鎖
,其次使用間隙鎖
,再其次使用表鎖
。
趕緊看看,你用對了沒?
8.分頁處理
有時候我們會調用某個接口批量查詢數據,比如:通過用戶 id 批量查詢出用戶信息,然后給這些用戶送積分。
但如果你一次性查詢的用戶數量太多了,比如一次查詢 2000 個用戶的數據。參數中傳入了 2000 個用戶的 id,遠程調用接口,會發現該用戶查詢接口經常超時。
調用代碼如下:
Listusers=remoteCallUser(ids);
眾所周知,調用接口從數據庫獲取數據,是需要經過網絡傳輸的。如果數據量太大,無論是獲取數據的速度,還是網絡傳輸受限于帶寬,都會導致消耗時間比較長。
那么,這種情況要如何優化呢?
答:分頁處理
。
將一次獲取所有數據的請求,改成分多次獲取,每次只獲取一部分用戶的數據,最后進行合并和匯總。
其實,處理這個問題,要分為兩種場景:同步調用
和 異步調用
。
8.1 同步調用
如果在job
中需要獲取 2000 個用戶的信息,它要求只要能正確獲取到數據就好,對獲取數據的總耗時要求不太高。
但對每一次遠程接口調用的耗時有要求,不能大于 500ms,不然會有郵件預警。
這時,我們可以同步分頁調用批量查詢用戶信息接口。
具體示例代碼如下:
List>allIds=Lists.partition(ids,200);
for(ListbatchIds:allIds){
Listusers=remoteCallUser(batchIds);
}
代碼中用了google
的guava
工具中的Lists.partition
方法,用它來做分頁簡直太好用了,不然要巴拉巴拉寫一大堆分頁的代碼。
8.2 異步調用
如果是在某個接口
中需要獲取 2000 個用戶的信息,它考慮的就需要更多一些。
除了需要考慮遠程調用接口的耗時之外,還需要考慮該接口本身的總耗時,也不能超時 500ms。
這時候用上面的同步分頁請求遠程接口,肯定是行不通的。
那么,只能使用異步調用
了。
代碼如下:
List>allIds=Lists.partition(ids,200);
finalListresult=Lists.newArrayList();
allIds.stream().forEach((batchIds)->{
CompletableFuture.supplyAsync(()->{
result.addAll(remoteCallUser(batchIds));
returnBoolean.TRUE;
},executor);
})
使用 CompletableFuture 類,多個線程異步調用遠程接口,最后匯總結果統一返回。
9.加緩存
解決接口性能問題,加緩存
是一個非常高效的方法。
但不能為了緩存而緩存,還是要看具體的業務場景。畢竟加了緩存,會導致接口的復雜度增加,它會帶來數據不一致問題。
在有些并發量比較低的場景中,比如用戶下單,可以不用加緩存。
還有些場景,比如在商城首頁顯示商品分類的地方,假設這里的分類是調用接口獲取到的數據,但頁面暫時沒有做靜態化。
如果查詢分類樹的接口沒有使用緩存,而直接從數據庫查詢數據,性能會非常差。
那么如何使用緩存呢?
9.1 redis 緩存
通常情況下,我們使用最多的緩存可能是:redis
和memcached
。
但對于 java 應用來說,絕大多數都是使用的 redis,所以接下來我們以 redis 為例。
由于在關系型數據庫,比如:mysql 中,菜單是有上下級關系的。某個四級分類是某個三級分類的子分類,這個三級分類又是某個二級分類的子分類,而這個二級分類又是某個一級分類的子分類。
這種存儲結構決定了,想一次性查出這個分類樹,并非是一件非常容易的事情。這就需要使用程序遞歸查詢了,如果分類多的話,這個遞歸是比較耗時的。
所以,如果每次都直接從數據庫中查詢分類樹的數據,是一個非常耗時的操作。
這時我們可以使用緩存,大部分情況,接口都直接從緩存中獲取數據。操作 redis 可以使用成熟的框架,比如:jedis 和 redisson 等。
用 jedis 偽代碼如下:
Stringjson=jedis.get(key);
if(StringUtils.isNotEmpty(json)){
CategoryTreecategoryTree=JsonUtil.toObject(json);
returncategoryTree;
}
returnqueryCategoryTreeFromDb();
先從 redis 中根據某個 key 查詢是否有菜單數據,如果有則轉換成對象,直接返回。如果 redis 中沒有查到菜單數據,則再從數據庫中查詢菜單數據,有則返回。
此外,我們還需要有個 job,每隔一段時間從數據庫中查詢菜單數據,更新到 redis 當中,這樣以后每次都能直接從 redis 中獲取菜單的數據,而無需訪問數據庫了。
這樣改造之后,能快速地提升性能。
但這樣做性能提升不是最佳的,還有其他的方案,我們一起看看下面的內容。
9.2 二級緩存
上面的方案是基于 redis 緩存的,雖說 redis 訪問速度很快。但畢竟是一個遠程調用,而且菜單樹的數據很多,在網絡傳輸的過程中,是有些耗時的。
有沒有辦法,不經過請求遠程,就能直接獲取到數據呢?
答:使用二級緩存
,即基于內存的緩存。
除了自己手寫的內存緩存之外,目前使用比較多的內存緩存框架有:guava、Ehcache、caffine等。
我們這里以caffeine
為例,它是 spring 官方推薦的。
第一步,引入 caffeine 的相關 jar 包。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-cacheartifactId>
dependency>
<dependency>
<groupId>com.github.ben-manes.caffeinegroupId>
<artifactId>caffeineartifactId>
<version>2.6.0version>
dependency>
第二步,配置 CacheManager,開啟 EnableCaching。
@Configuration
@EnableCaching
publicclassCacheConfig{
@Bean
publicCacheManagercacheManager(){
CaffeineCacheManagercacheManager=newCaffeineCacheManager();
//Caffeine配置
Caffeine
第三步,使用 Cacheable 注解獲取數據。
@Service
publicclassCategoryService{
@Cacheable(value="category",key="#categoryKey")
publicCategoryModelgetCategory(StringcategoryKey){
Stringjson=jedis.get(categoryKey);
if(StringUtils.isNotEmpty(json)){
CategoryTreecategoryTree=JsonUtil.toObject(json);
returncategoryTree;
}
returnqueryCategoryTreeFromDb();
}
}
調用 categoryService.getCategory() 方法時,先從 caffine 緩存中獲取數據,如果能夠獲取到數據,則直接返回該數據,不進入方法體。
如果不能獲取到數據,則再從 redis 中查一次數據。如果查詢到了,則返回數據,并且放入 caffine 中。
如果還是沒有查到數據,則直接從數據庫中獲取到數據,然后放到 caffine 緩存中。
具體流程圖如下:
該方案的性能更好,但有個缺點就是,如果數據更新了,不能及時刷新緩存。此外,如果有多臺服務器節點,可能存在各個節點上數據不一樣的情況。
由此可見,二級緩存給我們帶來性能提升的同時,也帶來了數據不一致的問題。使用二級緩存一定要結合實際的業務場景,并非所有的業務場景都適用。
但上面列舉的分類場景,是適合使用二級緩存的。因為它屬于用戶不敏感數據,即使出現了稍微有點數據不一致也沒有關系,用戶有可能都沒有察覺出來。
10. 分庫分表
有時候,接口性能受限的不是別的,而是數據庫。
當系統發展到一定的階段,用戶并發量大,會有大量的數據庫請求,需要占用大量的數據庫連接,同時會帶來磁盤 IO 的性能瓶頸問題。
此外,隨著用戶數量越來越多,產生的數據也越來越多,一張表有可能存不下。由于數據量太大,sql 語句查詢數據時,即使走了索引也會非常耗時。
這時該怎么辦呢?
答:需要做分庫分表
。
如下圖所示:
圖中將用戶庫拆分成了三個庫,每個庫都包含了四張用戶表。
如果有用戶請求過來的時候,先根據用戶 id 路由到其中一個用戶庫,然后再定位到某張表。
路由的算法挺多的:
-
根據 id 取模
,比如:id=7,有 4 張表,則 7%4=3,模為 3,路由到用戶表 3。 -
給 id 指定一個區間范圍
,比如:id 的值是 0-10 萬,則數據存在用戶表 0,id 的值是 10-20 萬,則數據存在用戶表 1。 -
一致性 hash 算法
分庫分表主要有兩個方向:垂直
和水平
。
說實話,垂直方向(即業務方向)更簡單。
在水平方向(即數據方向)上,分庫和分表的作用,其實是有區別的,不能混為一談。
-
分庫
:是為了解決數據庫連接資源不足問題和磁盤 IO 的性能瓶頸問題。 -
分表
:是為了解決單表數據量太大,sql 語句查詢數據時,即使走了索引也非常耗時問題。此外還可以解決消耗 cpu 資源問題。 -
分庫分表
:可以解決數據庫連接資源不足、磁盤 IO 的性能瓶頸、檢索數據耗時和消耗 cpu 資源等問題。
如果在有些業務場景中,用戶并發量很大,但是需要保存的數據量很少,這時可以只分庫,不分表。
如果在有些業務場景中,用戶并發量不大,但是需要保存的數量很多,這時可以只分表,不分庫。
如果在有些業務場景中,用戶并發量大,并且需要保存的數量也很多時,可以分庫分表。
11. 輔助功能
優化接口性能問題,除了上面提到的這些常用方法之外,還需要配合使用一些輔助功能,因為它們真的可以幫我們提升查找問題的效率。
11.1 開啟慢查詢日志
通常情況下,為了定位 sql 的性能瓶頸,我們需要開啟 mysql 的慢查詢日志。把超過指定時間的 sql 語句,單獨記錄下來,方面以后分析和定位問題。
開啟慢查詢日志需要重點關注三個參數:
-
slow_query_log
慢查詢開關 -
slow_query_log_file
慢查詢日志存放的路徑 -
long_query_time
超過多少秒才會記錄日志
通過 mysql 的set
命令可以設置:
setglobalslow_query_log='ON';
setglobalslow_query_log_file='/usr/local/mysql/data/slow.log';
setgloballong_query_time=2;
設置完之后,如果某條 sql 的執行時間超過了 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 服務。
很多公司每天早上都會發一封慢查詢日志的郵件,開發人員根據這些信息優化 sql。
11.2 加監控
為了出現 sql 問題時,能夠讓我們及時發現,我們需要對系統做監控
。
目前業界使用比較多的開源監控系統是:Prometheus
。
它提供了 監控
和 預警
的功能。
架構圖如下:
我們可以用它監控如下信息:
- 接口響應時間
- 調用第三方服務耗時
- 慢查詢 sql 耗時
- cpu 使用情況
- 內存使用情況
- 磁盤使用情況
- 數據庫使用情況
等等。。。
它的界面大概長這樣子:
可以看到 mysql 當前 qps、活躍線程數、連接數、緩存池的大小等信息。
如果發現數據量連接池占用太多,對接口的性能肯定會有影響。
這時可能是代碼中開啟了連接忘了關,或者并發量太大了導致的,需要做進一步排查和系統優化。
截圖中只是它一小部分功能,如果你想了解更多功能,可以訪問 Prometheus 的官網:https://prometheus.io/。
11.3 鏈路跟蹤
有時候某個接口涉及的邏輯很多,比如:查數據庫、查 redis、遠程調用接口,發 mq 消息,執行業務代碼等等。
該接口一次請求的鏈路很長,如果逐一排查,需要花費大量的時間,這時候,我們已經沒法用傳統的辦法定位問題了。
有沒有辦法解決這問題呢?
用分布式鏈路跟蹤系統:skywalking
。
架構圖如下:
通過 skywalking 定位性能問題:
在 skywalking 中可以通過traceId
(全局唯一的 id),串聯一個接口請求的完整鏈路。可以看到整個接口的耗時、調用的遠程服務的耗時、訪問數據庫或者 redis 的耗時等等,功能非常強大。
之前沒有這個功能的時候,為了定位線上接口性能問題,我們還需要在代碼中加日志,手動打印出鏈路中各個環節的耗時情況,然后再逐一排查。
-
接口
+關注
關注
33文章
8526瀏覽量
150862 -
SQL
+關注
關注
1文章
760瀏覽量
44081 -
索引
+關注
關注
0文章
59瀏覽量
10465
原文標題:接口性能優化的11個小技巧,大家務必掌握!
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論