MySQL索引、基礎補充以及優化筆記-上
MySQL索引、基礎補充以及優化筆記-上
MyISAM儲存引擎索引實現
MyISAM中為非聚集索引,也就是:索引,資料分開儲存。索引儲存在MYI檔案中,資料儲存在MYD檔案中。在搜尋資料時,先判斷查詢欄位是否有索引,如果有則開始從MYI檔案中的根節點開始,定位索引元素。一個一個節點查詢,內部折半查詢,最終找到葉子節點。葉子節點中存的就是索引所在行的磁碟檔案地址。在根據這個地址,在MYD檔案中快速定位到對應行的位置,將資料進行取出。
Innodb儲存引擎索引實現
Innodb中為聚集索引,也就是:索引和資料合併儲存,表資料檔案本身就是按照B+樹組織的一個索引結構檔案,葉節點包含了完整的資料記錄
葉子節點中存放的是索引所在行的其他列資料。
非主鍵索引結構為:
同樣也是使用B+樹進行構建,葉子節點存放的是索引所在行的主鍵。
標準B+樹,相鄰葉子節點之間是一個單向連線,而mysql中,是B+樹一個變種,相鄰葉子節點之間存放的是一個雙向連線。並且每個葉子節點中還會有很小的一部分割槽域用來儲存相鄰葉子節點的地址。
Hash索引儲存結構
很多時候Hash索引要比B+樹索引更高效,僅滿足“=”,“IN”,不支援範圍查詢。但是工作中基本不用Hash索引,因為它不支援範圍查詢,不支援模糊查詢,並且可能存在hash衝突問題。
面試題:為什麼建議InnoDB表必須建立主鍵,並且推薦使用整型的自增主鍵?
答:如果沒有建立主鍵索引,則會由Innodb幫你尋找一個主鍵,逐列尋找一個所有元素都不相同的列。也可以說對這個列添加了一個唯一索引。然後會使用這個列來組織整張表的所有資料。如果所有列都不滿足元素不同這個要求,mysql會幫這張表維護一個整型自增的隱藏列,類似於Rowid,並使用這個列維護整張表的結構。所以,如果沒有主鍵MySQL會多做很多事情,浪費資料庫資源。
查詢資料的時候,免不了資料大小比對。使用整型比大小會更快,效率更高。其次,正式開發環境下使用的SSD高速儲存硬碟會十分昂貴,所以應該儘量減少資料儲存的空間量。如果使用UUID作為主鍵,則會多出很多不必要的儲存量。那為何需要自增呢?
聯合索引的底層儲存結構
底層也為B+樹。根據條件依次對比,先後分別比對name,age,position。如果結果唯一則停止後續條件比對。
相關優化操作:如果將條件拆開分別使用三條查詢語句:
SELECT * FROM employees WHERE name = 'Bill' and age = 31;
SELECT * FROM employees WHERE age = 31 and position = 'dev';
SELECT * FROM employees WHERE position = 'manager';
則只有第一條資料會使用索引,剩餘兩條都不會使用索引,可以通過EXPLAIN命令進行查詢。所以為了在查詢時使用索引,則必須按照建立索引的順序進行查詢。本例中必須按照name,age,position的順序進行查詢,才會使用索引。
SELECT * FROM employees WHERE name = 'Bill' and age = 31 and position = 'dev';
在最左字首索引中,在相同的前置條件中,後續條件會依次排序。在本例子中,只有相同name條件下的age才是有序排列的。
MySQL讀寫分離
主從同步
主節點先將資料儲存到data庫中,再將sql語句存入到binlog檔案中。從節點中存在兩個執行緒,第一個執行緒為I/O執行緒,用來進行io讀寫,將主節點中的資料進行拷貝,放入從節點中的relay binlog檔案中(中繼日誌)。第二個執行緒為SQL執行緒,用來從relay binlog中一條一條讀出sql語句,並對從節點中的data庫進行資料操作。
show binary logs; // 列出伺服器上的二進位制日誌檔案
show master status; //顯示主節點正在使用的二進位制日誌檔案以及狀態
show binlog events in 'binlog name'; //顯示binlog檔案的日誌內容
binlog檔案中只會儲存對資料檔案的修改操作。binlog檔案還可以用來恢復資料庫檔案。
弊端:犧牲了資料庫之間的一致性可能導致髒讀,並且存在一定的延遲。可能存在資料庫之間同步中斷的問題(因為網路問題導致資料並沒有同步)。
對於資料庫不滿足一致性而導致髒讀情況的問題,可以使用強制查詢主庫的方式來解決。
對於同步中斷問題的解決方法是:採用半同步方法,使用了第三發的外掛來實現。具體過程為,web服務端將語句傳送到主節點進行執行,要等到結果存入主節點的data庫並且將sql語句存入到從節點的relay binlog檔案後返回給主節點一個通知,此時主節點才會返回語句成功執行的結果通知。
企業中常用的架構方案
一主一從:用來做資料的備份,提高資料可用性。不需要考慮資料一致性。但是不能代替資料的備份,因為如果在主節點清空了表,那麼從節點也會清空表。所以只能用來做容災,資料該備份還是要備份。如果資料丟失了,則可以使用快照恢復。binlog檔案雖然可以備份,但不是恢復資料的主要手段。
一主多從:從節點不適應過多,一般為2-4個從節點。否則會在主從同步上耗費大量的事件。如果為一主四從,則將三個從節點作為正常的讀寫,另外一個從節點做另外的特殊操作,比如十分耗時的整表操作等等。三個從節點中還需要選舉一個節點來作為主節點的備份節點,當主節點宕機以後來充當主節點。
雙主:業務場景中擁有大量的寫操作,單個主節點承受不來,則使用雙主架構。根據寫操作的id進行一個取模操作,或者id是單數則在一個主節點完成,為雙數則在另外一個主節點完成。如果是一個字串,則可以先hash,再把結果取模。如果一個節點宕機,整個資料都會很混亂。
級聯同步:解決master節點壓力過大,主節點只將資料同步給一個從節點。這個從節點再見資料同步給另外的多個從節點。好處是:分散主節點的壓力,其次如果主節點宕機以後,剩下的部分是一個天然的主從結構。弊端是:如果主節點同步的從節點宕機以後,二級從節點就變成了單獨節點。
環形多主:多個主節點都可以接多個從節點,從而形成一個大型環形多主多從結構。弊端是:一個主節點宕機以後,整個結構全部不可使用。
讀寫分離正式結構:增加一個代理節點:
app server中所有的讀與寫的請求全部往代理節點中傳送。代理節點去解析請求,如果語句是寫入操作,則把該請求傳送至主節點,如果是讀操作,則把請求以負載均衡的方式傳送至從節點。我們需要將資料來源的連線配置到代理節點中即可。代理節點也會存在宕機和單點故障以及其他一些故障的可能性,可以通過橫向擴充代理節點彌補。
基於Atlas代理實現讀寫分離
實施流程:
- 修改主節點配置my.cnf
- 修改從節點配置my.cnf ,配置主節點地址、埠、密碼
- 安裝atlas
- 配置atlas
master my.cnf:
server-id=99 //master id 不能和叢集中其他mysql實列的id相同
log-bin=mysql-bin //binlog 檔案字首
binlog-do-db = xxx //對應需要同步的資料庫
//以下都是不同步的資料庫
binlog-ignore-db = information_schema
binlog-ignore-db = mysql
binlog-ignore-db = personalsite
binlog-ignore-db = test
配置完成後重啟資料庫。可以通過show master status;檢視主節點正在使用的二進位制日誌檔案。
slave my.cnf:
server-id=99 //slave id
log-bin=mysql-bin //可加可不加,如果這個節點需要向其他節點同步資料時,需要新增這項
replicate-do-db = xxx //對應需要同步的資料庫
//以下都是不同步的資料庫
replicate-ignore-db = information_schema
replicate-ignore-db = mysql
replicate-ignore-db = personalsite
replicate-ignore-db = test
動態配置主節點連線資訊
change master to master_host='127.0.0.1:3306',master_user='root',master_password='123456789';
配置前必須通過stop slave;
命令關閉從節點後,才可以執行此項動態配置。配置完後使用start slave;
命令再次開啟從節點。使用命令show slave status;
檢視從節點狀態。
將配置檔案放入對應的資料夾中:
編寫初始化指令碼 init.sql :
change master to master_host='127.0.0.1:3306',master_user='root',master_password='123456789';
reset slave;
start slave;
使用docker-compose 編寫yaml檔案:
slave2節點仿照slave1節點編寫,注意更換埠號。下面提前給出atlas的相關yaml配置:
atlas:下載相關的包,進入conf資料夾中修改test.conf配置檔案:
注意這邊的埠號使用的是容器中的埠號。這邊使用了名字來代替,實際使用應該轉換成相對應的地址。
pwds項中格式為 [使用者名稱]:[密碼],密碼是經過加密的。
後面的配置與日誌相關。
使用命令dc up -d
(docker compose 命令)啟動。後續連線資料庫,如果在從節點進行修改操作的話,確實可以修改從節點中的資料,但是這個操作並不會同步到主節點中。
MySQL基礎補充
SQL語句執行流程
聯結器:負責管理連線。流程為:建立一個新的連線,接著通過系統庫中的user表載入當前使用者對應的許可權,然後將許可權載入進連線管理物件,進行許可權校驗。知道當前連線可以進行哪些操作。連線建立完成以後,更改許可權相對應的操作時,此連線所對應的許可權操作並不會改變。必須要新連線重新建立時重新載入許可權時才會被更改。
之後會去到快取區中查詢是否有這條語句的快取結果(快取資料形式為鍵值對,key為查詢sql,value為sql執行結果)。如果快取區中存在對應的鍵值對,就直接從快取區中讀出結果集並返回。
如果快取中沒有找到,則會進入詞法分析器:進行詞法分析,語法分析。分析是什麼操作,校驗語法等使用方法是否正確。如果不正確就返回語法錯誤結果。
分析完成確保語句沒有問題後,進入語句優化器:執行計劃生產索引選擇。比如聯表查詢中會判斷查A表和查B表那個更快,以此來判斷先查哪個表來作為條件。那麼優化器根據自己的優化演算法進行選擇執行效率最好的一個方案(優化器認為,有時候不一定最好)。那麼確認了執行計劃後就準備開始執行了。
進行許可權校驗,如果沒有許可權就會返回錯誤資訊,如果有許可權就會呼叫資料庫引擎介面,返回引擎的執行結果。
接著進入執行器:呼叫資料庫引擎介面獲取查詢結果。
MySQL資料庫引擎都是按照外掛形式,提供給了服務層呼叫介面。所以資料庫引擎可以根據需要動態擴充套件。
資料庫引擎會呼叫對應的資料庫檔案進行查詢並記錄結果記錄集,結束後將結果記錄集返回給執行器。執行器則會將結果集放入到快取區中。最後將結果返回給客戶端。
每次對資料庫進行update等寫操作,對資料庫有修改的操作時,就會清除快取區。所以快取區適合讀多寫少的場景。需要注意的是mysql query cache 是對大小寫敏感的,因為Query Cache 在記憶體中是以 HASH 結構來進行對映,HASH 演算法基礎就是組成 SQL 語句的字元,所以 任何sql語句的改變重新cache,這也是專案開發中要建立sql語句書寫規範的原因吧
MySQL開啟快取操作為:my.cnf檔案中:配置query_cache_type
引數,設定為0 的時候則是永遠都不會使用快取,設定為1的時候無論查詢哪張表快取中存在對應資料就從快取中拿取結果集,設定為2的時候為按需使用快取,例如我需要給test表設定快取則需要執行SQL語句:select SQL_CACHE * from test;
show status like 'Qca%';
可以用來檢視快取的情況。
注意:查詢快取這個功能在MySQL 8.0 版本後被移除(雞肋)。
如果是一條更新語句,執行過程與查詢類似如果有快取,也是會用到快取。然後拿到查詢的語句,進行許可權校驗,接著進行相關更新操作,然後呼叫引擎 API 介面,寫入這一行資料,InnoDB 引擎把資料儲存在記憶體中,同時記錄 redo log,此時 redo log 進入 prepare 狀態,然後告訴執行器,執行完成了,隨時可以提交。執行器收到通知後記錄 binlog,然後呼叫引擎介面,提交 redo log 為提交狀態。更新完成。
更新語句執行流程如下:分析器---->許可權校驗---->執行器--->引擎---redo log(prepare 狀態)--->binlog--->redo log(commit狀態)
日誌模組
Bin Log 歸檔日誌(邏輯日誌)
- 二進位制日誌,採用二進位制編寫
- Binlog在MySQL的Server層實現(引擎共用)
- Binlog為邏輯日誌,記錄的是一條語句的原始邏輯,類似於“給 ID=2 這一行的 c 欄位加 1”
- Binlog不限大小,追加寫入,不會覆蓋以前的日誌
使用binlog,必須要前開啟這個功能。通過語句show variables like '%log_bin%;'
查詢是否開啟此項功能:
binlog
日誌有三種格式,可以通過binlog_format
引數指定。
- statement,記錄的內容是
SQL
語句原文但是有個問題,update_time=now()
這裡會獲取當前系統時間,直接執行會導致與原庫的資料不一致。為了解決這種問題,我們需要指定為row
。 - row,記錄的內容還包含操作的具體資料,記錄內容如下。
row
格式記錄的內容看不到詳細資訊,要通過mysqlbinlog
工具解析出來。update_time=now()
變成了具體的時間。通常情況下都是指定為row
,這樣可以為資料庫的恢復與同步帶來更好的可靠性。但是需要大量的容量來記錄,佔用空間大,恢復與同步時會更消耗IO
資源,影響執行速度。 - mixed,記錄的內容是前兩者的混合。
MySQL
會判斷這條SQL
語句是否可能引起資料不一致,如果是,就用row
格式,否則就用statement
格式。
事務執行過程中,先把日誌寫到binlog cache
,事務提交的時候,再把binlog cache
寫到binlog
檔案中。因為一個事務的binlog
不能被拆開,無論這個事務多大,也要確保一次性寫入,所以系統會給每個執行緒分配一個塊記憶體作為binlog cache
。我們可以通過binlog_cache_size
引數控制單個執行緒 binlog cache 大小,如果儲存內容超過了這個引數,就要暫存到磁碟(Swap
)。
Redo Log 重做日誌(物理日誌)
- Innodb引擎特有,是記錄InnoDB儲存引擎的事務日誌
- 記錄的是執行的結果
- 有一個大小限定,不會無限大
- MySQL的WAL機制(Write-Ahead-Logging),先寫日誌再寫磁碟
- 儲存的檔名為:ib_logfile*
- 以迴圈的方式寫入日誌檔案。不斷的寫與擦除,檔案1寫滿時,切換到檔案2,檔案2寫滿時,切換到檔案1
比如 MySQL
例項掛了或宕機了,重啟時,InnoDB
儲存引擎會使用redo log
恢復資料,保證資料的永續性與完整性。MySQL
中資料是以頁為單位,你查詢一條記錄,會從硬碟把一頁的資料加載出來,加載出來的資料叫資料頁,會放入到 Buffer Pool
中。後續的查詢都是先從 Buffer Pool
中找,沒有命中再去硬碟載入,減少硬碟 IO
開銷,提升效能。更新表資料的時候,也是如此,發現 Buffer Pool
裡存在要更新的資料,就直接在 Buffer Pool
裡更新。然後會把“在某個資料頁上做了什麼修改”記錄到重做日誌快取(redo log buffer
)裡,接著刷盤到 redo log
檔案裡。
刷盤策略:InnoDB
儲存引擎為 redo log
的刷盤策略提供了 innodb_flush_log_at_trx_commit
引數,它支援三種策略:
- 0 :設定為 0 的時候,表示每次事務提交時不進行刷盤操作
- 1 :設定為 1 的時候,表示每次事務提交時都將進行刷盤操作(預設值)
- 2 :設定為 2 的時候,表示每次事務提交時都只把 redo log buffer 內容寫入 page cache
另外,InnoDB
儲存引擎有一個後臺執行緒,每隔1
秒,就會把 redo log buffer
中的內容寫到檔案系統快取(page cache
),然後呼叫 fsync
刷盤。還有一種情況,當 redo log buffer
佔用的空間即將達到 innodb_log_buffer_size
一半的時候,後臺執行緒會主動刷盤。
日誌策略:redo log儲存檔案採用的是環形陣列形式,從頭開始寫,寫到末尾又回到頭迴圈寫,如下圖所示。
在個日誌檔案組中還有兩個重要的屬性,分別是 write pos、checkpoint
- write pos 是當前記錄的位置,一邊寫一邊後移
- checkpoint 是當前要擦除的位置,也是往後推移
每次刷盤 redo log
記錄到日誌檔案組中,write pos
位置就會後移更新。
每次 MySQL
載入日誌檔案組恢復資料時,會清空載入過的 redo log
記錄,並把 checkpoint
後移更新。
write pos
和 checkpoint
之間的還空著的部分可以用來寫入新的 redo log
記錄。
如果 write pos
追上 checkpoint
,表示日誌檔案組滿了,這時候不能再寫入新的 redo log
記錄,MySQL
得停下來,清空一些記錄,把 checkpoint
推進一下。