<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
我負責的系統在去年初就完成了功能上的建設,然後開始進入到推廣階段。隨著推廣的逐步深入,收到了很多好評的同時也收到了很多對效能的吐槽。
剛剛收到吐槽的時候,我們的心情是這樣的:
當越來越多對效能的吐槽反饋到我們這裡的時候,我們意識到,介面效能的問題的優先順序必須提高了。
然後我們就跟蹤了 1 周的介面效能監控,這個時候我們的心情是這樣的:
有 20 多個慢介面,5 個介面響應時間超過 5s,1 個超過 10s,其餘的都在 2s 以上,穩定性不足 99.8%。
作為一個優秀的後端程式設計師,這個資料肯定是不能忍的,我們馬上就進入了漫長的介面優化之路。本文就是對我們漫長工作歷程的一個總結。
基於 Spring Boot + MyBatis Plus + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能。
這個問題的答案非常多,需要根據自己的業務場景具體分析。
這裡做一個不完全的總結:
基於微服務的思想,構建在 B2C 電商場景下的專案實戰。核心技術棧,是 Spring Boot + Dubbo 。未來,會重構成 Spring Cloud Alibaba 。
所謂的深度分頁問題,涉及到 mysql 分頁的原理。通常情況下,mysql 的分頁是這樣寫的:
select name,code from student limit 100,20
含義當然就是從 student 表裡查 100 到 120 這 20 條資料,mysql 會把前 120 條資料都查出來,拋棄前 100 條,返回 20 條。
當分頁所以深度不大的時候當然沒問題,隨著分頁的深入,sql 可能會變成這樣:
select name,code from student limit 1000000,20
這個時候,mysql 會查出來 1000020 條資料,拋棄 1000000 條,如此大的資料量,速度一定快不起來。
那如何解決呢?一般情況下,最好的方式是增加一個條件:
select name,code from student where id>1000000 limit 20
這樣,mysql 會走主鍵索引,直接連線到 1000000 處,然後查出來 20 條資料。但是這個方式需要介面的呼叫方配合改造,把上次查詢出來的最大 id 以引數的方式傳給介面提供方,會有溝通成本(呼叫方:老子不改!)。
這個是最容易解決的問題,我們可以通過:
show create table xxxx(表名)
檢視某張表的索引。具體加索引的語句網上太多了,不再贅述。不過順便提一嘴,加索引之前,需要考慮一下這個索引是不是有必要加,如果加索引的欄位區分度非常低,那即使加了索引也不會生效。
另外,加索引的 alter 操作,可能引起鎖表,執行 sql 的時候一定要在低峰期(血淚史!!!!)
這個是慢查詢最不好分析的情況,雖然 mysql 提供了 explain 來評估某個 sql 的查詢效能,其中就有使用的索引。
但是為啥索引會失效呢?mysql 卻不會告訴咱,需要咱自己分析。大體上,可能引起索引失效的原因有這幾個(可能不完全):
需要特別提出的是,關於欄位區分性很差的情況,在加索引的時候就應該進行評估。如果區分性很差,這個索引根本就沒必要加。
區分性很差是什麼意思呢,舉幾個例子,比如:
進一步的,那如果不符合上面所有的索引失效的情況,但是 mysql 還是不使用對應的索引,是為啥呢?
這個跟 mysql 的 sql 優化有關,mysql 會在 sql 優化的時候自己選擇合適的索引,很可能是 mysql 自己的選擇演演算法算出來使用這個索引不會提升效能,所以就放棄了。
這種情況,可以使用 force index 關鍵字強制使用索引(建議修改前先實驗一下,是不是真的會提升查詢效率):
select name,code from student force index(XXXXXX) where name = '天才'
其中 xxxx 是索引名。
我把 join 過多和子查詢過多放在一起說了。一般來說,不建議使用子查詢,可以把子查詢改成 join 來優化。同時,join 關聯的表也不宜過多,一般來說 2-3 張表還是合適的。
具體關聯幾張表比較安全是需要具體問題具體分析的,如果各個表的資料量都很少,幾百條几千條,那麼關聯的表的可以適當多一些,反之則需要少一些。
另外需要提到的是,在大多數情況下 join 是在記憶體裡做的,如果匹配的量比較小,或者 join_buffer 設定的比較大,速度也不會很慢。
但是,當 join 的資料量比較大的時候,mysql 會採用在硬碟上建立臨時表的方式進行多張表的關聯匹配,這種顯然效率就極低,本來磁碟的 IO 就不快,還要關聯。
一般遇到這種情況的時候就建議從程式碼層面進行拆分,在業務層先查詢一張表的資料,然後以關聯欄位作為條件查詢關聯表形成 map,然後在業務層進行資料的拼裝。
一般來說,索引建立正確的話,會比 join 快很多,畢竟記憶體裡拼接資料要比網路傳輸和硬碟 IO 快得多。
這種問題,如果只看程式碼的話不太容易排查,最好結合監控和資料庫紀錄檔一起分析。如果一個查詢有 in,in 的條件加了合適的索引,這個時候的 sql 還是比較慢就可以高度懷疑是 in 的元素過多。
一旦排查出來是這個問題,解決起來也比較容易,不過是把元素分個組,每組查一次。想再快的話,可以再引入多執行緒。
進一步的,如果in的元素量大到一定程度還是快不起來,這種最好還是有個限制:
select id from student where id in (1,2,3 ...... 1000) limit 200
//當然了,最好是在程式碼層面做個限制: if (ids.size() > 200) { throw new Exception("單次查詢資料量不能超過200"); }
這種問題,單純程式碼的修修補補一般就解決不了了,需要變動整個的資料儲存架構。或者是對底層 mysql 分表或分庫+分表;或者就是直接變更底層資料庫,把 mysql 轉換成專門為處理巨量資料設計的資料庫。
這種工作是個系統工程,需要嚴密的調研、方案設計、方案評審、效能評估、開發、測試、聯調,同時需要設計嚴密的資料遷移方案、回滾方案、降級措施、故障處理預案。
除了以上團隊內部的工作,還可能有跨系統溝通的工作,畢竟做了重大變更,下游系統的呼叫介面的方式有可能會需要變化。
出於篇幅的考慮,這個不再展開了,筆者有幸完整參與了一次億級別資料量的資料庫分表工作,對整個過程的複雜性深有體會,後續有機會也會分享出來。
這種情況,一般都回圈呼叫同一段程式碼,每次迴圈的邏輯一致,前後不關聯。
比如說,我們要初始化一個列表,預置 12 個月的資料給前端:
List<Model> list = new ArrayList<>(); for(int i = 0 ; i < 12 ; i ++) { Model model = calOneMonthData(i); // 計算某個月的資料,邏輯比較複雜,難以批次計算,效率也無法很高 list.add(model); }
這種顯然每個月的資料計算相互都是獨立的,我們完全可以採用多執行緒方式進行:
// 建立一個執行緒池,注意要放在外面,不要每次執行程式碼就建立一個,具體執行緒池的使用就不展開了 public static ExecutorService commonThreadPool = new ThreadPoolExecutor(5, 5, 300L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10), commonThreadFactory, new ThreadPoolExecutor.DiscardPolicy()); // 開始多執行緒呼叫 List<Future<Model>> futures = new ArrayList<>(); for(int i = 0 ; i < 12 ; i ++) { Future<Model> future = commonThreadPool.submit(() -> calOneMonthData(i);); futures.add(future); } // 獲取結果 List<Model> list = new ArrayList<>(); try { for (int i = 0 ; i < futures.size() ; i ++) { list.add(futures.get(i).get()); } } catch (Exception e) { LOGGER.error("出現錯誤:", e); }
如果不是類似上面迴圈呼叫,而是一次次的順序呼叫,而且呼叫之間沒有結果上的依賴,那麼也可以用多執行緒的方式進行,例如:
程式碼上看:
A a = doA(); B b = doB(); C c = doC(a, b); D d = doD(c); E e = doE(c); return doResult(d, e);
那麼可用 CompletableFuture 解決:
CompletableFuture<A> futureA = CompletableFuture.supplyAsync(() -> doA()); CompletableFuture<B> futureB = CompletableFuture.supplyAsync(() -> doB()); CompletableFuture.allOf(futureA,futureB) // 等a b 兩個任務都執行完成 C c = doC(futureA.join(), futureB.join()); CompletableFuture<D> futureD = CompletableFuture.supplyAsync(() -> doD(c)); CompletableFuture<E> futureE = CompletableFuture.supplyAsync(() -> doE(c)); CompletableFuture.allOf(futureD,futureE) // 等d e兩個任務都執行完成 return doResult(futureD.join(),futureE.join());
這樣 A B 兩個邏輯可以並行執行,D E 兩個邏輯可以並行執行,最大執行時間取決於哪個邏輯更慢。
有的時候,即使我們使用了執行緒池讓任務並行處理,介面的執行效率仍然不夠快,這種情況可能是怎麼回事呢?
這種情況首先應該懷疑是不是執行緒池設計的不合理。我覺得這裡有必要回顧一下執行緒池的三個重要引數:核心執行緒數、最大執行緒數、等待佇列。
這三個引數是怎麼打配合的呢?當執行緒池建立的時候,如果不預熱執行緒池,則執行緒池中執行緒為 0。當有任務提交到執行緒池,則開始建立核心執行緒。
當核心執行緒全部被佔滿,如果再有任務到達,則讓任務進入等待佇列開始等待。
如果佇列也被佔滿,則開始建立非核心執行緒執行。
如果執行緒總數達到最大執行緒數,還是有任務到達,則開始根據執行緒池拋棄規則開始拋棄。
那麼這個執行原理與介面執行時間有什麼關係呢?
在排查的時候,只要找到了問題出現的原因,那麼解決方式也就清楚了,無非就是調整執行緒池引數,按照業務拆分執行緒池等等。
鎖設計不合理一般有兩種:鎖型別使用不合理 or 鎖過粗。
鎖型別使用不合理的典型場景就是讀寫鎖。也就是說,讀是可以共用的,但是讀的時候不能對共用變數寫;而在寫的時候,讀寫都不能進行。
在可以加讀寫鎖的時候,如果我們加成了互斥鎖,那麼在讀遠遠多於寫的場景下,效率會極大降低。
鎖過粗則是另一種常見的鎖設計不合理的情況,如果我們把鎖包裹的範圍過大,則加鎖時間會過長,例如:
public synchronized void doSome() { File f = calData(); uploadToS3(f); sendSuccessMessage(); }
這塊邏輯一共處理了三部分,計算、上傳結果、傳送訊息。顯然上傳結果和傳送訊息是完全可以不加鎖的,因為這個跟共用變數根本不沾邊。
因此完全可以改成:
public void doSome() { File f = null; synchronized(this) { f = calData(); } uploadToS3(f); sendSuccessMessage(); }
造成這個問題的原因非常多,筆者就遇到了定時任務過大引起 fullGC,程式碼存線上程洩露引起 RSS 記憶體佔用過高進而引起機器重啟等待諸多原因。
需要結合各種監控和具體場景具體分析,進而進行大事務拆分、重新規劃執行緒池等等工作。
萬金油這個形容詞是從我們單位某位老師那裡學來的,但是筆者覺得非常貼切。這些萬金油解決方式往往能解決大部分的介面緩慢的問題,而且也往往是我們解決介面效率問題的最終解決方案。
當我們實在是沒有辦法排查出問題,或者實在是沒有優化空間的時候,可以嘗試這種萬金油的方式。
快取是一種空間換取時間的解決方案,是在高效能儲存媒介上(例如:記憶體、SSD 硬碟等)儲存一份資料備份。
當有請求打到伺服器的時候,優先從快取中讀取資料。如果讀取不到,則再從硬碟或通過網路獲取資料。
由於記憶體或 SSD 相比硬碟或網路 IO 的效率高很多,則介面響應速度會變快非常多。快取適合於應用在資料讀遠遠大於資料寫,且資料變化不頻繁的場景中。
從技術選型上看,有這些:
當然,memcached 現在用的很少了,因為相比於 redis 他不佔優勢。tair 則是阿里開發的一個分散式快取中介軟體,他的優勢是理論上可以在不停服的情況下,動態擴充套件儲存容量,適用於巨量資料量快取儲存。
相比於單機 redis 快取當然有優勢,而他與可延伸 Redis 叢集的對比則需要進一步調研。
進一步的,當前快取的模型一般都是 key-value 模型。如何設計 key 以提高快取的命中率是個大學問,好的 key 設計和壞的 key 設計所提升的效能差別非常大。
而且,key 設計是沒有一定之規的,需要結合具體的業務場景去分析。各個大公司分享出來的相關文章,快取設計基本上是最大篇幅。
這種方式往往是業務上的解決方式,在訂單或者付款系統中應用的比較多。
舉個例子:當我們付款的時候,需要呼叫一個專門的付款系統介面,該系統經過一系列驗證、儲存工作後還要呼叫銀行介面以執行付款。
由於付款這個動作要求十分嚴謹,銀行側介面執行可能比較緩慢,進而拖累整個付款介面效能。
這個時候我們就可以採用 fast success 的方式:當必要的校驗和儲存完成後,立即返回 success,同時告訴呼叫方一箇中間態“付款中”。
而後呼叫銀行介面,當獲得支付結果後再呼叫上游系統的回撥介面返回付款的最終結果“成果”or“失敗”。這樣就可以非同步執行付款過程,提升付款介面效率。
當然,為了防止多業務方接入的時候回撥介面不統一,可以把結果拋進 kafka,讓呼叫方監聽自己的結果。
以上就是java介面效能優化技巧的詳細內容,更多關於java介面效能優化的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45