1. 程式人生 > 資料庫 >Mysql核心總結

Mysql核心總結

Mysql核心總結

Mysql基本架構

如果要訪問一個Mqsql資料庫,那麼就需要Mysql驅動才能跟Mysql資料庫建立連線,執行各種各樣的SQL語句。

Mysql驅動會在底層跟資料庫建立網路連線,有了網路連線,才能去傳送請求給資料庫伺服器,那麼我們就可以用Java基於這個連線執行各種各樣的SQL語句。

在這裡插入圖片描述

資料庫連線池

由於開發的系統常常是多執行緒的,比如將Java Web系統部署在Tomcat中,那麼Tomcat本身是有多個執行緒來併發的處理同時接收到的多個請求,那麼如果多個請求都去爭搶一個連線去訪問資料庫,那麼肯定會堵塞住,效率很低。

那麼可不可以每個執行緒在訪問資料庫的時候,都去基於Mysql驅動來建立一個新的資料連線,然後執行完SQL語句後,再銷燬呢?

不可以的,這樣確實可以做到多個執行緒同時訪問,但是上百個執行緒併發的頻繁建立和銷燬資料庫連線,是很消耗資源的,且效率也不高。

這時候就需要用到資料庫連線池,也就是在資料庫連線池裡維持多個數據庫連線,讓多個執行緒使用裡面不同的資料庫連線去執行SQL語句,然後執行完之後也不用銷燬,而是放回到池子中,供其他執行緒來使用,這樣一個數據庫連線池的機制,可以解決多個執行緒併發去使用多個數據庫連線的問題,還避免了資料庫頻繁建立銷燬,消耗資源的問題。

那麼對於Mysql來說,要併發連線多個請求,Mysql本身也要建立連線,那麼Mysql也要維護與系統之間的多個連線,而且建立連線時,還要驗證身份和許可權等。

在這裡插入圖片描述

Mysql架構

1.網路連線

在資料庫伺服器的連線池中某個連線接收到了網路請求,這個時候是一個執行緒去進行處理,由一個執行緒來監聽請求以及讀取請求資料。

在這裡插入圖片描述
2.SQL介面

SQL語句是用來簡單化我們去資料庫進行資料增刪改查的操作,如果是由我們自己去底層資料,親子進行資料的增刪改查,那麼這就是一個極度複雜的任務,所以Mysql內部提供了一個元件,就是SQL介面來完成執行SQL語句。

在這裡插入圖片描述
3.查詢解析器

比如來了一個SQL語句:select id,name,age from users where id=1

這時我們要對這個SQL語句進行解析,判斷這條語句是用來做什麼的,去哪裡找,有什麼條件等,這就需要查詢解析器了

這個查詢解析器將上面的SQL語句進行拆解:

  1. 我們要從users表裡進行查詢資料
  2. 查詢id欄位等於1的那行資料
  3. 對查出來的那行資料要提取裡面的id,name,age三個欄位

在這裡插入圖片描述

4.查詢優化器

解析器理解了SQL語句要幹什麼,那麼查詢這個SQL有多個途徑,比如我可以先將所有的資料找出來,然後再篩選出id等於1的,或者我先將所有欄位且id等於1的資料查出來,然後再抽出需要的列等等。

查詢優化器就是要選擇最優的查詢路徑,提高查詢效率。

在這裡插入圖片描述
5.呼叫儲存引擎介面,執行真正的SQL語句

最後非同步就是把查詢優化器選擇的最優查詢路徑,然後把這個計劃交給底層的儲存引擎去執行,然後從記憶體或者磁碟中訪問和存放資料,儲存引擎也分為很多種,InnoDB,MyISAM,Memorry等等,現在Mysql一般是使用InnoDB儲存引擎的。

6.執行器

儲存引擎可以幫助我們去訪問內以及磁碟上的資料,那麼用來呼叫儲存引擎的介面就是執行器。

執行器會根據優化器選擇的執行方案去呼叫儲存引擎的介面按照一定的順序和步驟,把SQL語句執行,比如去users表的第一行,判斷id是否為1,如果不是,那麼久繼續呼叫儲存引擎的介面,去獲取users表的下一行。

基於上面的思路,執行器會根據我們的優化器生成的一套執行計劃,然後不停的呼叫儲存引擎的各種介面去完成SQL語句的執行計劃。

在這裡插入圖片描述

InnoDB架構

1.緩衝池

InnoDB儲存引擎種有一個非常重要的放在記憶體的元件,就是緩衝池,這裡面會緩衝很多的資料,以便於以後在查詢的時候,如果記憶體緩衝池裡有資料,就可以不用去查磁碟了,提高查詢速度。

在這裡插入圖片描述
2.undo log日誌檔案

每次在更新的時候,由於可能會發生事務回滾,那麼就會將更新前的值儲存在一個檔案中,這個就是undo log日誌檔案。

在這裡插入圖片描述
4.更新buffer pool中緩衝資料

當我們把更新的那行記錄從磁碟檔案載入到緩衝池,同時對它加鎖之後,還把更新前的值寫入undo log日誌檔案之後,久可以正式更新這行記錄,更新的時候,先是會更新緩衝池中的記錄,此時這個資料叫做髒資料。

在這裡插入圖片描述
5.Redo Log

按照上面更新完Buffer Pool之後,還沒更新磁碟檔案,這時候為了防止記憶體裡修改過的資料丟失,需要對記憶體所做的修改寫入到一個Redo Log Buffer裡,也是記憶體裡的一個緩衝區,用來存放redo log日誌的,裡面記錄對資料進行了什麼修改,也是一個日誌,是InnoDB獨有的日誌。

redo log是一種偏向物理性質的重做日誌,記錄的是類似“對哪個資料頁中的什麼記錄,進行了什麼樣的修改”

事務提交之後,才算完成了一次SQL執行,現在還沒有提交事務,所有的資料修改都放在了Buffer Pool中,同時Redo log Buffer裡也有redo log的資料。

那麼這時mysql宕機會出現問題嗎,並不會,因為你沒有提交事務,代表沒有執行成功,雖然記憶體裡的資料都丟失了,但是磁碟上的資料依然還保持原樣。

那麼重啟之後,資料沒有發生變化,那麼就相當於沒有執行這個事務,不會發生資料不一致的問題。

都正常完成,現在要提交事務了,那麼我們會根據一定的策略把redo log從redo log buffer裡刷入到磁碟,這個策略可以通過innodb_flush_log_at_trx_commit來配置:

當設定為0,那麼你提交事務的時候,不會把redo log buffer裡的資料刷入到磁碟檔案,那麼mysql宕機,redo log日誌也相當於不見了。

設定為1,提交事務時候必須把redo log buffer刷入到磁碟的redo log中,只要提交事務成功,redo log必然在磁盤裡。

設定為2,把麼會把redo log buffer刷入到os cache記憶體快取中,還是沒有時機刷入到磁碟,然後過段時間後刷入到磁碟上,可能是1s,2s,500ms等等。

當設定為1的時候,只要事務提交了,redo log必然刷入到磁碟上,雖然Buffer Pool的資料還沒有刷入到磁碟,還是髒資料,然後這時候mysql宕機,記憶體資料丟失,但是mysql可以通過redo log來查詢對應修改的操作進行恢復,那麼資料就不會丟失了。在這裡插入圖片描述
所以通常也是設定為1,這樣可以保證資料絕對不會因為redo log而丟失,如果選擇0,那麼只要宕機,BufferPool的資料沒刷回磁碟,就會丟失,如果是2,那麼進入os cache之後如果宕機了,還是沒有進入磁碟檔案,還是可能回導致redo log丟失。

binlog

binlog叫做歸檔日誌,裡面記錄的是偏向邏輯性的日誌,類似於“對users表中的id=1的資料做了更新操作,更新後的值是什麼”,binlog並不是InnoDB獨有的,而是mysql server自己的日誌檔案。

那麼我們在提交事務的時候,會把redo log日誌寫入磁碟檔案中,而且還會把對應的binlog日誌寫入到磁碟檔案中去。

對於binlog日誌,其實也有不同的刷盤策略,有一個引數sync_binlog引數可以控制binlog的刷盤策略:

預設值為0,這時候binlog並不是直接進入磁碟檔案,而是os cache記憶體快取中,那麼還是會有丟失的風險。

設定為1的話,會強制在提交事務的時候,把binlog直接寫入到磁碟檔案中去,那麼哪怕機器宕機,磁碟上的binlog也不會丟失。

在這裡插入圖片描述

基於redo log和binlog的兩階段提交

那麼提交事務時,redo log和binlog日誌的順序為:

1.將redo log刷入磁碟,並將狀態改成prepare狀態
2.binlog日誌寫入磁碟
3.將binlog日誌檔名和binlog日誌在檔案裡的位置寫入到redo log中,同時將redo log狀態改為commit狀態

那麼這樣的寫入就可以防止任何一個階段導致的資料不一致問題:

如果redo log刷完磁碟,mysql宕機,或者binlog刷入到磁碟,mysql宕機,那麼由於不是commit狀態,無法找到對應的binlog位置,就代表事務提交失敗,只有commit狀態的時候表示redo log中有更新對應的binlog日誌,redo log和binlog資料是一致的。

在這裡插入圖片描述

後臺IO執行緒隨機將髒資料刷回磁碟

Mysql會有一個後臺的IO執行緒,會在某個時間裡,隨機把記憶體buffer pool中的修改後的資料刷回到磁碟上的資料檔案中,如果刷回之前崩潰,就用redo log來恢復之前提交事務做過的修改到記憶體中去,然後等待適當時機,IO執行緒自然會把這個髒資料刷回磁碟。

在這裡插入圖片描述

Buffer Pool

由於對資料庫執行增刪改查的時候,直接更新磁碟上的資料是進行隨機讀寫操作,那速度是很慢的,所以大部分主要都是針對記憶體裡的Buffer Pool中的資料進行的。

Buffer Pool記憶體資料結構

由於是記憶體資料結構,肯定是有一定的大小,不可能超過記憶體大小的,Buffer Pool預設情況下是128MB,還是偏小的一些,我們可以通過配置innodb_buffer_pool_size來調整大小,如果16核的,差不多可以分配個2GB記憶體左右,innodb_buffer_pool_size = 2147483648

我們的資料是一頁一頁放在Mysql中,這就是資料頁的概念,然後把很多行資料放在一個數據頁裡,那我們要更新一行資料,此時資料庫會找到這行資料所在的資料也,然後從磁碟檔案裡把這行資料所在的資料頁直接給載入到bufer pool裡去。

預設情況下,磁碟存放的資料頁大小是16KB,而我們Buffer Pool存放的資料頁通常叫快取頁,畢竟Buffer Pool是一個緩衝池,裡面的資料都是從磁碟快取到記憶體去的。

而且Buffer Pool裡快取頁大小和磁碟上的一個數據頁大小是一一對應起來的,都是16KB。

除此之外,每個快取頁都有對應的描述資訊,可以認為是用來描述這個快取頁的,比如這個資料頁所屬的表空間,資料頁的編號,以及快取頁在Buffer Pool中的地址等等,每個快取頁的描述資料放在最前面,然後各個快取頁放在後面。

所以如果buffer pool大小為128MB,實際上Buffer Pool真正大小會超出一些,可能有130MB多,因為要存放描述資訊,大概相當於快取頁大小的5%左右。

在這裡插入圖片描述

free連結串列

從磁碟上讀取資料放入Buffer Pool快取頁的時候,怎麼知道哪些快取頁是空閒的呢,為了知道哪些快取頁是空閒的狀態,資料庫為Buffer Pool設計了一個free連結串列,只要你快取頁是空閒的,那麼它描述資料塊就會被放入到這個free連結串列中了。

這個free連結串列每個節點都會雙向連結自己的前後節點,組成一個雙向連結串列,除此之外,還有一個基礎節點,會引用連結串列的頭節點和尾節點,還儲存了連結串列中有多少個空閒的快取頁。

那我們從磁碟上把資料頁讀取到Buffer Pool中的快取頁裡去的步驟為:

先從free連結串列裡獲取一個描述資料塊,然後可以對應的獲取描述資料塊對應的空閒快取頁。
接著把磁碟上的資料頁讀取到對應的快取頁裡
最後把這個描述資料塊從free連結串列裡去除就可以了

那麼如何知道對應的資料頁有沒有被快取,資料庫還會有一個雜湊表資料結構,會用表空間號 + 資料頁號,作為一個key,然後快取頁的地址作為value,也就是每次資料頁快取之後,都會在這個雜湊表中寫一個key-value對,下次如果再使用這個資料頁,就可以直接從雜湊表裡瀰漫讀取出來已經被放入一個快取頁了。

在這裡插入圖片描述

flush連結串列

由於我們更新Buffer Pool的時候,肯定還沒有寫入到磁碟中,這時候Buffer Pool的資料就是髒資料,那麼對應的快取頁就是髒頁。

為了判斷哪些是髒頁,需要刷回到磁碟,就需要和free連結串列一樣的,這就是flush連結串列,和free連結串列一樣,通過快取頁的描述資料庫中的兩個指標,讓被修改過的快取頁的描述資料塊組成一個雙向連結串列,但凡被改過的快取頁都會被加入到flush連結串列中,這些後續都需要flush重新整理到磁碟上的。

那麼當重新整理回磁碟後,就會從flush連結串列中去除。

在這裡插入圖片描述

LRU連結串列

如果當你不停的從磁碟上將資料頁載入到Buffer Pool上的空閒快取頁的時候,free連結串列裡的空閒快取頁就會越來越少,那麼總會用完所有的空閒快取頁,這時候就要去快取頁中淘汰一些快取頁來使新的資料頁載入到快取頁裡。

那麼淘汰掉哪些快取頁呢,以什麼為標準,這就要引入一個快取命中率的概念。假如100次請求中,有50次都在查詢和修改這個快取頁裡的資料,那麼快取命中率就很高了,相比之下,100次請求,只有一兩次是修改和查詢這個快取頁的資料,那麼快取命中率就很低了,大部分還會走磁碟查詢資料。

那麼為了使經常訪問的快取頁繼續保留,不經常使用的快取頁進行淘汰,就需要一個LRU連結串列了,LRU代表Least Recently Used,也就是最近最少使用的意思。

這個連結串列的原理是:我們載入一個數據頁到快取頁的時候,就把這個快取頁的描述資料塊放到LRU連結串列頭部,只要是有資料的快取頁都在LRU裡,而且最近被載入資料的快取頁都會放到LRU連結串列的頭部。

那麼連結串列尾部的快取頁就是最少訪問的,那麼就會把尾部的快取頁刷入磁碟中,然後把需要的磁碟資料頁載入到這個快取頁中就可以了。

由於後臺會有一個執行緒,執行定時任務,每隔一段時間就把LRU連結串列的冷資料區域的尾部的一些快取頁刷入到磁碟,清空這幾個快取頁,放入到free連結串列中。

那麼對於Buffer Pool整個流程為:

Mysql在不怎麼繁忙的時候,會找個時間把flush連結串列中的快取頁都刷入磁碟,這樣修改過的資料,遲早會刷入磁碟,那麼這些快取頁也會從flush連結串列和LRU連結串列中移除,然後加入到free連結串列中,這就是動態執行起來的效果

如果free連結串列都被使用了,那麼就會將LRU連結串列的冷資料區域的尾部找到一批快取頁,因為是最不常使用的快取頁,就會被刷入磁碟和清空,然後資料頁載入到這個騰出來的空閒快取頁裡去。

簡單的LRU連結串列可能導致的問題

由於Mysql的預讀機制,這個預讀機制,就是當你從磁碟上載入一個數據頁的時候,可能會把這個資料頁相鄰的其他頁也都載入到快取頁裡。

那麼可能相鄰的資料頁並沒有被訪問到,但是通過Mysql預讀機制順利的和讀取的快取頁到達LRU連結串列的頭部,那麼後面兩個之前載入的快取頁就會被優先淘汰掉,這並不合理,因為相鄰的快取頁根本沒被用到。

在這裡插入圖片描述

觸發Mysql預讀機制為:

1.引數innodb_read_ahead_threshold,預設值56,也就是如果順序訪問超過一個區裡的56個數據頁,那麼就會把下一個相鄰區中的所有資料頁載入到快取裡去。

2.如果Buffer Pool裡快取了一個區連續13個數據頁,而且這些資料頁都比較頻繁訪問,那麼就會觸發預讀機制,把這個區其他資料頁都載入到快取裡去,這個是通過innodb_random_read_ahead來控制,預設是OFF

所以上述情況下,第一個規則更可能會觸發預讀機制,一下子多了相鄰區的資料頁,且全部放到LRU頭部,無疑是不合理的。

還有就是如果全表掃描的情況,之後也不繼續訪問了,結果之前一直訪問的快取頁就排在了後面,優先被淘汰,這也是不合理的。

那麼為什麼要有Mysql預讀機制,為了提高效能,因為如果你讀取了大部分資料頁,那麼很可能你還要接著順序讀取相鄰的資料頁,那麼如果沒有這個機制,就要再次發起一次磁碟IO,所以為了優化效能,才設計了預讀機制。

冷熱資料分離,優化LRU

真正Mysql在設計LRU連結串列的時候,採用的是冷熱資料分離的思想,並不是都混在一個LRU連結串列裡。

真正的LRU連結串列會被拆分成兩個部分,一部分是熱資料,一部分是冷資料,冷熱資料的比例是由引數innodb_old_blocks_pct引數控制的,預設是37,也就是冷資料佔比37%。

在這裡插入圖片描述

那麼兩個區域使用規則為:

首先資料頁第一次被載入到快取的時候,會放在冷資料區的連結串列頭部。

根據innodb_old_blocks_time引數,預設是1000,也就是1000ms,1s
也就是1s之後,你訪問這個資料頁的時候才會被移到熱資料區域的連結串列頭部,因為Mysql認為,你過了這個時間後還會訪問這個資料頁,可能以後再次訪問的機率很高。

那麼之前的預讀機制以及全表掃描進來的一大堆快取頁都會放在冷資料鏈表的頭部,如果不在一定時間之後訪問,那麼就會判定他們不是經常訪問的資料,就會慢慢被移到冷資料的尾部,最後被淘汰掉。

因為熱資料區域本身就是經常被訪問的快取頁,所以沒有必要頻繁的移動,所以說熱資料區域的訪問規則被優化了一下,只有在熱資料區域的後四分之三部分的快取頁被訪問,才會放到連結串列頭部,如果已經在連結串列的前四分之一內,那麼就不會移動 ,這樣就可以儘可能的減少連結串列的節點移動了。

Mysql物理資料模型

資料頁

為什麼Mysql要引入資料頁的概念,如果是每次修改一條資料,就要去磁盤裡載入到快取中,下一次修改其他行的時候,再去磁碟載入到快取,這樣的效率未免有點太低,所以引入資料頁的概念,每次載入磁碟的資料的時候,將這個資料所在的資料頁都載入到快取,這樣之後,修改和讀取相關資料的時候,可能就不需要再去磁碟讀取,而是從這個資料頁中直接讀取到。

包括刷回磁碟的時候,也是資料頁進行刷回,減少刷回次數,一次性刷回整頁的資料。

資料頁結構

其實資料頁並不是16KB全都是存放大量的資料行的,而是分為很多個部分:檔案頭,資料頁頭,最小記錄和最大記錄,多個數據行,空閒空間,資料頁目錄,檔案尾部

在這裡插入圖片描述
其中檔案頭部佔據38個位元組,資料頁頭佔據56個位元組,最大記錄和最小記錄佔據了26個位元組,資料行區域,空閒區域,還有資料頁目錄是不固定的,檔案尾部佔據8個位元組。

那麼我們一開始新的資料頁是沒有資料的,然後往裡插入資料,那麼空閒區域就會相應減少,資料行區域增加一行資料,然後隨著增加到滿了,那麼空閒區域也就沒了

在這裡插入圖片描述

資料儲存格式

我們的一行資料在磁碟上儲存的時候,不僅僅包含的是那一些資料,還包括其他的資訊:變長欄位長度列表 NULL值列表 頭資訊 column1=value1 column2=value2 … columnN=valueN

那麼除了欄位的值外,額外的資訊就是用來描述這一行資料的。

變長欄位的長度列表

由於Mysql裡一些欄位的長度是可變的,並不是固定長度,比如varchar(10),那麼它就可以在10個長度以內任意長度都可以。

由於落地到磁碟的時候,資料都是一大坨放在一個磁碟檔案裡,且都挨著的,那麼讀取的時候就比較難以讀取哪些是一行,哪些是可變的資料,所以我們儲存每一行的時候,都儲存一下它的變長欄位的長度列表,比如 hello a a,這個hello是varchar(10)型別的變長欄位的值,那麼hello實際長度是5,十六進位制為0×05,此時,變長欄位的長度列表,就會儲存這個額外資訊,那麼這行的格式為:0x05 null值列表 資料頭 hello a a。

如果這個時候還有一行資料可能是:0x02 null值列表 資料頭 hi a a,兩行資料放在一起儲存在磁碟檔案裡,就是:

0x05 null值列表 資料頭 hello a a 0x02 null值列表 資料頭 hi a a

那麼如果說有多個變長欄位,比如一行資料有VARCHAR(10) VARCHAR(5) VARCHAR(20) CHAR(1) CHAR(1),一共5個欄位,其中三個是變長欄位,此時假設一行資料是這樣的:hello hi hao a a

此時磁碟中儲存的,必須在開頭的變長欄位長度列表中儲存幾個變長欄位的長度,但是這裡是逆序儲存的。也就是先存放20長度的,然後5,然後10的,那麼這行資料實際儲存可能為:

0x03 0x02 0x05 null值列表 頭欄位 hello hi hao a a

NULL值列表

如果Null值給個字串NULL,這種方式儲存在磁碟上就很浪費空間的,因為它本身沒有值,那我們只要判斷它有沒有值就可以了,所以NULL值是以二進位制bit位來儲存的,假設一行資料裡有多個欄位的值,都可以為null,是可以為空,而不是值就是空,就會放入到NULL值列表的,1代表是NULL,0代表不是NULL,且按照逆序存放,且起碼是8個bit位的倍數,因為要以byte為單位儲存,如果不足8個bit,就高位補0。

比如:“jack NULL m NULL xx_school”,其中jack是 not null欄位,那麼剩下的四個欄位2個為null,2個不是null,那麼4個bit就是1010,但由於是逆著放的,那麼就應該是0101,且不足8個bit,那麼高位補0,實際存放是:

0x09 0x04 00000101 頭資訊 column1=value1 column2=value2 … columnN=valueN

那麼結合變長欄位長度列表,我們可以先判斷哪些欄位為NULL,哪些不是NULL,然後不是NULL的欄位在變長欄位長度列表中一一對應,然後進行讀取,這樣就可以完美的把一行資料的值讀取出來了。

資料頭

資料頭是40個bit位,用來描述這行資料的:

首先第1位和第2位bit都是預留位,沒有什麼意義。

第3位是delete_mask,標識的是這行資料是否被刪除了,其實Mysql在刪除一行資料的時候,未必是立馬把他從磁碟上清理掉,而是用一個標記為來標識是否已經被刪除。

第4位是min_rec_mask,這個代表是B+數裡每一層的非葉子節點裡最小值都有這個標記

第5位~第8位是n_owned,這個是記錄了一個記錄數。

第9位~第21位是heap_no,代表的是這行資料在記錄堆裡的位置

第22位~第24位是recored_type,標識這行資料的型別,0代表普通型別,1代表B+數非葉子節點,2代表最小值資料,3代表最大值資料

第25~第40位是next_record,指向下一條資料的指標。

總結

那麼對應“jack NULL m NULL xx_school”,它真實儲存大致是:

0x09 0x04 00000101 0000000000000000000010000000000000011001 jack m xx_school

但是實際上字串這些東西是根據我們資料庫指定的字符集編碼,進行編碼後再儲存的,那麼實際儲存為:

0x09 0x04 00000101 0000000000000000000010000000000000011001 616161 636320 6262626262

除了這些以外,其實還有一些隱藏欄位:

DB_ROW_ID,這是這一個行的唯一標識,是資料庫內部給你搞的一個標識,並不是主鍵ID欄位,如果沒有指定主鍵的時候,回自動加一個ROW_ID作為主鍵。

DB_TRX_ID,這是與事務相關的,是事務ID

DB_ROLL_PTR,回滾指標,用來進行事務回滾

那麼實際一行資料應該是:

0x09 0x04 00000101 0000000000000000000010000000000000011001 00000000094C(DB_ROW_ID)00000000032D(DB_TRX_ID) EA000010078E(DB_ROL_PTR) 616161 636320 6262626262

括號代表前面的意義。

但是有可能某個欄位很長,比如TEXT這種資料結構,那麼很可能就會超過資料頁16kb的大小,那麼這個時候,實際上會在那一頁裡儲存你這行資料的一部分,然後同時20個位元組的指標指向其他的一些資料頁,哪些資料頁用連結串列串聯起來,存放這個超大的資料。

在這裡插入圖片描述
那麼在Buffer Pool從磁碟上會讀取這些多個數據頁來載入到快取行裡。

表空間

表空間其實就是我們平時建立的那些表或者系統表在物理層面的磁碟上的資料檔案。

比如磁碟上都會對應著“表明.ibd”這樣的一個磁碟資料檔案,系統表空間可能對應多個磁碟檔案,然後表空間的磁碟檔案裡,會有很多的資料頁,但是表空間裡的資料頁太多了,所以為了便於管理,在表空間裡引入了一個數據區(extent)的概念。

一個數據區對應著連續的64個數據頁,每個資料頁是16KB,那麼一個數據區是1MB,然後256個數據區被劃分為一組,對於表空間而言,它的第一組資料區的第一個資料區的前三個資料頁,都是固定的,裡面存放了一些描述性的資料:

比如FSP_HRD這個資料頁,存放的是一組資料頁的所有insert buffer一些資訊

INODE資料頁,存放了一些特殊的資訊

然後表空間裡的其他資料區,每一組資料區的第一個資料區的頭兩個資料頁都是放特殊資訊的,比如XDES資料頁用來存放這一組資料區相關屬性,其實就是描述這組資料區的東西。

當我們執行crud操作的時候,就是從磁碟上的表空間的資料檔案,去載入一些資料頁到Buffer Pool的快取頁去使用。

在這裡插入圖片描述

redo log

redo log和快取頁刷入磁碟,都是寫磁碟,但是差別就在於,快取頁刷入磁碟是隨機寫,而redo log寫入磁碟是順序寫,也就是每次都是追加到磁碟檔案末尾,速度要比隨機寫快很多,所以用redo log的形式記錄下來修改,效能會遠遠超過刷快取頁的方式,這樣可以讓資料庫的併發能力更強大。

redo log裡具體記錄的是:表空間號,資料頁號,偏移量,修改幾個位元組的值,具體的值

也就是會根據你修改的資料頁裡幾個位元組的值對應劃分不同的型別,比如MLOG_1BYTE型別的日誌就是指修改了1個位元組的值,MLOG_2BYTE就是修改了2個位元組的值,依此類推,如果你一下子修改了一大串的值,那麼型別就是MLOG_WRITE_STRING,標識一下子在資料頁的某個偏移量的位置插入或者修改了一大串的值,這時候除了上面的記錄的東西以外,還會有修改資料長度的記錄,那麼redo log格式為:

日誌型別(就是類似MLOG_1BYTE之類的),表空間ID,資料頁號,資料頁中的偏移量,修改資料長度,具體修改的資料

redo log block

redo log內部也不是直接簡單粗暴的寫入,而是通過redo log block的結構來存放多個單行日誌的,一個redo log block是512位元組,redo log block分為3個部分,一個是12位元組的header快頭,一個是496位元組的body塊體,一個是4位元組的trailer塊尾。

在這裡插入圖片描述
header頭又分為了四個部分:

1).包括4個位元組的block no,就是塊唯一編號
2).2個位元組的data length ,就是block裡寫入了多少位元組資料
3).2個位元組的first record group,每個事務都會有多個redo log,是一個redo log group,即一組redo log,那麼在這個block裡的第一組redo log的偏移量就是這2個位元組來儲存的。
4).4個位元組的checkpoint on

在這裡插入圖片描述
那麼我們從記憶體中寫入redo log的時候,會將redo log放入到redo log block資料結構中,然後等待記憶體裡的一個redo log block的512位元組都滿了,再一次性把這個redo log block寫入磁碟,那麼磁盤裡的redo log檔案裡就多了一個block。

在這裡插入圖片描述

redo log buffer

redo log buffer是在Mysql啟動的時候,就跟作業系統申請的一塊連續記憶體空間,然後i面劃分出N多個空的redo log block,通過設定mysql的innodb_log_buffer_size,可以指定這個redo log buffer的大小,預設是16MB,已經挺大了,已經一個redo log block也就512KB,每一條redo log也就幾個位元組到幾十個位元組。

當你要寫入一條redo log的時候,會先從redo log buffer的第一個redo log block開始寫入,寫滿了一個redo log block 之後,繼續寫下一個block,直到所有的redo log block寫滿位置。

平時我們執行一個事務的過程,是有多個增刪改的操作,那麼就會有多個redo log,這多個redo log就是一組redo log,每次一組redo log都是在別的地方暫存,然後都執行完之後,再把redo log寫入到redo log buffer的block裡去,如果redo log太多,可能就要存放到兩個redo log block裡,反之,一個redo log group比較小,那麼也可能多個redo log group都在一個redo log block裡。在這裡插入圖片描述
那麼什麼時候要從redo log buffer刷入到磁碟檔案redo log日誌檔案,有以下幾種情況:

1.如果redo log buffer的日誌已經佔據了redo log buffer總容量的一半,也就是超過8MB的redo log在緩衝裡,此時就會把它們刷入到磁碟檔案裡。

2.如果一個事務提交,那麼就必須把它的那些redo log所在的redo log block都刷入到磁碟檔案裡去,只有這樣,當事務提交之後,修改的資料才不會丟失。

3.後臺執行緒定時重新整理,有一個後臺執行緒每隔1s,就會把redo log buffer裡的redo log block刷到磁碟檔案裡去

4.Mysql關閉的時候,redo log block都會刷入到磁盤裡去

第一種情況發生在Mysql承載高併發請求的時候,比如每秒執行上萬個增刪改SQL語句,每隔SQL產生的redo log假設有幾百個位元組,那麼此時會瞬間生成超過8MB的redo log日誌,必然會觸發立馬重新整理到磁碟上。

第二種情況則是提交事務的時候,一半都是在幾十毫秒到幾百毫秒執行完畢,那麼就會把這個事務的redo log都刷入磁碟中。

在這裡插入圖片描述
redo log都會寫入一個目錄中的檔案裡,這個目錄可以通過show variables like 'datadir’來檢視,可以通過innodb_log_group_home_dir引數來設定這個目錄的。

然後redo log是有多個的,寫滿一個就會寫下一個redo log,而且可以限制redo log檔案的數量,通過innodb_log_file_size可以指定每隔redo log檔案的大小,預設是48MB,通過innodb_log_files_in_group可以指定日誌檔案的數量,預設就2個。

兩個96MB的redo log一般已經足夠用了,可以儲存上百萬條redo log了,這時候如果寫滿了,那麼就會繼續寫第一個,覆蓋第一個日誌檔案裡的原來的redo log,所以redo log本身是不具備長時間的持久化,因為會被覆蓋,所以恢復資料的時候用bin log來進行恢復。

undo log

如果一個事務裡的增刪改執行到一半,結果就回滾事務了,但是Buffer Pool裡的資料已經更改一半了,那麼為了能恢復原來的資料,就需要undo log這個日誌來恢復回滾事務的資料。

如果你執行了一個insert語句,那麼此時在undo log日誌裡,就會對這個操作記錄的回滾日誌就必須有一個主鍵和對應的delete操作,能讓insert操作給回退。

如果執行的是delete,那麼就需要有insert操作把這條資料插入回去。

如果執行的是update語句,那麼就需要把更新之前的那個值記錄下來,回滾時候重新update以下,把舊值更新回去。

在這裡插入圖片描述那麼INSERT語句的undo log日誌裡面包含:

這條日誌的開始位置
主鍵的各列長度和值
表id
undo log日誌編號
undo log日誌型別
這條日誌的結束位置

1.日誌的開始位置的結束位置就是這條undo log所在的位置

2.主鍵的各列長度和值,由於主鍵有可能是聯合主鍵,也有可能沒有主鍵,用row_id隱藏欄位,作為主鍵,所以就需要直到這個主鍵的長度為多少,具體的值是什麼才能進行修改

3.表id就是插入哪一個表

4.undo log日誌編號,每個undo log日誌都有自己的編號,在一個事務裡有多少個SQL語句,就會有多少個undo log日誌,在每個事務裡的undo log日誌的編號都是從0開始的,然後依次遞增

5.undo log型別,就是TRX_UND_INSERT_REC,帶包insert語句的undo log日誌型別

事務

一個事務就是要麼一起成功都提交,要麼有一個SQL失敗,就事務回滾,所有SQL修改都撤銷。

但是業務系統並不是一個單執行緒系統,是多執行緒同時併發訪問的,每個SQL語句就是之前的那一套原理,如果提交了,那麼redo log刷盤,然後redo log裡記錄事務提交標識之類的,如果宕機,就從redo log中恢復事務修改過的快取資料,如果回滾,就把快取頁做的修改都回滾就可以了。

但是多個事務併發執行的時候,可能會同時對快取頁的一行資料進行更新,可能還有其他事務在查詢這行資料,那麼為了解決這些衝突問題,就需要事務的其他機制,比如事務隔離MVCC,鎖機制等等。

如果多個事務對快取頁裡的同一條資料同時進行更新的問題,就會發生四種情況:髒寫,髒讀,不可重複讀,幻讀。

髒讀和髒寫

髒寫就是自己更新的值,結果卻莫名其妙沒了,比如:

事務A更新一條資料,會記錄一條undo log,更新前的值為NULL,然後事務B更新了事務A更新之後的值,那麼最後這行資料的值應該是B,但是事務A突然回滾了,那麼就會用它的undo log進行回滾,結果更新回之前的NULL值,事務B發現自己更新的值變為NULL了,這就是髒寫。

也就是事務B去修改事務A修改過的值,但是此時事務A還沒有提交,事務A隨時可能回滾,導致事務B修改的值頁沒了,這就是髒寫的定義。

髒讀就是再次查詢相同行資料的值時,發現值是不一樣的,比如:

事務A更新了一行資料為A值,然後事務B去查詢了這行資料的值,此時是A,然後事務A突然回滾了,事務B再次查詢的時候發現是NULL。這就是髒讀。

無論是髒寫還是髒讀,都是因為一個事務去更新或者查詢了另外一個還沒提交的事務更新過的資料。

不可重複讀

如果一個事務只能讀到其他事務提交之後的資料,那麼就不會發生髒讀,但是會發生不可重複讀。

假設事務A沒有提交,讀到的值是A,但是事務B修改之後提交成B,然後事務A第二次查詢時候就為B,事務C提交成C,事務A再次查詢時候就是C。

在這裡插入圖片描述
如果你希望事務A讀到的值是可重複讀的,也就是事務A自己讀到的值不發生變化,那麼這種發生不可重複讀的現象就是一種問題了。

不可重複度,就是一條資料的值沒辦法滿足多次重複讀值都一樣,別的事務修改後提交,就不可重複讀了。

幻讀

幻讀指的就是你一個事務用一樣的SQL多次查詢,結果每次查詢都會發現查到了值卡沒看到過的資料。特指的是之前查詢沒看到過的資料。

比如執行select * from table where id > 10,查詢出來10條資料,結果事務B往裡面插入了2條資料,並且提交,你第二次查詢時候,查出來12條資料,一模一樣的SQL查出來沒有看到過的資料,就像中了幻覺一樣,這就是幻讀。

在這裡插入圖片描述

隔離級別

SQL標準中規定了四種隔離級別,包括:read uncommitted(讀未提交),read committed(讀已提交),repeatable read(可重複讀),serializable(序列化)

1.read uncommitted級別

不允許發生髒寫的,也就是不可能兩個事物在沒提交的情況下去更新同一行資料的值,但是在這種隔離級別下,可能發生髒讀,不可重複讀,幻讀。

因為可以讀到其他事務沒有提交的資料,那麼肯定會發生髒讀,更別說其他的情況了,所以一般來說,沒有人做系統開發的時候,會把事務隔離級別設定為讀未提交這個級別。

2.read committed隔離級別

這個級別下,不會發生髒寫和髒讀,也就是說其他事務提交後,你才可以看到修改的值,那麼就會發生不可重複讀和幻讀的問題,簡寫為RC,但別的事務沒有提交的時候,絕對不會讀到人家修改的值。

3.repeatable read隔離級別

就是可重複度,這個隔離級別下,不會發生髒寫,髒讀,不可重複讀的問題,因為哪怕別的事務修改提交了,也只會讀到同一個值,簡寫RR,但是還會發生幻讀,因為RR隔離級別,只不過保證對同一行資料的多次查詢,不會讀到不一樣的值,但是不是同一行的就還是會查到,那麼就會發生幻讀。

4.serializable級別

這種級別,根本不允許多個事務併發執行,只能序列執行,所以不可能會有幻讀,那麼別的情況也不會發生,一般不會設定這種級別,那麼1s併發可能也就幾十個,效能是很差的。

在Mysql也是支援這四種隔離級別的,且預設隔離級別是RR,還可以避免幻讀的發生,這是因為MVCC的控制。

Mysql的事務隔離級別,可以設定為不同的level:

SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level;

但是一般來說,不用修改這個級別,預設的RR就特別好,但是有的業務也會有需要RC的情況。

Spring裡通過@Transactional註解來做事務這塊,通過isolation來更改事務隔離級別。

MVCC

MVCC,多版本併發控制

undo log版本鏈

每條資料都有兩個隱藏欄位,一個是trx_id,一個是roll_pointer,這個trx_id就是最近一次更新這條資料的事務id,roll_pointer就是指向你更新這個事務之前生成的undo log。

在這裡插入圖片描述

ReadView機制

ReadView,就是你執行一個事務的時候,會給你生成一個ReadView檢視,這裡面有4個比較關鍵的東西:

1.m_ids,這個就是說此時有哪些事務在Mysql裡還沒有提交的
2.min_trx_id,這是m_ids裡最小的值
3.max_trx_id,這是mysql下一個要生成的事務id,就是最大事務id
4.creator_trx_id,就是你這個事務的id

例子:

現在有一行資料,事務id是32

接著兩個事務比你高法過來,一個是事務A,id為45,一個是事務B,id為59,事務B是要更新這行資料,事務A是讀取這行資料的值。

現在事務A開啟一個ReadView,那麼這個ReadView裡的m_ids就包含了事務A和事務B的兩個id,45和59,然後min_trx_id就是45,max_trx_id就是60,creator_trx_id就是45,也就是事務A自己。

那麼事務A第一次查詢這行資料,會判斷一下當前這行資料的trx_id是否小於ReadView中的min_trx_id,發現32小於45,那麼就代表開啟事務之前,修改這行資料的事務早就提交了。

在這裡插入圖片描述

接著事務B修改為值B,那麼這行資料的trx_id也為自己的id,也就是59,同時roll_pointer指向了修改之前生成的一個undo log,然後事務B提交了。

事務A再次查詢的時候,發現trx_id為59,大於min_trx_id,同時小於max_trx_id,那麼說明修改這條資料的事務,就是自己開啟事務的時候還存在的事務,那麼就會檢查下是否在m_ids列表中,結果是,那麼修改資料的事務就是跟自己併發執行然後提交的,那麼這行資料是不能查詢的。

然後就會順著roll pointer往下查詢,找到最近的一條undo log,接著和自己的min_trx_id判斷,那麼trx_id為32,小於min_trx_id,是可以查詢到的。

在這裡插入圖片描述

如果自己修改的,那麼自己肯定是可以看到的,這時候來個事務C,事務id為78,然後進行更改,提交,事務A查詢的時候,發現大於max_trx_id,說明這條資料是被建立ReadView之後的事務進行修改並提交的,那麼就不能夠查詢,接著會順著roll pointer來往下繼續找,下一個undo log是自己修改的,那麼這個版本是可以被檢視的,就會查詢這個版本。

在這裡插入圖片描述

RC級別下ReadView實現

RC隔離級別,代表別的事務修改資料還提交了,就可以讀到人家修改的資料的,那麼就會發生不可重複讀和幻讀的問題。

RC隔離級別的核心在於,每次發起查詢的時候,都會重新生成一個ReadView。

例子:

首先假設有一行資料,事務id為50,現在有一個事務A,id為60,事務B,id為70。事務B發起一次更新操作,將這個值修改為了B。

那麼A發起一次查詢操作,就會生成一個ReadView,此時min_trx_id = 60,max_trx_id = 71,reator_trx_id = 60,但是這條資料的trx_id為70,也就是在ReadView事務id範圍之內修改,由於事務B還沒有提交,那麼m_ids活躍事務列表裡還有70這個事務id,那麼事務A是無法檢視事務B修改的值的。

接著就會根據roll_pointer往下找,發現trx_id為50,那麼就可以查到這個值。
在這裡插入圖片描述

接著事務B進行提交,那麼根據RC隔離級別,事務B一旦提交事務A下次再查詢,就可以讀到事務B修改過的值了,那麼事務A下次再發起查詢時,會再次生成一個ReadView,那麼新的ReadView的m_ids只有60這個活躍事務了,那麼這條trx_id為70,在min_trx_id和max_trx_id範圍之間,但是不在m_ids列表內,說明事務B已經提交了,那麼就可以查詢到這個事務B修改過的值了。

在這裡插入圖片描述

RR隔離級別基於ReadView實現

例子:

首先還是有一條資料,事務id為50,然後有事務A,id為60,事務B,id為70,然後事務A發起一個查詢,第一次查詢會生成一個ReadView,此時ReadView如圖所示:

在這裡插入圖片描述
然後事務B修改為B,同時生成undo log,然後事務B還提交了,說明此時事務B已經結束了。

但是由於RR隔離級別下,ReadView一旦生成就不會改變,所以事務A的ReadView裡還是會有60,70這兩個事務id。

接著事務A去查詢這條資料的值,會發現這行資料的trx_id為70,且在min_trx_id和max_trx_id的範圍之間,且在m_ids列表中,那麼說明事務A開啟的時候,事務B還是在執行的,然後事務B更新了這條資料,所以事務A是不能查詢到事務B更新的這個值,那麼就會順著roll_pointer來查詢上一個undo log版本,結果找到trx_id為50,那麼事務A是可以查詢到這個值的。

在這裡插入圖片描述

如果多個事務對同一行資料進行更新操作,那麼就需要用到鎖來保證每次只有一個事務可以更新,要不然就會出現髒寫的現象了。

一個事務更新一行資料,首先看這行資料有沒有被加鎖,如果沒有,說明這個事務是第一個到達的,那麼它就會對這行資料進行加鎖,trx_id設為自己的事務id,等待狀態為fasle。

在這裡插入圖片描述

之後有另一個事務B過來了,也要更新這一行,那麼就要檢查一下,發現已經被加鎖了,那麼事務B也進行加鎖,但是需要排隊等待前面執行完解鎖。

在這裡插入圖片描述

事務A執行完之後,就會釋放自己的鎖,然後去查詢是否別人也進行加鎖了,那麼就會喚醒其他加鎖的事務,並把等待狀態修改為false,那麼事務B被喚醒,得到鎖,進行修改。

在這裡插入圖片描述

獨佔鎖和共享鎖

獨佔鎖就是這個事務對這行資料進行更新的時候,會加一個獨佔鎖,表示這行資料更新是由我這個事務獨佔,其他事務更新時候也會加獨佔鎖,但是隻能排隊等待在前面獨佔鎖的後面。

共享鎖是可以共享進行操作的鎖,比如你select * from table lock in share mode,lock in share mode就是共享鎖,雖然共享鎖之間不是互斥的,但是獨佔鎖和獨佔鎖,或者獨佔鎖和共享鎖是互斥的,都要在後面進行等待。

查詢的時候也可以通過加for update來加獨佔鎖,但是一般開發的時候很少會主動加共享鎖,反而會基於redis/zookeeper的分散式鎖來控制業務系統的鎖邏輯。

表級鎖

除了行鎖之外,還有對整個表進行加鎖,比如在執行DDL語句的時候,會和增刪改操作互斥。

表鎖分為兩種:一種是表鎖,另一種是表級的意向鎖。

表鎖可以用下面的語法來加:

LOCK TABLES xxx READ:這是加表級共享鎖

LOCK TABLES xxx WRITE:這是加表級獨佔鎖

一般來講,幾乎沒人會用這兩個語法去加表鎖。。

另一個意向鎖的話:

在表裡執行增刪改操作,會加上獨佔鎖,同時還會加一個表級意向獨佔鎖

在表裡進行查詢操作,就會加表級意向共享鎖。

鎖之間的互斥關係為:

S

也就是更新資料加的意向獨佔鎖,會跟表鎖是互斥的,意向共享鎖會跟表級獨佔鎖是互斥的。

但是一般來說不會手動加表級鎖,只有很少部分DDL的時候會加,所以一般來說,讀寫操作自動的表級意向鎖,相互之間是不會互斥的。

所以行級獨佔鎖都是互斥的,但是讀操作都是不互斥的,因為讀操作預設走mvcc機制讀快照版本

索引

資料頁物理儲存結構

大量的資料頁是按順序一頁一頁存放的,且資料頁之間會採用雙向連結串列的格式相互引用,然後資料頁內部會儲存一行一行的資料,按照主鍵大小順序進行排序,且每一行資料指向下一行資料的位置,組成單向連結串列。

每個資料頁裡面都會有一個頁目錄,裡面根據資料行的主鍵存放了目錄,同時資料行是被分散儲存到不同的槽位裡面去。

在這裡插入圖片描述
假設要根據主鍵查詢一條時速局,那麼就很到第一個資料頁通過二分查詢在目錄裡定位到主鍵對應的資料在哪個槽位裡,然後到那個槽位離去,就能快速找到那個主鍵對應的資料了。

如果不是按照主鍵查詢,那麼就要從資料頁內部的資料間的單向連結串列來遍歷查找了。

如果找不到,就要找下一個資料頁的快取頁,以此類推,那麼上述操作,其實說白了就是全表掃描的過程,效能是非常低的,隨著資料量越多,會變得越來越慢。

頁分裂

資料頁裡面的一行一行的資料,剛開始是一行起始頁,行型別為2,然後指標指向了下一行資料,每一行資料都有自己每個欄位的值,然後每一行通過一個指標不停的指向下一行資料,普通的資料行型別都是0,最後一行是一個型別為3的,代表最大的一行。

但是索引運作的一個核心基礎就是要求你後一個數據頁的主鍵值大於前面一個數據頁的主鍵值,如果是主鍵自增還可以,但是如果並不是自增,而是手動插入的,那麼可能會出現後一個數據頁的主鍵值,有的主鍵小於前一個數據頁的主鍵值。

這時候就會出現頁分裂,也就是將前一個數據頁裡主鍵值較大的,挪動到後一個數據頁裡,然後將後一個數據頁較小的值,挪到前一個數據頁中去。

比如有如圖所示的兩個資料頁,後一個的資料頁裡面的資料比前面資料頁的資料小,那麼就要通過頁分裂來挪動。
在這裡插入圖片描述

最後將後一個數據頁的2,3的資料挪到前面,前面資料頁的5,6挪到後面。

在這裡插入圖片描述

索引頁

由於資料頁的數量增多,我們不可能每次都從第一個資料頁開始按照主鍵id二分查詢,所以就需要有一個管理資料頁的一個目錄,裡面存放了這個資料頁的最小主鍵id和它自己的頁號,並且這裡也是按照最小主鍵id順序排列的,那麼我們可以通過主鍵id來對比它的最小主鍵id,如果大於這個最小主鍵id,並且小於下一個頁的最小主鍵id,那麼說明這行資料就在這個資料頁中。

在這裡插入圖片描述

但是資料頁越來越多,幾百萬,幾千萬,甚至億級別的資料。那麼就有大量的資料頁,那麼就不能存放在一個目錄裡了,所以這時候就有了索引頁,那麼索引頁裡就會存放一部分的資料頁的最小主鍵id和頁號,然後與資料頁裡面的排序一樣,指標引用這下一行的資料頁資料。

在這裡插入圖片描述

但是有很多個索引頁,你需要知道在哪個索引頁裡去找主鍵資料,那麼就需要把索引頁多加一個層級出來,在更高的索引層級裡,儲存了每個索引頁號和索引頁裡的最小主鍵id。更高層裡的存放的索引頁資料也是按照最小主鍵id順序排序的,也是可以二分查詢的

在這裡插入圖片描述
如果頂層的索引頁裡存放不下更多的下層索引頁資料,那麼就需要再次分裂,更加一層索引頁,再存放下層索引頁的資料。

在這裡插入圖片描述
這種層級關係,且每個層級內的順序排序,構成了一顆B+樹,當主鍵建立起來索引之後,這個主鍵的索引就是一顆B+樹,然後你根據主鍵來查詢資料的時候,直接就是從B+樹的頂層開始二分查詢,找到下層的索引頁,再根據這裡的索引頁資料再次按照主鍵二分查詢,一層一層往下定位,最終找到一個數據頁,然後在資料頁內部的目錄裡二分查詢,找到那條資料。

聚簇索引和非聚簇索引

由於主鍵索引的最底層的索引頁裡會有每個資料頁的資料,最後會指向資料頁,那麼資料頁就相當於整個B+樹葉子節點,那麼這種B+樹就可以稱為聚簇索引。

如果針對其他欄位建立索引,比如name,age之類的,都是一樣呢的原理,那麼其他的和主鍵id索引的一樣,不一樣的就是資料頁裡面記錄的並不是整行資料,而是這個欄位和對應的主鍵id,這就是非聚簇索引。

在這裡插入圖片描述

所以如果要找出了這個索引值和主鍵之外的列的時候,就需要根據主鍵值,從聚簇索引裡從根節點開始,一路找到對應的完整資料行,再把要的欄位值拿出來,這個叫做回表,那麼這時候就要多一次查詢聚簇索引。

如果是聯合索引,那麼就會按照放的順序排序,比如name + age,那麼就會先按name排序,如果name相等的時候,再按age排序,然後走這個索引的B+樹,再搜尋到主鍵,根據主鍵到聚簇索引裡去搜索。

插入資料時維護不同索引的B+樹

首先,剛開始一個表,就有一個數據頁,這個資料頁就屬於聚簇索引的一部分,而且還是空的。

然後插入資料,直接就插入到這個資料頁了,也沒必要弄索引頁

那這個資料頁就是根頁,每個資料頁內部都有一個基於主鍵的頁目錄,所以這時候根據主鍵來搜尋直接從頁目錄裡找就行

資料頁滿了,就會多出一個新的資料頁,然後拷貝一些資料到新的資料頁,並根據主鍵值的大小進行挪動,讓兩個資料頁根據主鍵值排序,讓第二個資料頁的主鍵值都大於第一個資料頁的主鍵值。

在這裡插入圖片描述

此時的根頁就升級為索引頁,這個根頁裡放的是兩個資料頁的頁號和他們最小的主鍵值。

隨著資料頁索引條目越來越多那麼就需要多個索引頁來儲存資料頁索引,那麼這多個索引頁就需要一個上層索引頁來存放下層的索引頁,此時根頁就是上層的索引頁。資料頁越來越多,索引頁也不停分裂,分裂出更多的索引頁,然後又多出來上層索引頁來儲存這一層的索引頁,那麼根頁再次往上提上一層。

在這裡插入圖片描述

但是索引也有兩個缺點:

1.空間上,要建立很多的索引,那麼就必須有多棵索引樹,每個B+樹都要佔用很多的磁碟空間,那麼建立太多索引,是很消耗磁碟的。

2.時間上,你在增刪改的時候,需要維護各個索引的資料有序性,因為每個所以你B+樹都要求頁內按照值大小排序,頁之間也是有序的,下一個頁的所有值必須大於上一個頁的所有值,如果插入的資料較小,那麼就會進行資料頁的挪動,維護頁之間的順序,不停的插入資料,可能會導致資料頁不停的反分裂,不停的增加新的索引頁,整個過程都是消耗時間的,那麼就會導致增刪改的速度比較差了。

所以一般都會使用聯合索引,可以複用很多空間和時間上的消耗。

聯合索引查詢

1.等值匹配規則,也就是幾個欄位名稱和聯合索引的欄位完全一樣,而且都是基於等號的等值匹配,那麼百分百會用上所以你,即使順序不一樣,Mysql優化器也會優化成欄位順序去找。

2.最左側列匹配,這個意思就是我們的聯合索引是KEY(class_name, student_name, subject_name),那麼只要根據最左側部分欄位來查,也是可以的。

比如select * from student_score where class_name=’’ and student_name=’’",查某個學生所有科目的成績,不一定非得要後面suvject_name。

但是不能跳過前面欄位,直接查後面的,因為排序是先按照前面排,然後前面相等的時候,才按照後面排,那麼也就是前面不相等的時候,後面是沒有順序的,也就用不到索引了。

3.最左字首匹配原則,如果用like語法來查select * from student_score where class_name like ‘1%’,查詢所有1打頭的班級的分數,那麼也是可以用到索引的。

因為你的聯合索引是按照class_name排序的,那麼只要確定最左字首,那麼就可以基於索引來查,但是不能基於最右,畢竟排序是從左到右依次排序的,比如a,cb,cc,d,這樣的順序排的。

4.範圍查詢規則,我們可以用class_name按照範圍去找比如"1班"到"5班",那麼索引就會先找到"1班"對應的資料頁,再找到"5班"對應的資料頁,兩個資料頁中間的那些資料頁就都是你範圍內的資料了。而且資料頁之間使用雙向連結串列連線的,所以可以很好的遍歷出來。

5.等值匹配 + 範圍匹配的規則,如果你要是用select * from student_score where class_name=‘1班’ and student_name>’’ and subject_name<’’,那麼此時你首先可以用class_name在索引裡精準定位到一波資料,接著這波資料裡的student_name都是按照順序排列的,所以student_name>’‘也會基於索引來查詢,但是接下來的subject_name<’'是不能用索引的。

排序使用索引

如果普通的情況下,類似於select * from table order by xx1,xx2,xx3 limit 100這樣的SQL,那麼就需要把這些資料放到一個臨時磁碟檔案裡,然後在通過排序演算法在磁碟檔案裡排序,再按照指定的要求拿走limit語句,那麼SQL速度簡直慢到家了。

那麼這種時候,我們建立了INDEX(xx1,xx2,xx3)這樣的聯合索引,本身是一次按照xx1,xx2,xx3三個欄位的值排序,那麼此時再執行上面那樣的語句,就不需要再臨時硬碟檔案裡排序了。

那麼聯合索引的索引樹裡都排序好了,那麼我們就直接按照從小到大的值獲取前100條就可以了,再拿到前100條資料的主鍵再去聚簇索引裡回表查詢剩餘的欄位。

如果都是降序,那麼就要都進行xx1 desc,xx2 desc,xx3 desc,不能有升有降的,索引樹裡面的因為都是順序排序的。

分組使用索引

一般做分組的時候,都會group by把資料分組,接著用count,sum之類的聚合函式,如果不用索引,那麼就需要把所有的資料放到一個臨時磁碟檔案裡還有加上部分記憶體,然後去弄一個分組,按照指定欄位的值分成一組一組,接著對每一組都執行一個聚合函式,那麼效能是極差的,畢竟要涉及大量的磁碟互動。

但是我們的索引樹裡預設都是按照指定的一些欄位都排序好的,那麼欄位值相同的資料都是在一起的,假設要走索引去執行分組後,再聚合,那效能一定是要比臨時磁碟檔案去執行好多了。

group by和order by用索引的原理和條件都差不多,本質都是按照最左側開始的欄位順序一致,然後利用索引樹已經完成排序的特性,快速根據排序好的資料執行後續操作。

那麼設計表的索引的時候,充分考慮後續你的SQL要怎麼寫, 然後大概會根據哪些欄位進行where語句裡的篩選和過濾,大概用哪些欄位進行排序和分組,考慮好之後,就可以為表設計兩三個常用的的索引,覆蓋常見的where篩選,order by排序和group by分組的需要,保證常見的SQL語句都可以用上索引,那麼查詢效能就不會有太大的問題。

回表對效能的損害以及覆蓋索引

一般我們自己建立的索引都是獨立的索引B+樹,那麼僅僅包含索引裡的幾個欄位和主鍵值,如果需要查詢其他欄位,那麼就需要回表操作,跑到主鍵的聚簇索引裡去找。

類似select * from table order by xx1,xx2,xx3的語句,相當於是得把聯合索引和聚簇索引,兩個索引的所有資料都掃描一遍了,那還不如就不走聯合索引了,直接全表掃描得了,這樣還就掃描一個索引而已。

但要是select * from table order by xx1,xx2,xx3 limit 10這樣的語句,那執行引擎就會先掃描聯合索引樹,拿到十條資料,然後再去聚簇索引查詢10次就可以了,那麼還是會走聯合索引的。

覆蓋索引就是需要查詢的欄位僅僅需要聯合索引或者單獨欄位的索引裡的幾個欄位的值,那麼只需要掃描聯合索引的索引樹就可以了,不需要進行回表操作,這種方式就是覆蓋索引,那麼我們可以用到聯合索引的時候,就用聯合索引,減少回表的次數,要麼可能直接給你做成全表掃描,不走聯合索引了。

如果真的要回表做聚簇索引的時候,也儘量用limit where等語句限定以下回表聚簇索引的次數,那麼效能也會好一些,會走聯合索引。

設計索引

首先設計好表結構之後,就是設計表的索引:

第一點就是未來我們對錶進行查詢的時候,大概會如何進行查詢,如果一開始根本不知道要怎麼查詢表,那麼我們可以先進入系統的開發,等功能差不多開發完畢了,你就可以考慮如何建立索引了,針對SQL語句的where條件,order by條件以及group by條件去設計索引。

此時可以設計一個或者兩三個聯合索引,每個聯合索引都儘量去包含你的where,order by,group by裡的欄位,檢視,是否都是最左側欄位開始部分欄位。

如果有的欄位就幾個值的選擇,要麼0要麼1,或者就幾個值的範圍,那麼這種欄位建立索引是沒有太大意義的,因為根本沒辦法用快速的二分查詢,沒有什麼意義,所以儘量使用那些基數比較大的欄位,也就是值比較多的欄位,才能發揮出B+樹快速二分查詢的優勢。

儘量對欄位的型別比較小的列來設計索引,比如tinyint之類的,那麼自己本身的值佔用的磁碟空間小,搜尋時候效能也會比較好,如果針對varchar(255)這樣的欄位建立索引,可能值太大了,佔用很多磁碟空間,那麼可以針對這個欄位的前幾個字元建立索引,比如前20個字元,那麼建立索引就類似於KEY my_index(name(20), age, course)這樣的形式,此時你再where條件裡搜尋的時候,如果是根據name欄位來搜尋,那麼就會先到索引樹里根據name欄位的前20個字元去搜索,然後再到聚簇索引去提取完整的name欄位值進行比較就可以了。

但是對於order by ,group by這種的,前20個字元是無法使用索引了,有可能前20個字元是一種順序,但是整個name排序並不是那個順序。

如果你在查詢的欄位裡使用函式,那麼也不會走索引,畢竟通過函式計算之後的欄位並不是索引樹裡的順序。

之後插入的資料值如果不是按照順序來的,可能就會導致索引樹裡的某個頁自動分裂,那麼頁分裂就很耗費時間,因此一般設計索引別太多,建議兩三個聯合索引就可以覆蓋掉你這個表的全部查詢了,還有很關鍵的一點,就是建議主鍵id一定是自增的,別用UUID之類的,因為主鍵自增,起碼聚簇索引不會頻繁的分裂,但是如果用UUID,那麼也就導致聚簇索引頻繁的頁分裂。

執行計劃

也就是MYSQL地層,針對磁碟上的大量資料表,聚簇索引和二級索引,如何檢索查詢,如何篩選,如何使用函式,如何排序,如何分組等等,這個過程就是執行計劃。

Mysql單表查詢的執行計劃

如果寫一個通過主鍵id查詢的,或者通過二級索引加上聚簇索引查詢的,這種根據索引直接可以快速查詢資料的過程,在執行計劃裡稱為const,也就是效能超高的常量級。但是這裡的二級索引也必須是唯一索引,也就是二級索引的每個值都是唯一的。

如果是一個普通的二級索引,那麼查詢速度也是很快的,只不過由於不是唯一的,查到等於或者範圍的情況,還要檢視下一個是否也是這個值,這時候執行計劃裡叫做ref,如果包含多個列的普通索引,那麼最左側開始連續多個列都是等值比較才可以是ref方式。類似於select * from table where name=x and age=x and xx=xx,然後索引可能是個KEY(name,age,xx)

另外,使用name is null這種語法,那麼即便name是主鍵或者唯一索引還是會走ref方式,但是如果你是針對一個二級索引同時比較了一個值還有限定了IS NULL,類似於select * from table where name=x or name IS NULL,那麼此時在執行計劃裡就叫做ref_or_null

如果你的普通索引進行範圍篩選,比如age > x and age < y這種的,這種方式就是range,如果篩選的範圍不是很大,效能也是很快,但如果一下子查出來幾十萬條資料,那麼效能也不會很高了。

還有一種比較特殊的資料訪問方式,就是index,比如聯合索引是KEY(x1,x2,x3),好,現在我們寫一個SQL語句是select x1,x2,x3 from table where x2=xxx,那麼x2不是聯合索引最左側的那個欄位,但是查詢的值是在二級索引樹內的,那麼就會直接遍歷KEY(x1,x2,x3)索引樹的葉子節點的那些頁,一個接一個的遍歷,然後找到x2=xxx的那些資料,因為二級索引葉子節點除了這三個值以外,還有主鍵的值,但是要比聚簇索引葉子節點小多了,所以速度也快,這種只要遍歷二級索引就可以拿到查詢的資料,而不用回表到聚簇索引的訪問方式,就是index訪問方式。

那麼針對上面5種訪問方式,const,ref,ref_or_null和range,只要查出來的資料量不是特別大,效能都極為高效,index稍微次一點,畢竟是遍歷某個二級索引,但是索引比較小,遍歷效能也還可以。

最次的就是all了,all的意思就是直接全表掃描,掃描你聚簇索引的所有葉子節點,一行一行去掃描,那麼有幾萬條,幾十萬條資料以上的,基本都會很慢很慢。

舉例:

1.select * from table where x1 = xx or x2 > xx,這時候建立的索引只有(x1, x3)和(x2, x4),那麼這時候因為等值比較,掃描的資料比較少,那麼Mysql優化器可能會挑選x1的索引,做一個查詢x1 = xx,之後接著回表,取出完整的資料,然後到記憶體裡,根據每條資料x2欄位的值,再進行條件篩選。

2.select * from table where x1 = xx and c1 = xx and c2 > xx and c3 is not null,這時候x1是有索引的,其他都沒有索引,那麼這種情況下,查詢優化器生成的執行計劃,就會僅僅針對x1欄位走一個ref訪問,然後再去聚簇索引把完整的欄位查出來,載入到記憶體裡去,接著就可以針對這波資料的c1,c2,c3欄位按照條件進行篩選和過濾了,所以你的x1索引的設計,必然儘可能是讓x1=xx這個條件在條件樹裡查找出來的資料量比較少,才能保證後續的效能比較高。

3.select * from table where x1 =xx and x2 = xx,這兩個欄位分別都有一個索引,那麼這個時候是有可能同時查兩個索引樹,然後取交集,再回到聚簇索引查詢,如果經過交集之後,資料量由多變少,那麼這種可能性就會很高。如果不是and 是or,那麼也有可能會查詢兩個索引樹,然後並集來進行合併,intersection(交集),union(並集),但是這種情況也不一定會發生。

多表關聯的執行計劃

多表關聯的基本原理就是,先在一個表裡通過篩選條件,查出一批資料,這個表就是驅動表,然後將這一批資料去另一個表進行查詢,那麼另一個表就是被驅動表。

如果在驅動表裡找到10條資料,那麼就要到另一個被驅動表裡去根據連線條件裡篩選資料,那麼就要去查詢10次,這就是巢狀迴圈關聯查詢(nested-loop join)

所以對驅動表根據where條件進行查詢的時候,要走索引來查詢,並且被驅動表也一樣,如果其中有一個表走全表掃描,資料還很大,那麼速度就很很慢,加上巢狀迴圈關聯查詢,幾十次的全表查詢,速度會慢的無法接受。

成本優化

成本:分為從磁碟讀資料到記憶體的IO成本,因為都是一頁一頁的讀,讀一頁的成本約定為1.0,還有就是拿到資料之後驗證是否符合搜尋條件,活著排序分組之類的,這些資料CPU成本,因為消耗CPU資源,一般約定讀取和檢測一條資料是否符合條件的成本是0.2。

可以通過show table status like “表名”,拿到這個表的統計資訊,rows就是表裡的記錄數,data_length就是聚簇索引的位元組數大小,除以1024的就是kb,再除以16,就是資料頁的數量,那麼資料頁數量 * 1.0 + rows * 0.2就大致總成本就出來了,當然,這是全表掃描的成本。

如果使用二級索引,且查詢條件涉及到幾個範圍,比如name值在25~100,250 ~ 350,那麼就是兩個範圍,否則name = xx就僅僅是一個範圍區間,一般一個範圍區間就簡單粗暴的認為等同於一個數據頁,這時候,IO成本都會估計很小,要麼是1 * 1.0,要麼是 n *1.0,只是個位數這個級別。

然後估算出可能拿到的資料有多少, 在 * 0.2左右,之後拿到的資料還要回表到哦聚簇索引裡去查詢完整時速局,這裡預設一條資料回表就要查詢一個數據頁,如果是100條資料,那麼就是100左右的IO成本。

最後再用這100條資料,查詢是否符合其他查詢條件,那麼耗費CPU成本就是100 * 0.2,也就是20左右,那麼一共成本就是 1 + 20 + 100 + 20 =141,比如全表掃描的幾千來說,成本是很低的。

多表關聯的成本與單表差不多,先對驅動表進行最佳訪問方式,用最低成本從驅動表裡查出符合條件,然後再去被驅動表查出條件,與之前一樣的估算方法,然後挑選一個成本最低的方法,驅動表的估算成本 + 驅動表資料條數 * 被驅動表一次查詢的估算成本,就是總成本了。

Mysql基於各種規則去優化執行計劃

對於一些相對較複雜的SQL語句的時候,有時候可能會覺得你寫的SQL執行計劃效率不夠高,就會自動進行優化。

比如Mysql可能覺得你的SQL裡有很多括號,那麼無關緊要的括號就會給你刪除,其次比如i = 5 and j > i這樣的,就會改寫成 i = 5 and j > 5,做一個常量替換。

還比如b = b and a = a這種沒有意義的都會直接刪掉。

如果多表查詢的時候,select * from t1 join t2 on t1.x1=t2.x1 and t1.id=1,這個SQL明顯是針對t1表的id主鍵進行了查詢,同時還要跟t2表進行關聯,其實這個SQL語句就可能在執行前就先查詢t1表的id=1的資料,然後直接做一個替換,把SQL替換為:select t1表中id=1的那行資料的各個欄位的常量值, t2.* from t1 join t2 on t1表裡x1欄位的常量值=t2.x1

對於子查詢,如果子查詢的where條件依賴於外面表的欄位,那麼查詢效率是很低的,那麼就不是先執行子查詢,在執行外面的查詢,而變成遍歷外面表裡的每一條資料放到子查詢裡去執行,然後找到這條資料的值,在拿到外層去判斷,是否符合條件,比如:select * from t1 where x1 = (select x1 from t2 where t1.x2=t2.x2)

explain

id:如果複雜的SQL裡可能會很多個Select,也可能會包含多條執行計劃,每條執行計劃都會有一個唯一的id,如果一個select涉及到多個表,那麼多條執行計劃的id是一樣的。

select_type:這一條執行計劃對應的查詢是什麼查詢型別,一般單表連線或者多表連線查詢的select_type都是SIMPLE,然後有子查詢的時候,主查詢就是PRIMARY,子查詢就是SUBQUERY,如果是union語句的話,第一條執行的是PRIMARY,第二條就是UNION,而且由於要合併兩個查詢結果,還有會第三條執行計劃,這時的select_type就是union_result。

table:表名,要查詢哪個表,如果有臨時表,那麼就會有類似< derived2 > 這樣的通過臨時物化表為要查詢的表。

partitions:表分割槽的概念

type:針對當前這個表的訪問方式,包括const,ref,range等等。如果被驅動表基於主鍵進行等值匹配,那麼查詢方式就是eq_ref。

possible_keys:你type確定訪問方式,那麼哪些索引是可供選擇的。

key:在possible_keys裡實際選擇的那個索引

key_len:索引的長度

ref:使用某個欄位的索引進行等值匹配搜尋的時候,索引列進行等值匹配的那個目標值的一些資訊

rows:是預估通過索引或者別的方式訪問這個表的時候,大概可能會讀取多少條資料。

filtered:經過搜尋條件過濾之後的剩餘資料的百分比。

extra:一些額外的資訊,比如Unsing index就代表僅僅在二級索引裡執行,沒有回表操作;Using index conndition表示過濾索引後找到所有符合索引條件的資料行,然後用where語句的其他條件去過濾這些資料行;Using where表示優化器需要通過索引回表查詢資料。

如果你的關聯條件並不是索引,那麼就會用到join buffer的記憶體技術來提升關聯的效能

如果我們的排序沒有用到索引的時候,那麼就要基於記憶體或磁碟檔案來排序,大部分都得基於磁碟檔案來排序,這時候就會Using filesort來進行排序,效能是很差的

如果我們group by,union,distinct之類的語法,沒有利用到索引來進行分組聚合,那麼就需要基於臨時表來完成,也有大量的磁碟操作,Using temporary,效能也是很低的

所以我們要儘可能合理優化索引,保證執行計劃每個步驟都可以基於索引執行,避免掃描過多的資料。

例子:

EXPLAIN SELECT * FROM t1 WHERE x1 IN (SELECT x1 FROM t2) OR x3 = ‘xxxx’;

執行計劃為:

在這裡插入圖片描述
那麼從這裡可以看出,主查詢可能用到x3的索引,但是並沒有走索引樹,而是全表掃描了,那麼x3 = xx可能大部分的值都是xx,成本可能還不如全表掃描,所以走了全表掃描,子查詢裡用到了x1的索引,但是沒有什麼篩選條件,所以遍歷了x1的索引樹,index形式。

主從複製架構

主從複製架構歐,就是部署兩臺伺服器,每臺伺服器上都得有一個Mysql,其中一個Mysql是master(主節點),另外一個Mysql是slave(從節點)。

然後我們的系統平時連線到master節點寫入資料,也可以從裡面查詢資料,然後master節點會把寫入的資料自動複製到slave節點去,讓slave節點可以跟master節點有一抹一樣的資料。

在這裡插入圖片描述

那麼這種架構的意義就在於,如果主節點宕機了,那麼就可以到從節點寫入資料和查詢資料,因為主從資料是一致的。那麼就自燃實現了Mysql的高可用了,如果只有一個Mysql伺服器,如果這個伺服器宕機,那麼就會導致無法訪問資料庫,整個系統就會崩潰。

在這裡插入圖片描述

除了高可用之外,還有讀寫分離架構,也就是主節點寫入資料,從節點去查詢資料,如果請求過多,那麼就可以加從節點伺服器,分攤讀請求,這就是一主多從架構。

在這裡插入圖片描述

大多數公司來說,讀請求並沒有那麼高,所以並不是非得要做讀寫請求,但是高可用是必須要做的。

除此之外,還可以掛一個從庫,專門用來跑一些報表SQL語句,防止和其他從庫爭搶資源,因為報表SQL,往往要執行好幾秒。

主從複製的基本原理:

從庫有一個IO執行緒,跟主庫建立一個TCP連線,請求主庫傳輸binlog日誌給自己,然後主庫上有一個IO dump執行緒,就會負責通過這個TCP連線把binlog日誌傳輸給從庫的IO執行緒。

接著從庫的IO執行緒把讀取到的binlog日誌資料寫入到自己本地的relay日誌檔案中去,然後從庫上另外有一個SQL執行緒會服務relay日誌裡的內容,進行日誌重做,把所有在主庫執行過的增刪改操作,在從庫上重新做一邊,達到一個 還原資料的過程。

在這裡插入圖片描述

如何為Mysql搭建一套主從複製架構

主從複製配置:

首先要卻把主庫和從庫的server -id是不同的,其次就是主庫必須開啟binlog功能,才會寫binlog到本地磁碟。

1.主庫上要建立一個用於主從複製的賬號:
create user ‘backup_user’@‘192.168.31.%’ identified by ‘backup_123’;
grant replication slave on . to ‘backup_user’@‘192.168.31.%’;
flush privileges;

使用mysqldump工具把主庫在這個時刻的資料做一個全量備份,此時一定是不能允許系統操作主庫了,主庫資料不能有變動。

/usr/local/mysql/bin/mysqldump --single-transaction -uroot -proot --master-data=2 -A > backup.sql

master-data=2就是備份SQL檔案裡,要記錄一下此時主庫的binlog檔案和position號,為主從複製做準備的,且這些在backup.sql裡就有。

接著可以通過scp之類的命令把這個backup.sql檔案拷貝到你的從庫伺服器上去。

接著把backup.sql檔案裡的語句都執行一遍,包括database,table以及資料。

CHANGE MASTER TO MASTER_HOST=‘192.168.31.229’, MASTER_USER=‘backup_user’,MASTER_PASSWORD=‘backup_123’,MASTER_LOG_FILE=‘mysql -bin.000015’,MASTER_LOG_POS=1689;

接著執行一個開始進行主從複製的命令:start slave,再用show slave status檢視一下主從複製的狀態,如果看到Slave_IO_Running和Slave_SQL_Running都是Yes就是一切正常,主從開始複製了。

問題所在:

現在搭建出來的主從複製架構是一種非同步的方式,它不會管從庫到底有沒有收到日誌。

那麼這時候如果還沒有同步到從庫,結果主庫宕機了,此時資料就會丟失了,所以要將複製方式採取半同步,這就是你主庫寫入資料,日誌進入binlog之後,可以確保binlog日誌複製到從庫了,再告訴客戶端本次寫入事務是否成功。

半同步有兩種方式,第一種是AFTER_COMMIT方式,意思是主庫寫入日誌到binlog,等待binlog複製到從庫了,主庫就提交自己的本地事務,接著等待從庫返回給自己一個成功的響應,然後主庫提交事務成功的響應給客戶端。

另外一種是Mysql 5.7預設的方式,主庫把日誌寫入binlog,並且複製給從庫,等待從庫的響應,從庫返回成功後,主庫再提交,接著再返回事務成功的響應給客戶端。

搭建版同步只需要安裝一下版同步複製外掛就可以,現在主庫中安裝半同步複製外掛,同時開啟半同步複製:

install plugin rpl_semi_sync_master soname ‘semisync_master.so’;
set global rpl_semi_sync_master_enabled=on;
show plugins;

然後從庫也安裝這個外掛以及開啟半同步複製:

install plugin rpl_semi_sync_slave soname ‘semisync_slave.so’;
set global rpl_semi_sync_slave_enabled=on;
show plugins;

接著要重啟從庫的IO執行緒:stop slave io_thread; start slave io_thread;

然後在主庫上檢查一下半同步複製是否正常執行:show global status like ‘%semi%’;,如果看到了Rpl_semi_sync_master_status的狀態是ON,那麼就可以了。

GTID搭建方式:

除了傳統搭建方式外,還有GTID搭建方式,首先在主庫配置:

gtid_mode=on
enforce_gtid_consistency=on
log_bin=on

server_id=單獨設定一個
binlog_format=row

接著在從庫進行配置:

gtid_mode=on
enforce_gtid_consistency=on
log_slave_updates=1

server_id=單獨設定一個

接著按照之前的講解步驟在主庫建立好複製的賬號之後,就可以之前一樣進行操作了,比如在主庫dump出來一份資料,在從庫裡匯入這份資料,備份違建裡會有SET @@GLOBAL.GTID_PURGED=***一類的字樣,可以照著執行一下就可以了。

最後執行一下show master status,可以看到executed_gtid_set,裡面記錄的是執行過的GTID,接著執行一下SQL:select * from gtid_executed,可以查詢到,對比一下,就會發現對應上了。

主從複製延遲問題:

由於主庫寫入的資料很快,是併發寫入的,但是從庫是單個執行緒緩慢拉取資料,那麼就會導致從庫複製資料的速度比較慢,那麼半自動返回的時間也就會更長一些。

可以使用percona-toolkit工具集裡的pt-heartbeat工具,他會在主庫裡建立一個hearbeat表,然後會有一個執行緒定時更新這個表裡的時間戳欄位,從庫上就有一個monitor執行緒會負責檢查從庫同步過來的heartbeat表裡的時間戳,把時間戳跟當前的時間戳比較一下就知道同步落後了多長時間。

如果有延遲的話,如果做了讀寫分離,那麼寫入的資料不能立即在從庫裡讀取到,還沒有同步過去,所以為了加快複製速度,5.7版本已經支援並行複製了,可以在從庫裡設定slave_parallel_workers > 0,然後把slave_parallel_type設定為LOGICAL_CLOCK,就可以了。

如果要求立馬強制讀取到,可以在類似MyCat或者Sharding-Sphere之類的中介軟體裡設定強制讀寫都從主庫走,這樣你寫入的資料,強制從主庫裡讀取,就一定可以讀取到了。

基於主從複製實現故障轉移

一般生產環境裡用於進行資料庫高可用架構管理的工具是MHA,用perl指令碼寫的一個工具,這個工具專門用於監控主庫的狀態,如果感覺不對勁,就趕緊把從庫切換成主庫。

這個MHA自己也是需要單獨部署的,分為兩種節點,一個是Manager節點,一個是Node節點,Manager節點一般是單獨部署一臺機器的,Node節點一般是部署在每臺Mysql機器上的,因為Node節點得通過解析各個Mysql的日誌來進行一些操作。

Manager節點會通過探測叢集裡的Node節點去判斷各個Node所在機器上的Mysql執行是否正常,如果發現某個Master故障了,就直接把他的Slave提升為Master,然後讓其他Slave都掛到新的Master上去。

生產配置經驗

資料庫機器配置

一般資料庫機器配置最低在8核16G,正常是16核32G.

因為相對於Java程式的機器配置,資料庫的機器需要執行大量的磁碟IO操作,所以每個請求都比較耗時,所以機器的配置要高一些才能更快的反應請求。

對於8核16G的機器,每秒大概可以抗1,2千併發請求,如果再高一點,16核32G的機器每秒可以抗2,3k,甚至4k的併發也是可以的,如果達到上萬,那麼資料庫也是扛不住宕機的。

對於資料庫而言,如果可以,最好採用SSD固態硬碟,因為SSD讀寫的效能要高於機械硬碟很多,那麼抗住的併發量就會更多一些。

資料庫如果進行效能測試

首先要利用一些工具每秒秒發出1k個冰球,檢視它的CPU負載,磁碟IO負載,網路IO負載,記憶體負載等,然後檢視資料庫能否每秒處理掉這些請求。

根據逐步的測試,大致在一個負載壓力下,可以每秒抗多少請求。

QPS:表示每秒可以處理多少的請求。

TPS:每秒會處理多少次事務提交或者回滾。

所以TPS往往是指多少個事務執行完畢,是每秒處理完事務的數量。

IO相關的壓測效能指標:

1.IOPS
這個指的是機器隨機IO併發處理的能力,這個能力就是後臺IO將資料刷回磁盤裡的能力,如果太低,那麼就會導致刷回磁碟的效率不高。

2.吞吐量
每秒可以讀寫多少位元組的資料量,一般普通磁碟的順序寫入的吞吐量每秒都可以達到200MB左右

3.latency
往磁盤裡寫入一條資料的延遲,那麼延遲越低,速度就越快。

4.CPU負載
CPU負載過高,說明資料庫不能繼續往下壓測更高的QPS,否則CPU是吃不消的

5.網路負載
壓測到一定的QPS和TPS的時候,每秒鐘機器的網絡卡會輸入多少MB,輸出多少MB,網絡卡滿了也不能繼續壓測了

6.記憶體負載
壓測到一定情況,檢視機器記憶體耗費了多少,耗費過高,也不能繼續壓測

資料庫壓測工具

sysbench,這個工具可以自動在資料庫裡構造出來大量的資料,接著可以模擬幾千個執行緒併發的訪問你的資料庫,模擬出來各種事務提交到你的資料庫裡去。

1.安裝

curl -s https://packagecloud.io/install/repositories/akopytov/sysbench/script.rpm.sh | sudo bash

sudo yum -y install sysbench

sysbench --version

如果能看到sysbench版本號,就代表安裝成功了

2.執行壓測

sysbench --db-driver=mysql --time=300 --threads=10 --report-interval=1 --mysql-host=127.0.0.1 --mysql-port=3306 --mysql-user=test_user --mysql-password=test_user --mysql-db=test_db --tables=20 --table_size=1000000 oltp_read_write --db-ps-mode=disable prepare

在這裡插入圖片描述最後的prepare表示準備測試用的測試表和測試資料。

如果是run,則代表執行測試

3.全方位測試

通過更改模式來進行不同的壓測,比如綜合讀寫TPS,使用oltp_read_write模式:

sysbench --db-driver=mysql --time=300 --threads=10 --report-interval=1 --mysql-host=127.0.0.1 --mysql-port=3306 --mysql-user=test_user --mysql-password=test_user --mysql-db=test_db --tables=20 --table_size=1000000 oltp_read_write --db-ps-mode=disable run

其他還有隻讀效能,oltp_read_only

測試資料庫的刪除效能,使用的是oltp_delete模式

測試資料庫的更新索引欄位的效能,使用的是oltp_update_index模式

測試資料庫的更新非索引欄位的效能,使用的是oltp_update_non_index模式

測試資料庫的插入效能,使用的是oltp_insert模式

測試資料庫的寫入效能,使用的是oltp_write_only模式

最後壓測完之後,使用cleanup命令,清理資料:

sysbench --db-driver=mysql --time=300 --threads=10 --report-interval=1 --mysql-host=127.0.0.1 --mysql-port=3306 --mysql-user=test_user --mysql-password=test_user --mysql-db=test_db --tables=20 --table_size=1000000 oltp_read_write --db-ps-mode=disable cleanup

4.壓測報告

類似會出現這樣的東西:[ 22s ] thds: 10 tps: 380.99 qps: 7312.66 (r/w/o: 5132.99/1155.86/1321.35) lat (ms, 95%): 21.33 err/s: 0.00 reconn/s: 0.00

在這裡插入圖片描述
另外會有一個總的壓測報告:

在這裡插入圖片描述

壓測過程觀察機器效能

當你不停的增加執行緒數量,發現在資料庫下一個QPS的數值的同時,機器CPU,記憶體,網路和磁碟的負載已經非常高了,到了有一定風險的臨界值,此時就不能繼續增加執行緒數量和提高資料庫抗下QPS了

1.觀察機器的CPU負載

最常用的linux機器效能的命令,就是top命令,比如我們會看到如下這一行:

top - 15:52:00 up 42:35, 1 user, load average: 0.15, 0.05, 0.01

先來解釋一下這行資訊,這行資訊是最直觀可以看到機器的cpu負載情況的,首先15:52:00指的是當前時間,up 42:35指的是機器已經運行了多長時間,1 user就是說當前機器有1個使用者在使用。

最重要的是load average: 0.15, 0.05, 0.01這行資訊,他說的是CPU在1分鐘、5分鐘、15分鐘內的負載情況。

如果一個4核的CPU,出現了3.5,4,那麼說明幾個CPU都跑滿了,就不能再往上提高了

2.觀察機器的記憶體負載

使用top命令之後可以看到:

Mem: 33554432k total, 20971520k used, 12268339 free, 307200k buffers

這裡說的就是當前機器的記憶體使用情況,這個其實很簡單,明顯可以看出來就是總記憶體大概有32GB,已經使用了20GB左右的記憶體,還有10多G的記憶體是空閒的,然後有大概300MB左右的記憶體用作OS核心的緩衝區了。

一般來說記憶體的使用率到了70%~80%,就有點危險了,就不能再繼續增加壓測的執行緒數量核QPS了

3.磁碟IO情況

使用dstat -d命令可以看到:

在這裡插入圖片描述

在上面可以清晰看到,儲存的IO吞吐量是每秒鐘讀取103kb的資料,每秒寫入211kb的資料,像這個儲存IO吞吐量基本上都不算多的,因為普通的機械硬碟都可以做到每秒鐘上百MB的讀寫資料量。

使用dstat -r命令可以看到:

在這裡插入圖片描述

他的這個意思就是讀IOPS和寫IOPS分別是多少,也就是說隨機磁碟讀取每秒鐘多少次,隨機磁碟寫入每秒鐘執行多少次,大概就是這個意思,一般來說,隨機磁碟讀寫每秒在兩三百次都是可以承受的。

如果磁碟IO吞吐量達到上百MB或者讀寫次數為2,3百了,那麼就不要繼續增加了

4.觀察網絡卡的流量情況

接著我們可以使用dstat -n命令,可以看到如下的資訊:

在這裡插入圖片描述

這個說的就是每秒鐘網絡卡接收到流量有多少kb,每秒鐘通過網絡卡傳送出去的流量有多少kb,通常來說,如果你的機器使用的是千兆網絡卡,那麼每秒鐘網絡卡的總流量也就在100MB左右,甚至更低一些。

那麼總的來說,在硬體的一定合理的負載範圍內,把資料庫的QPS提高到最大,就是資料庫壓測的時候最合理的一個極限QPS值

部署監控系統

要針對資料庫搭建一個統一的視覺化監控平臺,雖然是DBA負責,但是我們也要對這個資料庫視覺化監控的技術有一定的瞭解

簡單來說,Prometheus其實就是一個監控資料採集和儲存系統,他可以利用監控資料採集元件(比如mysql_exporter)從你指定的MySQL資料庫中採集他需要的監控資料,然後他自己有一個時序資料庫,他會把採集到的監控資料放入自己的時序資料庫中,其實本質就是儲存在磁碟檔案裡。

我們採集到了MySQL的監控資料還不夠,現在我們還要去看這些資料組成的一些報表,所以此時就需要使用Grafana了,Grafana就是一個視覺化的監控資料展示系統,他可以把Prometheus採集到的大量的MySQL監控資料展示成各種精美的報表,讓我們可以直觀的看到MySQL的監控情況。

通過多個Buffer Pool來提高效能

如果是併發訪問這個Buffer Pool,且都在訪問記憶體裡一些共享的資料結構,比如快取頁,各種連結串列之類的,那麼就需要對此進行加鎖,比如說載入資料頁到快取頁,更新free連結串列,更新lru連結串列,然後釋放鎖,接著下一個執行緒再執行一系列操作。

那麼我們可以設定多個Buffer Pool來優化併發能力,一般來說Mysql預設的規則是,如果你給Buffer Pool分配的記憶體小於1GB,那麼最多就只會給你一個Buffer Pool,但是如果你的機器記憶體很大,那麼就會給Buffer Pool分配較大的記憶體,那麼此時你是可以同時設定多個Buffer Pool的,比如;

innodb_buffer_pool_size = 8589934592
innodb_buffer_pool_instances = 4

我們給Buffer Pool設定了8GB記憶體,然後設定了4個Buffer Pool,那麼每個Buffer Pool的大小就是2GB,那麼每個Buffer Pool負責管理一部分的快取頁和描述資料塊,有自己獨立的free,flush,lru連結串列等,那麼併發的時候就可以並行執行多個Buffer Pool,可以在不同的Buffer Pool中加鎖和執行自己的操作,提高數倍的併發效能提升。

在這裡插入圖片描述

通過chunk來支援資料庫執行期間的調整

Buffer Pool的大小一般是不能變的,如果你執行期間調整大了1倍,那要怎麼實現呢。

但是Mysql總會想一些辦法來進行優化,實際上,它涉及了一個chunk機制,也就是Buffer Pool是由多個chunk組成的,它的大小是innodb_buffer_pool_chunk_size引數,預設值是128MB。

那麼我們如果這時候想要擴大一倍,那就申請一系列的128MB大小的chunk就可以了,只要每個chunk是連續的128MB記憶體就可以了,然後把申請到的chunk記憶體分配給Buffer Pool就行了。

那麼Buffer Pool的結構為:

在這裡插入圖片描述

如何基於機器配置合理的Buffer Pool

由於記憶體裡除了給mysql的Buffer Pool以外,還有作業系統等等其他的記憶體,那麼通常Buffer Pool設定機器記憶體的50%~60%是比較合理的。

Buffer Pool的總大小 = (chunk大小 * Buffer Pool數量)的兩倍數

也就上面公式的2的倍數。

通過SHOW ENGINE INNODB STATUS命令,可以檢視innodb裡的具體情況,可以看到如下的東西:

S

主要講解這裡跟Buffer Pool相關的東西:

在這裡插入圖片描述

Linux作業系統的儲存系統軟體層原理以及IO排程優化原理

Linux作業系統分為VFS層,檔案系統層,Page Cache快取層,通用Block層,IO排程層,Block裝置驅動層,Block裝置層

在這裡插入圖片描述
當Mysql發起一次資料頁的隨機讀寫時,實際上會把磁碟IO請求交給LInux作業系統的VFS層,然後層層傳遞:

1.VFS層的作用是根據你是對哪個目錄下的檔案發起的讀寫IO請求,並把請求轉交給對應的檔案系統。

2.接著檔案系統現在Page Cache這個基於記憶體的快取裡找你要的資料,如果有,則基於記憶體快取來執行讀寫,如果沒有就繼續往下一層走。

3.這時候就會交給Block層,在這一層會把你對檔案的IO請求轉換為Block IO請求。

4.之後會把這個Block IO請求交給IO排程層,這一層預設是用CFQ公平排程演算法,也就是優先執行需要大量資料的IO操作,那麼耗時短的就一直等待,得不到機會,所以一般情況下,需要調整為deadline排程演算法,也就是任何一個IO操作都不能一直等待,在指定時間範圍內,必須讓他去執行。

5.IO排程完成之後,就會決定哪個IO請求先執行,哪個後執行,此時可以把執行的IO請求交給Block裝置驅動層,最後在傳送給真正的儲存硬體,Block裝置層。

6.完成讀寫操作之後,就要把相應經過反向依此返回,最終Mysql就可以得到本次IO讀寫操作的結果。

資料庫伺服器使用的RAID儲存架構

Mysql資料庫就是一套資料庫管理軟體而已,底層是磁碟來儲存資料,基於記憶體來提升資料讀寫效能,然後設計複雜的資料模型,幫助我們高效的儲存和管理資料。然後通過Linux作業系統提供的介面,來執行負責操作底層的硬體。

在這裡插入圖片描述
一般來說,很多資料庫部署在機器上的時候,儲存都是搭建的RAID儲存架構,RAID就是一個磁碟冗餘陣列。

一般磁碟不夠的時候就需要多加幾個磁碟,但是這樣我們就不知道要放在哪個磁碟上,從哪個磁碟取出資料,所以RAID技術就可以幫助我們選擇一塊磁碟寫入,讀取資料,除此之外,RAID技術很重要的作用就是他還可以實現資料冗餘機制,也就是可以把寫入的同一份資料,在兩塊磁碟上都寫入,這樣當一塊磁碟壞掉的時候,就可以從另一塊磁碟讀取冗餘資料出來,這一切都是RAID技術自動幫你管理的。

在這裡插入圖片描述

RAID儲存架構的電池充放電原理

使用RAID陣列的時候,一般會有一個RAID卡,這個卡是帶有一個快取的,然後我們把RAID的快取模式設定為write back,那麼所有寫入到磁碟陣列的資料,先會快取在RAID卡的快取裡,然後慢慢再寫入到磁碟陣列去,這樣就可以大幅度提升我們資料庫磁碟寫的效能。

但是如果突然斷電,那麼可能快取裡的資料就會丟失,所以RAID卡一般都配置有獨立的鋰電池或者電容,如果伺服器突然斷電,那麼RAID卡自己基於鋰電池來供電執行,就會趕緊把快取裡的資料寫入到陣列中的磁碟上。

在這裡插入圖片描述
但是鋰電池是存在效能衰減的問題,所以一般來說鋰電池都要配置定時充放電的,也就是每隔30~90填,就會自動對鋰電池充放電依此,這樣可以延長鋰電池的壽命和校準電池容量。

但是鋰電池在充放電的過程中,RAID的快取級別會從write back變成write through,通過RAID寫資料的時候,就直接變成磁碟寫了,那麼效能就會退化10倍以上,這時候往往會導致你的資料庫伺服器的RAID儲存定期的效能出現幾十倍的抖動,間接導致你的資料庫每隔一段時間就會出現效能幾十倍的抖動。

案例實戰

資料庫無法連線故障的定位,Too many connections

資料庫無法連線的問題,“ERROR 1040(HY000): Too many connections”,由於Mysql自己也有Socket連線池,所以當你配置的max_connections過低,就可能會導致這個問題,超過一定數量的連線,那我們調大就一定可以連線更多的麼,並不是。

通過show cariable like ‘max_connections’,可以看到連線可能要少於你設定的max_connections很多,這個原因是因為我們的Linux作業系統把進行可以開啟的檔案控制代碼限制為1024,那麼會導致Mysql最大連線數只能為214。

在這裡插入圖片描述
由於Linux上一個程序佔用過多的資源的話,其他程序可能會有使用受限,所以進行了限制,那麼我們通過以下方式進行修改:

1.ulimit -HSn 65535,修改控制代碼為65535

2.然後通過以下命令檢視最大檔案控制代碼數是否倍修改:

cat /etc/security/limits.conf

cat /etc/rc.local

如果都修改生效了,那麼我們重啟伺服器,然後重啟Mysql,最大連線數也就可以生效了。由於我們在生產環境部署一個系統,比如資料庫系統,訊息中介軟體系統,快取系統等等,都需要這個程序為主來執行,那麼通常控制代碼數都會設定為65535,要不然像kafka這樣的訊息中介軟體,可能無法建立足夠的執行緒,是無法執行的。

我們可以通過ulimit命令來設定每個程序被限制使用的資源量,用ulimit -a就可以看到程序被限制使用的各種資源的量

比如core file size代表的程序崩潰時候的轉儲檔案的大小限制,max locked memory就是最大鎖定記憶體大小,open file就是最大可以開啟的檔案控制代碼數量,max user processes就是最多可以擁有的子程序數量,設定之後,要確保變更落地到/etc/security/limits.conf檔案裡,永久性的設定程序的資源限制,所以要用第二步的命令檢查是否落地到配置檔案中去了。

資料庫抖動優化

第一種情況:Buffer Pool裡的快取頁,如果進行更新,就會變為髒頁,但如果Buffer Pool裡快取頁滿了,那麼就會根據LRU連結串列找最近最少訪問的快取頁去刷入磁碟。

萬一你要執行的一個查詢語句,需要查詢大量的資料到快取頁裡,那麼就會導致記憶體裡大量的髒頁需要淘汰出去刷入磁碟,才能騰出足夠的空間來執行這條查詢語句。那麼可能平時幾十毫秒的查詢語句,由於要等待大量髒頁flush,可能要幾秒才能執行。

在這裡插入圖片描述

第二種情況:

如果redo log buffer裡的redo log本身也會刷入到磁碟上的日誌檔案,那麼磁碟上的redo log檔案寫滿了,就會重新回到第一個日誌檔案再次寫入,這時候如果第一個日誌之檔案裡的redo log對應的記憶體裡的快取頁的資料都沒被重新整理到磁碟上的資料頁,那麼為了防止資料丟失,就要將這些資料對應的快取頁刷入到磁碟中,那麼這時候資料庫就會hang住,因為任何一個更新請求都要寫redo log,redo log這時候正在將快取頁刷入磁碟,為了可以覆蓋原來的資料。那麼必然要等待一定的時間才能完成,效能是很差的。

在這裡插入圖片描述

這兩種情況都會出現抖動,但是第一種情況很難進行優化,因為Buffer Pool的大小就那麼些,並不是無限大的,那麼只能採用大記憶體機器,給Buffer Pool分配的記憶體空間大一些,那麼快取頁填滿的速度也就低一些,flush磁碟的頻率也就更低點。

第二個的問題,就是要提升快取頁flush到磁碟的速度,刷入的越快,那麼執行時間也就越短。

由於SSD固態硬碟的隨機IO效能很高,所以選擇使用SSD固態硬碟,還有一個很關鍵的引數,就是innodb_io_capacity,這個引數是資料庫採用多大的IO速率把快取頁flush到磁碟的,如果你SSD能承載600次每秒IO,結果你設定為300,那麼並沒有把SSD固態硬碟的隨機IO效能發揮出來。

這裡可以使用fio工具測試磁碟最大隨機IO速率,然後通過這個數值給innodb_io_capacity設定,儘可能的全速率去flush快取頁到磁碟。

另外一個引數就是innodb_flush_neighbors,這個引數是flush快取頁到磁碟的時候,可能會控制把快取頁臨近的其他快取頁也刷到磁碟,那麼flush到磁碟的快取頁太多了,我們用SSD固態硬碟的時候,沒有必要同時刷臨近的快取頁,這時候設定為0,禁止刷臨近快取頁,那麼就把每次重新整理快取頁數量降低到最少了。

陌生人社交APP的Mysql索引設計實戰

針對社交APP,主要是使用者資訊表,可以叫做user_info這個表,這個表裡大概會有你的地區(你在哪個省份、哪個城市,這個很關鍵,否則不在一個城市,可能線上聊的好,線下見面的機會都沒有),性別,年齡,身高,體重,興趣愛好,性格特點,還有照片,當然肯定還有最近一次線上時間(否則半年都不上線APP了,你把他搜出來幹什麼呢?)

針對這個使用者表進行搜尋,不僅僅是篩選,還得支援分頁,所以肯定得跟上limit xx, xx的分頁語句,並且根據篩選出來的結果進行一個排序,那麼最終SQL語句可能類似於:select xx from user_info where xx=xx order by xx limit xx,xx。

但是類似下面:select xx from user_info where age between 20 and 25 order by score,那麼要麼基於age建立索引,但是score就用不到索引了,相反score用上索引,那麼age就用不到索引了。

那麼這種情況下,一般都會讓where條件去使用索引來快速篩選出來一部分指定資料,然後再進行排序,因為篩選出來的資料比較少,那麼後續排序和分頁的成本不會很大。

那麼對於社交APP系統來說,聯合索引裡先要放省份,城市,性別這三個欄位在最左側,雖然這幾個欄位的基數小,但是實際查詢的時候都要基於這幾個欄位,再加上其他欄位進行查詢,那麼還不如直接放在最左側,這樣跟其他欄位組成聯合索引後,大部分的查詢都可以通過索引樹可以把where條件指定的資料篩選出來。

那麼除了這三個以外,還加了一個年齡,但是where後面加上年齡的範圍之後,前面的性別沒有用上怎麼辦,這個時候可以寫成:where province=xx and city=xx and sex in (‘female’, ‘male’) and age >=xx and age<=xx。那麼整個where條件就都可以在索引樹裡進行篩選和搜尋了,除了這些以外,還有興趣,性格這些欄位,那我們可以設計成(province, city, sex, hobby, character, age)這樣的一個聯合索引,那麼還是按照之前的思路,即使不需要按性別和愛好進行篩選,也可以將這兩個欄位用in語句,把所有的列舉類值都放進去,那麼就順利的讓所有的欄位都可以用上索引。所以搜尋的大部分列舉有限的值索引放在前面,像age,name這種的不確定的值放在後面,那麼大部分查詢語句裡,都可以用in列舉值的方式,使用一個聯合索引了。

假設條件裡還有一個登陸時間小於7天的語句,那麼就沒辦法用上索引了,畢竟在age前進行範圍查詢的話,age就不能用到索引了,這時候可以將這種幾天內登陸過APP設定成一個true,false,也就是0,1欄位值,登陸過就是1,沒登陸就是0,那麼就可以放在age前,用等於1或0,或者in(1,0)的方式走索引,使後面的age可以用上索引。

那麼最終這個聯合索引就是(province, city, sex, hobby, character, does_login_in_latest_7_days, age)這樣的索引。這種索引差不多可以滿足80%的查詢要求。

那麼剩下的要求呢,如果有對基數低的欄位進行查詢,然後還要進行排序,比如查詢性別男,然後按照評分排序,這一下子篩選出所有的男性,可能有上百萬使用者資料,還有磁碟檔案進行排序再分頁,那麼效能是很差的,所以這裡要對sex和score都要走索引才行,那麼就可以設計聯合索引(sex,score),由於sex是基數低的欄位,那麼sex相等的情況下,都是按照score進行排序的,那麼就可以sex = 男,然後將性別確定後,裡面都是按照score順序排序的,那就可以order by score走索引了,然後去limit語句指定的資料分頁出來。

那麼通過對查詢場景的分析,用(province, city, sex, hobby, character, does_login_in_latest_7_days, age)這樣的聯合索引去抗下複雜的where條件篩選的查詢,那麼篩選出的資料量較少,接著進行排序和limit分頁,同時針對低基數字段篩選 + 評分排序的場景,可以設計(sex,score)的輔助索引來應對,定位一大片低基數字段對應的資料,然後按照索引順序走limit、語句獲取指定分頁的資料,速度同樣會很快。

千萬級使用者場景下運營系統SQL調優

一般儲存使用者資料的表分為兩張表,一個是用來儲存使用者的核心資料,比如id,name,暱稱,手機號之類的資訊,也就是users表,另一個表可能會儲存使用者的一些拓展資訊,比如家庭住址,興趣愛好,最近一次登入時間,也就是users_extent_info表。

SELECT COUNT(id) FROM users WHERE id IN (SELECT user_id FROM users_extent_info WHERE latest_login_time < xxxxx)

系統執行時候,這個SQL在千萬級大表的場景下,要花幾十秒才能跑出來,所以必須進行優化,通過explian來得到這條sql語句的執行計劃。

在這裡插入圖片描述
首先針對子查詢,是執行計劃裡的第三行實現的,針對users_extent_info使用了idx_login_time這個索引,做了range型別的索引範圍掃描,查出來4561條資料,沒有額外篩選,所以filtered是100%。

接著他這裡的METERIALIZED,表示這裡把子查詢的這些資料代表的結果集進行了物化,物化成了一個臨時表,這個臨時表物化一定會臨時落到磁碟檔案裡去,那麼過程就很慢了。

第二條執行計劃表示,對usrs表做了全表掃描,而且有Using join buffer,有join操作。

執行計劃第一條表示,這裡針對子查詢的產出的臨時物化表,也就是< subquery2 >,做了一個全表查詢,把裡面的資料都掃描了一遍,因為就是讓users表的每一條資料,都要去物化臨時表裡的資料進行join,所以users表裡的每一條資料只能去全表掃描一遍物化臨時表,結果也就有49651條資料的10%左右被篩選出來。

所以整個過程,不僅要做一次物化臨時表,落地磁碟,接著還全表掃描了users表的所有資料,每一條資料還要去沒有索引的物化臨時表裡在做一次全表掃描找匹配資料,那麼整個過程就很耗時了。

這裡執行完SQL的explain命令之後,看到執行計劃後,可以執行show warnning命令,此時顯示出來的內容為:/* select#1 */ select count(`d2.`users`.`user_id``) AS `COUNT(users.user_id)`。

這就顯而易見了,在生成執行計劃的時候,自動把一個普通的IN子句優化成了semi join來進行IN + 子查詢的操作,semi join簡單來說就是對users表裡的每一條資料,去對物化臨時表全表掃描做semi join,不需要把users表裡的資料真的跟物化臨時表裡的資料join上,只要users表裡的一條資料,在物化臨時表裡可以找到匹配的資料,那麼users表裡的資料就會返回,這就叫做semi join,用來篩選的。

所以慢,那麼既然知道了semi join和物化臨時表導致的,那麼我們先關掉半連線優化,執行SET optimizer_switch=‘semijoin=off’,這時候就會發現,效能提升了幾十倍,變成了100多ms,由於自動執行semi join半連線優化,一旦禁止掉semi join自動優化,用正常的方式讓他基於索引去執行,效能都很高的,但是一般生產環境是不能隨意更改這些設定的。

那麼不音響語義的情況下, 儘可能去改變SQL語句的結構和格式,最終變為:

SELECT COUNT(id) FROM users WHERE ( id IN (SELECT user_id FROM users_extent_info WHERE latest_login_time < xxxxx) OR id IN (SELECT user_id FROM users_extent_info WHERE latest_login_time < -1))

由於後面的第二個子查詢,根本不可能成立,沒有小於-1的,但是我們發現改變了SQL寫法之後,執行計劃也隨之改變,並沒有再進行semi join優化,而是正常用了子查詢,主查詢也是基於索引去執行的,那麼我們在線上SQL語句效能提升至幾百毫秒。

億級資料量商品系統的SQL調優實戰

突然一個SQL語句導致慢查詢,結果就導致上千個請求等待,然後商品系統本身也大量的報警說查詢資料超時異常。

select * from products where category=‘xx’ and sub_category=‘xx’ order by id desc limit xx,xx

當時有所以KEY index_category(category, sub_category),所以上面的絕對會走索引才對的。

通過explain發現,possible_keys裡是由index_category的,但是實際用的key不是這個索引,而是PRIMARY,聚簇索引,而且Extra裡清晰謝了Using where,所以效能才會慢。

那麼結果也就是Mysql使用了錯誤的執行計劃,這時候,就可以使用force index語法,強行使用index_category,變成:

select * from products force index(index_category) where category=‘xx’ and sub_category=‘xx’ order by id desc limit xx,xx

但是為什麼這個SQL會選擇對主鍵的聚簇索引進行掃描,沒有使用我們的二級索引,以前沒有問題,現在為什麼突然有走聚簇索引。

因為Mysql認為你用索引之後,查出來的資料太多,還得在臨時磁盤裡排序,因此效能可能會很差,不如直接掃描主鍵的聚簇索引,因為聚簇索引的都是按照id值排序的,所以掃描的時候,直接按主鍵id倒序掃描過去就可以了,那麼就會導致在聚簇索引進行全表掃描的操作,就會出現幾十秒的情況。然後這一次可能多了一些新的商品類和子類,然後資料庫裡還沒有這類的商品,之前是找到返回值後立馬進行返回的,這回由於沒有找到,就全表掃描了,從頭到尾,所以才會出現慢查詢的情況。

商品幾十萬評論的深分頁問題

評論表的分頁查詢的SQL語句:

SELECT * FROM comments WHERE product_id =‘xx’ and is_good_comment=‘1’ ORDER BY id desc LIMIT 100000,20

由於要看第5000頁評論,那麼此時limit的offset就會是(5001 - 1) * 20,20就是每一頁的數量,所以offset就是100000,那麼最核心的索引就是index_product_id,那麼正常情況下,肯定會走這個索引的,然後從表裡篩選出來指定商品的評論資料。

第二部就是按照is_good_comment = '1’條件,篩選出這個商品評論的所有好評,但是索引並沒有這個欄位,那麼就只能進行回表操作,也就是說,每一條評論都要回表一次,然後根據id找到那條資料,取出is_good_comment欄位的值和1條件做比對,篩選出符合條件的資料。

雖然都是根據id在聚簇索引裡回表,但是有幾十萬條,效能很差的,之後還要根據id進行倒序排序,還得基於臨時硬碟檔案進行倒序排序,又得耗時很久,最後再獲取第5001頁的20條資料,結果這條SQL基本就要跑1~2s。

我們可以改造成:SELECT * from comments a,(SELECT id FROM comments WHERE product_id =‘xx’ and is_good_comment=‘1’ ORDER BY id desc LIMIT 100000,20) b WHERE a.id=b.id

這樣就會先執行子查詢,反而會使用聚簇索引,按照id的倒序方向進行掃描,並把符合where要求的條件資料給篩選出來,篩選出10w條資料後,再加20條資料就可以取到了符合要求的20條的主鍵id,然後再到主查詢的時候,通過這主鍵id回表查到完整資料就可以了。

千萬級資料刪除導致的慢查詢

當時的情況基本上就是單行查詢,應該不會出現按查詢,都是根據索引查詢的,效能應該很高的, 那麼可能是另一種情況,不是SQL的問題,而是Mysql生產伺服器的問題,由於Mysql伺服器自己的負載過高,導致SQL語句執行很慢。

比如現在Mysql伺服器的磁碟IO負載特別高,每秒執行大量的高負載的隨機IO,但是磁碟本身每秒能執行的隨機IO是有限的。結果就是你正常的SQL語句去執行的時候,磁碟太繁忙,顧不上你這個SQL,本來很快執行完的,也可能變成慢查詢,還有就是網路負載很高,等待Mysql連線就要很久,寬頻打滿了,返回資料結果都發布出去,也會變成慢查詢,另外就是CPU負載過高,導致去執行別的任務,沒時間執行SQL語句。

這時候應該排查當時Mysql伺服器的負載,看看磁碟,網路以及CPU的負載是否正常。

加入某個離線作業瞬間大批量的把資料往Mysql裡灌入的時候,一瞬間伺服器磁碟,網路以及CPU負載就會超高,這時候正常的SQL也會變成慢查詢,如果發現一切正常,SQL本身也沒問題,看執行計劃也正常,Mysql伺服器的負載也正常,那麼就需要第三種,用MySQL profiling工具去細緻的分析SQL語句的執行過程和耗時。

首先開啟profiling,使用set profiling = 1這個命令,接著Mysql就會自動記錄查詢語句的profiling資訊了,這時候執行show profiles命令,就會列出各種查詢語句的profiling資訊,會記錄下來每個查詢語句的query id,所以你要針對你需要分析的query,找到他的query id,檢視他的profiling具體資訊,使用show profile cpu,block io for query xx,這裡的xx是數字,這時就可以看到具體的profile資訊了。

除了cpu和block io之外,還可以看其他的各項,仔細檢查了這個SQL語句的profiling資訊,發現它的Sending Data耗時是最高的,幾乎使用了1s,Mysql官方解釋為:為一個Select語句讀取和處理資料行,同時傳送資料給客戶端的過程。

這時候又用一個show engine innodb status,看一下innodb儲存引擎的一些狀態發現一個奇怪的指標,就是history list length這個指標,非常高,達到了上萬的級別,這個指標就是資料undo log多版本快照連結串列長度,一般會根據多版本快照鏈條的自動purge清理機制,所以不會很高,如果過高,很可能是有的事務長時間執行,不能被purge清理,才導致這個history list length個值過高。

結果是因為後臺跑了一個定時任務,開了一個事務,然後一個事務裡刪除上千萬資料,導致這個事務一直在執行,因為刪除的時候,僅僅加了一個刪除標記,這時候同事執行的其他事務裡,查詢的時候可能要把千萬條資料都要掃描一遍,因為發現是刪除,然後繼續往下掃描,所以才會導致這麼慢。

大型電商網站的上億資料量的使用者表進行水平拆分

一般Mysql單表資料量不要超過1000w,最好是在500w以內,如果能控制在100w以內,效能基本不會有太大的問題。

那麼如果有幾千萬資料量,那麼就要把這個表拆分成比如100張表,那麼每張表也就幾十萬資料而已,其次可以把這100個表分散到多臺資料庫伺服器上去,一般一億行資料,大致在1GB到幾個GB之間的範圍。

所以綜上所述,可以完全分配兩個資料庫伺服器,放兩個庫,然後100張表均勻分散在2臺伺服器上就可以了,分的時候需要指定一個欄位來分,一般來說指定userid,根據使用者id進行hash之後,對錶取模,路由到一個表裡去,這樣就可以讓資料均勻分散。

如果登陸的時候,沒有userid,而是根據username,那麼可以建立一個索引對映表,也就是搞一個表結構為(username,userid)的索引對映表,把username和userid一一對映,然後針對username再做一次分庫分表。

那麼使用者登入的時候,就可以根據username先去索引對映表裡查詢對應的userid,然後按照userid分庫分表到一個表裡去,找到完整資料即可,雖然效能上有所損耗,但是比放在一個表裡的效能要高得多。

如果要針對不同的欄位,手機號,住址,年齡,性別等,那麼就要對你的使用者資料表進行binlog監聽,把搜尋的所有欄位同步到ElasticSearch裡,建立好搜尋的索引,然後在定位到一批userid。