java性能調優實戰學習筆記

這是極客時間專欄《java性能調優實戰》的部分學習筆記,個人感覺這個專欄內容不深,適合初學者,我只看了編程性能調優和數據庫性能調優兩塊,其他的暫時不打算看了,後續有時間再看吧
有任何問題可在我個人博客留言,或者直接在此處留言即可

如何制定性能調優標準

有哪些參考因素可以體現系統的性能?

在我們瞭解性能指標之前,我們先來了解下哪些計算機資源會成爲系統的性能瓶頸。

CPU 、內存、磁盤IO、網絡、異常、數據庫、鎖競爭

  • CPU:有的應用需要大量計算,他們會長時間、不間斷地佔用 CPU 資源,導致其他資源無法爭奪到 CPU 而響應緩慢,從而帶來系統性能問題。例如,代碼遞歸導致的無限循環,正則表達式引起的回溯,JVM 頻繁的 FULL GC,以及多線程編程造成的大量上下文切換等,這些都有可能導致 CPU 資源繁忙。
  • 內存:Java 程序一般通過 JVM 對內存進行分配管理,主要是用 JVM 中的堆內存來存儲 Java 創建的對象。系統堆內存的讀寫速度非常快,所以基本不存在讀寫性能瓶頸。但是由於內存成本要比磁盤高,相比磁盤,內存的存儲空間又非常有限。所以當內存空間被佔滿,對象無法回收時,就會導致內存溢出、內存泄露等問題。
  • 磁盤 I/O:磁盤相比內存來說,存儲空間要大很多,但磁盤 I/O 讀寫的速度要比內存慢,雖然目前引入的 SSD 固態硬盤已經有所優化,但仍然無法與內存的讀寫速度相提並論。
  • 網絡:網絡對於系統性能來說,也起着至關重要的作用。如果你購買過雲服務,一定經歷過,選擇網絡帶寬大小這一環節。帶寬過低的話,對於傳輸數據比較大,或者是併發量比較大的系統,網絡就很容易成爲性能瓶頸。
  • 異常:Java 應用中,拋出異常需要構建異常棧,對異常進行捕獲和處理,這個過程非常消耗系統性能。如果在高併發的情況下引發異常,持續地進行異常處理,那麼系統的性能就會明顯地受到影響。
  • 數據庫:大部分系統都會用到數據庫,而數據庫的操作往往是涉及到磁盤 I/O 的讀寫。大量的數據庫讀寫操作,會導致磁盤 I/O 性能瓶頸,進而導致數據庫操作的延遲性。對於有大量數據庫讀寫操作的系統來說,數據庫的性能優化是整個系統的核心。
  • 鎖競爭:在併發編程中,我們經常會需要多個線程,共享讀寫操作同一個資源,這個時候爲了保持數據的原子性(即保證這個共享資源在一個線程寫的時候,不被另一個線程修改),我們就會用到鎖。鎖的使用可能會帶來上下文切換,從而給系統帶來性能開銷。JDK1.6 之後,Java 爲了降低鎖競爭帶來的上下文切換,對 JVM 內部鎖已經做了多次優化,例如,新增了偏向鎖、自旋鎖、輕量級鎖、鎖粗化、鎖消除等。而如何合理地使用鎖資源,優化鎖資源,就需要你瞭解更多的操作系統知識、Java 多線程編程基礎,積累項目經驗,並結合實際場景去處理相關問題。

可以用下面幾個指標,來衡量一般系統的性能 :

響應時間

  • 數據庫響應時間:數據庫操作所消耗的時間,往往是整個請求鏈中最耗時的;
  • 服務端響應時間:服務端包括 Nginx 分發的請求所消耗的時間以及服務端程序執行所消耗的時間;
  • 網絡響應時間:這是網絡傳輸時,網絡硬件需要對傳輸的請求進行解析等操作所消耗的時間;
  • 客戶端響應時間:對於普通的 Web、App 客戶端來說,消耗時間是可以忽略不計的,但如果你的客戶端嵌入了大量的邏輯處理,消耗的時間就有可能變長,從而成爲系統的瓶頸。

吞吐量

在測試中,我們往往會比較注重系統接口的 TPS(每秒事務處理量),因爲 TPS 體現了接口的性能,TPS 越大,性能越好。在系統中,我們也可以把吞吐量自下而上地分爲兩種:磁盤吞吐量和網絡吞吐量。

我們先來看磁盤吞吐量,磁盤性能有兩個關鍵衡量指標。

一種是IOPS(Input/Output Per Second),即每秒的輸入輸出量(或讀寫次數),這種是指單位時間內系統能處理的 I/O 請求數量,I/O 請求通常爲讀或寫數據操作請求,關注的是隨機讀寫性能。適應於隨機讀寫頻繁的應用,如小文件存儲(圖片)、OLTP 數據庫、郵件服務器。

另一種是數據吞吐量,這種是指單位時間內可以成功傳輸的數據量。對於大量順序讀寫頻繁的應用,傳輸大量連續數據,例如,電視臺的視頻編輯、視頻點播 VOD(Video On Demand),數據吞吐量則是關鍵衡量指標。

網絡吞吐量這個是指網絡傳輸時沒有幀丟失的情況下,設備能夠接受的最大數據速率。網絡吞吐量不僅僅跟帶寬有關係,還跟 CPU 的處理能力、網卡、防火牆、外部接口以及 I/O 等緊密關聯。而吞吐量的大小主要由網卡的處理能力、內部程序算法以及帶寬大小決定。

計算機資源分配使用率

通常由 CPU 佔用率、內存使用率、磁盤 I/O、網絡 I/O 來表示資源使用率。這幾個參數好比一個木桶,如果其中任何一塊木板出現短板,任何一項分配不合理,對整個系統性能的影響都是毀滅性的。

負載承受能力

當系統壓力上升時,你可以觀察,系統響應時間的上升曲線是否平緩。這項指標能直觀地反饋給你,系統所能承受的負載壓力極限。例如,當你對系統進行壓測時,系統的響應時間會隨着系統併發數的增加而延長,直到系統無法處理這麼多請求,拋出大量錯誤時,就到了極限。

如何制定性能調優策略

img-aNHai3Bg-1590596845194

慎重使用正則表達式

正則表達式引擎

正則表達式是一個用正則符號寫出的公式,程序對這個公式進行語法分析,建立一個語法分析樹,再根據這個分析樹結合正則表達式的引擎生成執行程序(這個執行程序我們把它稱作狀態機,也叫狀態自動機),用於字符匹配。

而這裏的正則表達式引擎就是一套核心算法,用於建立狀態機。

目前實現正則表達式引擎的方式有兩種:DFA 自動機(Deterministic Final Automaton 確定有限狀態自動機)和 NFA 自動機(Non deterministic Finite Automaton 非確定有限狀態自動機)。

對比來看,構造 DFA 自動機的代價遠大於 NFA 自動機,但 DFA 自動機的執行效率高於 NFA 自動機。

假設一個字符串的長度是 n,如果用 DFA 自動機作爲正則表達式引擎,則匹配的時間複雜度爲 O(n);如果用 NFA 自動機作爲正則表達式引擎,由於 NFA 自動機在匹配過程中存在大量的分支和回溯,假設 NFA 的狀態數爲 s,則該匹配算法的時間複雜度爲 O(ns)。

NFA 自動機的優勢是支持更多功能。例如,捕獲 group、環視、佔有優先量詞等高級功能。這些功能都是基於子表達式獨立進行匹配,因此在編程語言裏,使用的正則表達式庫都是基於 NFA 實現的。

用 NFA 自動機實現的比較複雜的正則表達式,在匹配過程中經常會引起回溯問題。大量的回溯會長時間地佔用 CPU,從而帶來系統性能開銷。

舉例說明 :

text=“abbc”

regex=“ab{1,3}c”

這個例子,匹配目的比較簡單。匹配以 a 開頭,以 c 結尾,中間有 1-3 個 b 字符的字符串。NFA 自動機對其解析的過程是這樣的:首先,讀取正則表達式第一個匹配符 a 和字符串第一個字符 a 進行比較,a 對 a,匹配。

然後,讀取正則表達式第二個匹配符 b{1,3} 和字符串的第二個字符 b 進行比較,匹配。但因爲 b{1,3} 表示 1-3 個 b 字符串,NFA 自動機又具有貪婪特性,所以此時不會繼續讀取正則表達式的下一個匹配符,而是依舊使用 b{1,3} 和字符串的第三個字符 b 進行比較,結果還是匹配。

接着繼續使用 b{1,3} 和字符串的第四個字符 c 進行比較,發現不匹配了,此時就會發生回溯,已經讀取的字符串第四個字符 c 將被吐出去,指針回到第三個字符 b 的位置。
img-4Y4zrUZt-1590596845200

那麼發生回溯以後,匹配過程怎麼繼續呢?程序會讀取正則表達式的下一個匹配符 c,和字符串中的第四個字符 c 進行比較,結果匹配,結束。

如何減少回溯問題?

既然回溯會給系統帶來性能開銷,那我們如何應對呢?如果你有仔細看上面那個案例的話,你會發現 NFA 自動機的貪婪特性就是導火索,這和正則表達式的匹配模式息息相關。

**貪婪模式(Greedy)**顧名思義,就是在數量匹配中,如果單獨使用+、 ? 、* 或{min,max} 等量詞,正則表達式會匹配儘可能多的內容。例如,上邊那個例子:

**懶惰模式(Reluctant)**在該模式下,正則表達式會盡可能少地重複匹配字符。如果匹配成功,它會繼續匹配剩餘的字符串。

例如,在上面例子的字符後面加一個“?”,就可以開啓懶惰模式。

text=“abc”

regex=“ab{1,3}?c”

匹配結果是“abc”,該模式下 NFA 自動機首先選擇最小的匹配範圍,即匹配 1 個 b 字符,因此就避免了回溯問題。

懶惰模式是無法完全避免回溯的,我們再通過一個例子來了解下懶惰模式在什麼情況下會發生回溯問題。

text=“abbc”

regex=“ab{1,3}?c”

以上匹配結果依然是成功的,這又是爲什麼呢?我們可以通過懶惰模式的匹配過程來了解下原因。

首先,讀取正則表達式第一個匹配符 a 和字符串第一個字符 a 進行比較,a 對 a,匹配。然後,讀取正則表達式第二個匹配符 b{1,3} 和字符串的第二個字符 b 進行比較,匹配。

其次,由於懶惰模式下,正則表達式會盡可能少地重複匹配字符,匹配字符串中的下一個匹配字符 b 不會繼續與 b{1,3}進行匹配,轉而匹配正則表達式中的下一個字符 c。

此時你會發現匹配字符 b 與正則表達式中的字符 c 是不匹配的,這個時候會發生一次回溯,這次的回溯與貪婪模式中的回溯剛好相反,懶惰模式的回溯是回溯正則表達式中一個匹配字符,與上一個字符再進行匹配。如果匹配,則將匹配字符串的下一個字符和正則表達式的下一個字符。

獨佔模式(Possessive)

同貪婪模式一樣,獨佔模式一樣會最大限度地匹配更多內容;不同的是,在獨佔模式下,匹配失敗就會結束匹配,不會發生回溯問題。

還是上邊的例子,在字符後面加一個“+”,就可以開啓獨佔模式。

text=“abbc”

regex=“ab{1,3}+bc”

結果是不匹配,結束匹配,不會發生回溯問題。

同樣,獨佔模式也不能避免回溯的發生,我們再拿最開始的這個例子來分析下:

text=“abbc”

regex=“ab{1,3}+c”

結果是匹配的,這是因爲與貪婪模式一樣,獨佔模式一樣會最大限度地匹配更多內容,即匹配完所有的 b 之後,再去匹配 c,則匹配成功了。

在很多情況下使用懶惰模式和獨佔模式可以減少回溯的發生。

正則表達式的優化

  1. 少用貪婪模式,多用獨佔模式

    貪婪模式會引起回溯問題,我們可以使用獨佔模式來避免回溯。前面詳解過了,這裏我就不再解釋了。

  2. 減少分支選擇

    分支選擇類型“(X|Y|Z)”的正則表達式會降低性能,我們在開發的時候要儘量減少使用。如果一定要用,我們可以通過以下幾種方式來優化:

    首先,我們需要考慮選擇的順序,將比較常用的選擇項放在前面,使它們可以較快地被匹配;

    其次,我們可以嘗試提取共用模式,例如,將“(abcd|abef)”替換爲“ab(cd|ef)”,後者匹配速度較快,因爲 NFA 自動機會嘗試匹配 ab,如果沒有找到,就不會再嘗試任何選項;

    最後,如果是簡單的分支選擇類型,我們可以用三次 index 代替“(X|Y|Z)”,如果測試的話,你就會發現三次 index 的效率要比“(X|Y|Z)”高出一些。

  3. 減少捕獲嵌套

    捕獲組是指把正則表達式中,子表達式匹配的內容保存到以數字編號或顯式命名的數組中,方便後面引用。一般一個 () 就是一個捕獲組,捕獲組可以進行嵌套。

    非捕獲組則是指參與匹配卻不進行分組編號的捕獲組,其表達式一般由(?:exp)組成。

    在正則表達式中,每個捕獲組都有一個編號,編號 0 代表整個匹配到的內容。我們可以看下面的例子:

    public static void main( String[] args )
    {
      String text = "<input high=\"20\" weight=\"70\">test</input>";
      String reg="(<input.*?>)(.*?)(</input>)";
      Pattern p = Pattern.compile(reg);
      Matcher m = p.matcher(text);
      while(m.find()) {
        System.out.println(m.group(0));//整個匹配到的內容
        System.out.println(m.group(1));//(<input.*?>)
        System.out.println(m.group(2));//(.*?)
        System.out.println(m.group(3));//(</input>)
      }
    }
    

    運行結果:

    <input high=\"20\" weight=\"70\">test</input>
    <input high=\"20\" weight=\"70\">
    test
    </input>
    

    如果你並不需要獲取某一個分組內的文本,那麼就使用非捕獲分組。例如,使用“(?:X)”代替“(X)”,我們再看下面的例子:

    public static void main( String[] args )
    {
      String text = "<input high=\"20\" weight=\"70\">test</input>";
      String reg="(?:<input.*?>)(.*?)(?:</input>)";
      Pattern p = Pattern.compile(reg);
      Matcher m = p.matcher(text);
      while(m.find()) {
        System.out.println(m.group(0));//整個匹配到的內容
        System.out.println(m.group(1));//(.*?)
      }
    }
    

    運行結果:

    <input high=\"20\" weight=\"70\">test</input>
    test
    

    綜上可知:減少不需要獲取的分組,可以提高正則表達式的性能。

正則表達式雖然小,卻有着強大的匹配功能。我們經常用到它,比如,註冊頁面手機號或郵箱的校驗。

但很多時候,我們又會因爲它小而忽略它的使用規則,測試用例中又沒有覆蓋到一些特殊用例,不乏上線就中招的情況發生。

如果使用正則表達式能使你的代碼簡潔方便,那麼在做好性能排查的前提下,可以去使用;如果不能,那麼正則表達式能不用就不用,以此避免造成更多的性能問題。

ArrayList和LinkedList

我們在查看 ArrayList 的實現類源碼時,你會發現對象數組 elementData 使用了 transient 修飾,我們知道 transient 關鍵字修飾該屬性,則表示該屬性不會被序列化,然而我們並沒有看到文檔中說明 ArrayList 不能被序列化,這是爲什麼?

ArrayList 屬性主要由數組長度 size、對象數組 elementData、初始化容量 default_capacity 等組成, 其中初始化容量默認大小爲 10。

 //默認初始化容量
    private static final int DEFAULT_CAPACITY = 10;
    //對象數組
    transient Object[] elementData; 
    //數組長度
    private int size;

從 ArrayList 屬性來看,它沒有被任何的多線程關鍵字修飾,但 elementData 被關鍵字 transient 修飾了。

由於 ArrayList 的數組是基於動態擴增的,所以並不是所有被分配的內存空間都存儲了數據。如果採用外部序列化法實現數組的序列化,會序列化整個數組。ArrayList 爲了避免這些沒有存儲數據的內存空間被序列化,內部提供了兩個私有方法 writeObject 以及 readObject 來自我完成序列化與反序列化,從而在序列化與反序列化數組時節省了空間和時間。

因此使用 transient 修飾數組,是防止對象數組被其他外部方法序列化

Stream如何提高遍歷集合效率

官方將 Stream 中的操作分爲兩大類:中間操作(Intermediate operations)和終結操作(Terminal operations)。中間操作只對操作進行了記錄,即只會返回一個流,不會進行計算操作,而終結操作是實現了計算操作。

中間操作又可以分爲無狀態(Stateless)與有狀態(Stateful)操作,前者是指元素的處理不受之前元素的影響,後者是指該操作只有拿到所有元素之後才能繼續下去。

終結操作又可以分爲短路(Short-circuiting)與非短路(Unshort-circuiting)操作,前者是指遇到某些符合條件的元素就可以得到最終結果,後者是指必須處理完所有元素才能得到最終結果。操作分類詳情如下圖所示:
img-HJ6c83OJ-1590596845204

我們通常還會將中間操作稱爲懶操作,也正是由這種懶操作結合終結操作、數據源構成的處理管道(Pipeline),實現了 Stream 的高效。

在循環迭代次數較少的情況下,常規的迭代方式性能反而更好;在單核 CPU 服務器配置環境中,也是常規迭代方式更有優勢;而在大數據循環迭代中,如果服務器是多核 CPU 的情況下,Stream 的並行迭代優勢明顯。所以我們在平時處理大數據的集合時,應該儘量考慮將應用部署在多核 CPU 環境下,並且使用 Stream 的並行迭代方式進行處理。

Hashmap

HashMap 擴容優化 :

在 JDK1.7 中,HashMap 整個擴容過程就是分別取出數組元素,一般該元素是最後一個放入鏈表中的元素,然後遍歷以該元素爲頭的單向鏈表元素,依據每個被遍歷元素的 hash 值計算其在新數組中的下標,然後進行交換。這樣的擴容方式會將原來哈希衝突的單向鏈表尾部變成擴容後單向鏈表的頭部。

而在 JDK 1.8 中,HashMap 對擴容操作做了優化。由於擴容數組的長度是 2 倍關係,所以對於假設初始 tableSize = 4 要擴容到 8 來說就是 0100 到 1000 的變化(左移一位就是 2 倍),在擴容中只用判斷原來的 hash 值和左移動的一位(newtable 的值)按位與操作是 0 或 1 就行,0 的話索引不變,1 的話索引變成原索引加上擴容前數組。

之所以能通過這種“與運算“來重新分配索引,是因爲 hash 值本來就是隨機的,而 hash 按位與上 newTable 得到的 0(擴容前的索引位置)和 1(擴容前索引位置加上擴容前數組長度的數值索引處)就是隨機的,所以擴容的過程就能把之前哈希衝突的元素再隨機分佈到不同的索引中去。

實際應用中,我們設置初始容量,一般得是 2 的整數次冪。你知道原因嗎?

1)通過將 Key 的 hash 值與 length-1 進行 & 運算,實現了當前 Key 的定位,2 的冪次方可以減少衝突(碰撞)的次數,提高 HashMap 查詢效率;
2)如果 length 爲 2 的次冪,則 length-1 轉化爲二進制必定是 11111…… 的形式,在於 h 的二進制與操作效率會非常的快,而且空間不浪費;如果 length 不是 2 的次冪,比如 length 爲 15,則 length-1 爲 14,對應的二進制爲 1110,在於 h 與操作,最後一位都爲 0,而 0001,0011,0101,1001,1011,0111,1101 這幾個位置永遠都不能存放元素了,空間浪費相當大,更糟的是這種情況中,數組可以使用的位置比數組長度小了很多,這意味着進一步增加了碰撞的機率,減慢了查詢的效率!這樣就會造成空間的浪費。

MySQL調優之SQL語句:如何寫出高性能SQL語句?

慢 SQL 語句的幾種常見誘因

  • 無索引、索引失效導致慢查詢

  • 鎖等待

我們常用的存儲引擎有 InnoDB 和 MyISAM,前者支持行鎖和表鎖,後者只支持表鎖。

如果數據庫操作是基於表鎖實現的,試想下,如果一張訂單表在更新時,需要鎖住整張表,那麼其它大量數據庫操作(包括查詢)都將處於等待狀態,這將嚴重影響到系統的併發性能。

這時,InnoDB 存儲引擎支持的行鎖更適合高併發場景。但在使用 InnoDB 存儲引擎時,我們要特別注意行鎖升級爲表鎖的可能。在批量更新操作時,行鎖就很可能會升級爲表鎖。

MySQL 認爲如果對一張表使用大量行鎖,會導致事務執行效率下降,從而可能造成其它事務長時間鎖等待和更多的鎖衝突問題發生,致使性能嚴重下降,所以 MySQL 會將行鎖升級爲表鎖。還有,行鎖是基於索引加的鎖,如果我們在更新操作時,條件索引失效,那麼行鎖也會升級爲表鎖。

因此,基於表鎖的數據庫操作,會導致 SQL 阻塞等待,從而影響執行速度。在一些更新操作(insert\update\delete)大於或等於讀操作的情況下,MySQL 不建議使用 MyISAM 存儲引擎。

除了鎖升級之外,行鎖相對錶鎖來說,雖然粒度更細,併發能力提升了,但也帶來了新的問題,那就是死鎖。因此,在使用行鎖時,我們要注意避免死鎖。關於死鎖,我還會在第 35 講中詳解。

  • 不恰當的 SQL 語句

優化 SQL 語句的步驟

通常,我們在執行一條 SQL 語句時,要想知道這個 SQL 先後查詢了哪些表,是否使用了索引,這些數據從哪裏獲取到,獲取到數據遍歷了多少行數據等等,我們可以通過 EXPLAIN 命令來查看這些執行信息。這些執行信息被統稱爲執行計劃

通過 EXPLAIN 分析 SQL 執行計劃

假設現在我們使用 EXPLAIN 命令查看當前 SQL 是否使用了索引,先通過 SQL EXPLAIN 導出相應的執行計劃如下:
img-josLIzGQ-1590596845205

  • id:每個執行計劃都有一個 id,如果是一個聯合查詢,這裏還將有多個 id。
  • select_type:表示 SELECT 查詢類型,常見的有 SIMPLE(普通查詢,即沒有聯合查詢、子查詢)、PRIMARY(主查詢)、UNION(UNION 中後面的查詢)、SUBQUERY(子查詢)等。
  • table:當前執行計劃查詢的表,如果給表起別名了,則顯示別名信息。
  • partitions:訪問的分區表信息。
  • type:表示從表中查詢到行所執行的方式,查詢方式是 SQL 優化中一個很重要的指標,結果值從好到差依次是:system > const > eq_ref > ref > range > index > ALL

system/const:表中只有一行數據匹配,此時根據索引查詢一次就能找到對應的數據。
img-Mhs3nURR-1590596845207

eq_ref:使用唯一索引掃描,常見於多表連接中使用主鍵和唯一索引作爲關聯條件。
img-MyBfZqNl-1590596845208

ref:非唯一索引掃描,還可見於唯一索引最左原則匹配掃描。
img-9bSj6Vgz-1590597247652
range:索引範圍掃描,比如,<,>,between等操作。
img-BCzVZrbf-1590596845210
index:索引全表掃描,此時遍歷整個索引樹。
img-ykHfSi3W-1590596845211

ALL:表示全表掃描,需要遍歷全表來找到對應的行。

possible_keys:可能使用到的索引。

key:實際使用到的索引。

key_len:當前使用的索引的長度。

ref:關聯 id 等信息。

rows:查找到記錄所掃描的行數。

filtered:查找到所需記錄佔總掃描記錄數的比例。

Extra:額外的信息。

通過 Show Profile 分析 SQL 執行性能

上述通過 EXPLAIN 分析執行計劃,僅僅是停留在分析 SQL 的外部的執行情況,如果我們想要深入到 MySQL 內核中,從執行線程的狀態和時間來分析的話,這個時候我們就可以選擇 Profile。

Profile 除了可以分析執行線程的狀態和時間,還支持進一步選擇 ALL、CPU、MEMORY、BLOCK IO、CONTEXT SWITCHES等類型來查詢 SQL 語句在不同系統資源上所消耗的時間。以下是相關命令的註釋:

SHOW PROFILE [type [, type] ... ]
[FOR QUERY n]
[LIMIT row_count [OFFSET offset]]

type參數:
| ALL:顯示所有開銷信息
| BLOCK IO:阻塞的輸入輸出次數
| CONTEXT SWITCHES:上下文切換相關開銷信息
| CPU:顯示CPU的相關開銷信息 
| IPC:接收和發送消息的相關開銷信息
| MEMORY :顯示內存相關的開銷,目前無用
| PAGE FAULTS :顯示頁面錯誤相關開銷信息
| SOURCE :列出相應操作對應的函數名及其在源碼中的調用位置(行數) 
| SWAPS:顯示swap交換次數的相關開銷信息

可以通過 select @@have_profiling 查詢是否支持profile功能

Show Profiles 只顯示最近發給服務器的 SQL 語句,默認情況下是記錄最近已執行的 15 條記錄,我們可以重新設置profiling_history_size增大該存儲記錄,最大值爲 100。
img-gxnQXHeJ-1590596845212

獲取到 Query_ID 之後,我們再通過 Show Profile for Query ID語句,就能夠查看到對應 Query_ID 的 SQL 語句在執行過程中線程的每個狀態所消耗的時間了:
img-REtSwZot-1590596845213

通過以上分析可知:SELECT COUNT(*) FROM order; SQL 語句在 Sending data 狀態所消耗的時間最長,這是因爲在該狀態下,MySQL 線程開始讀取數據並返回到客戶端,此時有大量磁盤 I/O 操作。

常用的 SQL 優化

優化分頁查詢

通常我們是使用 + 合適的 order by 來實現分頁查詢,這種實現方式在沒有任何索引條件支持的情況下,需要做大量的文件排序操作(file sort),性能將會非常得糟糕。如果有對應的索引,通常剛開始的分頁查詢效率會比較理想,但越往後,分頁查詢的性能就越差。

這是因爲我們在使用 LIMIT 的時候,偏移量 M 在分頁越靠後的時候,值就越大,數據庫檢索的數據也就越多。例如 LIMIT 10000,10 這樣的查詢,數據庫需要查詢 10010 條記錄,最後返回 10 條記錄。也就是說將會有 10000 條記錄被查詢出來沒有被使用到。

  • 利用子查詢優化分頁查詢

    以上分頁查詢的問題在於,我們查詢獲取的 10020 行數據結果都返回給我們了,我們能否先查詢出所需要的 20 行數據中的最小 ID 值,然後通過偏移量返回所需要的 20 行數據給我們呢?我們可以通過索引覆蓋掃描,使用子查詢的方式來實現分頁查詢:

    select * from `demo`.`order` where id> (select id from `demo`.`order` order by order_no limit 10000, 1)  limit 20;
    

img-O7cZC4Q1-1590596845214
img-Q06b0iVn-1590596845216

優化 SELECT COUNT(*)

通常在沒有任何查詢條件下的 COUNT(*),MyISAM 的查詢速度要明顯快於 InnoDB。這是因爲 MyISAM 存儲引擎記錄的是整個表的行數,在 COUNT(*) 查詢操作時無需遍歷表計算,直接獲取該值即可。而在 InnoDB 存儲引擎中就需要掃描表來統計具體的行數。而當帶上 where 條件語句之後,MyISAM 跟 InnoDB 就沒有區別了,它們都需要掃描表來進行行數的統計。

如果對一張大表經常做 SELECT COUNT(*) 操作,這肯定是不明智的。那麼我們該如何對大表的 COUNT() 進行優化呢?

  • 使用近似值

    有時候某些業務場景並不需要返回一個精確的 COUNT 值,此時我們可以使用近似值來代替。我們可以使用 EXPLAIN 對錶進行估算,要知道,執行 EXPLAIN 並不會真正去執行查詢,而是返回一個估算的近似值。

  • 增加彙總統計

    如果需要一個精確的 COUNT 值,我們可以額外新增一個彙總統計表或者緩存字段來統計需要的 COUNT 值,這種方式在新增和刪除時有一定的成本,但卻可以大大提升 COUNT() 的性能。

我們可以打開慢 SQL 配置項,記錄下都有哪些 SQL 超過了預期的最大執行時間。首先,我們可以通過以下命令行查詢是否開啓了記錄慢 SQL 的功能,以及最大的執行時間是多少:

Show variables like 'slow_query%';
Show variables like 'long_query_time';

如果沒有開啓,我們可以通過以下設置來開啓:

set global slow_query_log='ON'; //開啓慢SQL日誌
set global slow_query_log_file='/var/lib/mysql/test-slow.log';//記錄日誌地址
set global long_query_time=1;//最大執行時間

思考題:

假設有一張訂單表 order,主要包含了主鍵訂單編碼 order_no、訂單狀態 status、提交時間 create_time 等列,並且創建了 status 列索引和 create_time 列索引。此時通過創建時間降序獲取狀態爲 1 的訂單編碼,以下是具體實現代碼:

select order_no from order where status =1 order by create_time desc

你知道其中的問題所在嗎?我們又該如何優化?

status和create_time單獨建索引,在查詢時只會遍歷status索引對數據進行過濾,不會用到create_time列索引,將符合條件的數據返回到server層,在server對數據通過快排算法進行排序,Extra列會出現file sort;應該利用索引的有序性,在status和create_time列建立聯合索引,這樣根據status過濾後的數據就是按照create_time排好序的,避免在server層排序

MySQL調優之事務:高併發場景下的數據庫事務調優

InnoDB 中的 RC 和 RR 隔離事務是基於多版本併發控制(MVCC)實現高性能事務。一旦數據被加上排他鎖,其他事務將無法加入共享鎖,且處於阻塞等待狀態,如果一張表有大量的請求,這樣的性能將是無法支持的。

MVCC 對普通的 Select 不加鎖,如果讀取的數據正在執行 Delete 或 Update 操作,這時讀取操作不會等待排它鎖的釋放,而是直接利用 MVCC 讀取該行的數據快照(數據快照是指在該行的之前版本的數據,而數據快照的版本是基於 undo 實現的,undo 是用來做事務回滾的,記錄了回滾的不同版本的行記錄)。MVCC 避免了對數據重複加鎖的過程,大大提高了讀操作的性能。

行鎖的具體實現算法有三種:record lock、gap lock 以及 next-key lock。record lock 是專門對索引項加鎖;gap lock 是對索引項之間的間隙加鎖;next-key lock 則是前面兩種的組合,對索引項以其之間的間隙加鎖。

只在可重複讀或以上隔離級別下的特定操作纔會取得 gap lock 或 next-key lock,在 Select 、Update 和 Delete 時,除了基於唯一索引的查詢之外,其他索引查詢時都會獲取 gap lock 或 next-key lock,即鎖住其掃描的範圍。

優化高併發事務

  • 結合業務場景,使用低級別事務隔離
  • 避免行鎖升級表鎖
  • 控制事務的大小,減少鎖定的資源量和鎖定時間長度

MySQL調優之索引:索引的失效與優化

覆蓋索引優化查詢

假設我們只需要查詢商品的名稱、價格信息,我們有什麼方式來避免回表呢?我們可以建立一個組合索引,即商品編碼、名稱、價格作爲一個組合索引。如果索引中存在這些數據,查詢將不會再次檢索主鍵索引,從而避免回表。

從輔助索引中查詢得到記錄,而不需要通過聚簇索引查詢獲得,MySQL 中將其稱爲覆蓋索引。使用覆蓋索引的好處很明顯,我們不需要查詢出包含整行記錄的所有信息,因此可以減少大量的 I/O 操作。

自增字段作主鍵優化查詢

InnoDB 創建主鍵索引默認爲聚簇索引,數據被存放在了 B+ 樹的葉子節點上。也就是說,同一個葉子節點內的各個數據是按主鍵順序存放的,因此,每當有一條新的數據插入時,數據庫會根據主鍵將其插入到對應的葉子節點中。

如果我們使用自增主鍵,那麼每次插入的新數據就會按順序添加到當前索引節點的位置,不需要移動已有的數據,當頁面寫滿,就會自動開闢一個新頁面。因爲不需要重新移動數據,因此這種插入數據的方法效率非常高。

如果我們使用非自增主鍵,由於每次插入主鍵的索引值都是隨機的,因此每次插入新的數據時,就可能會插入到現有數據頁中間的某個位置,這將不得不移動其它數據來滿足新數據的插入,甚至需要從一個頁面複製數據到另外一個頁面,我們通常將這種情況稱爲頁分裂。頁分裂還有可能會造成大量的內存碎片,導致索引結構不緊湊,從而影響查詢效率。

因此,在使用 InnoDB 存儲引擎時,如果沒有特別的業務需求,建議使用自增字段作爲主鍵。

前綴索引優化

前綴索引顧名思義就是使用某個字段中字符串的前幾個字符建立索引,那我們爲什麼需要使用前綴來建立索引呢?我們知道,索引文件是存儲在磁盤中的,而磁盤中最小分配單元是頁,通常一個頁的默認大小爲 16KB,假設我們建立的索引的每個索引值大小爲 2KB,則在一個頁中,我們能記錄 8 個索引值,假設我們有 8000 行記錄,則需要 1000 個頁來存儲索引。如果我們使用該索引查詢數據,可能需要遍歷大量頁,這顯然會降低查詢效率。減小索引字段大小,可以增加一個頁中存儲的索引項,有效提高索引的查詢速度。在一些大字符串的字段作爲索引時,使用前綴索引可以幫助我們減小索引項的大小。不過,前綴索引是有一定的侷限性的,例如 order by 就無法使用前綴索引,無法把前綴索引用作覆蓋索引。

防止索引失效

在大多數情況下,我們習慣使用默認的 InnoDB 作爲表存儲引擎。在使用 InnoDB 作爲存儲引擎時,創建的索引默認爲 B+ 樹數據結構,如果是主鍵索引,則屬於聚簇索引,非主鍵索引則屬於輔助索引。基於主鍵查詢可以直接獲取到行信息,而基於輔助索引作爲查詢條件,則需要進行回表,然後再通過主鍵索引獲取到數據。如果只是查詢一列或少部分列的信息,我們可以基於覆蓋索引來避免回表。覆蓋索引只需要讀取索引,且由於索引是順序存儲,對於範圍或排序查詢來說,可以極大地極少磁盤 I/O 操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章