《高性能MySQL》&《MySQL技術內幕 InnoDB存儲引擎》筆記
第一章 MySQL架構與歷史
MySQL的架構
從上圖可以看出,MySQL數據庫區(qū)別于其他數據庫的最重要的一個特點就是其插件式的表存儲引擎。需要注意的是,存儲引擎是基于表的,而不是數據庫的(即同一個數據庫中的不同表可以有不同的存儲引擎)。
MySQL是一個單進程多線程架構的數據庫。
連接MySQL
連接MySQL是一個連接進程和MySQL數據庫實例進行通信。從程序設計的角度來說,本質上是進程通信。
連接MySQL的方式有:TCP/IP套接字、命名管道和共享內存、UNIX域套接字。
InnoDB與MyISAM存儲引擎對比
InnoDB支持事務、外鍵、行鎖;支持非鎖定讀,即默認讀取操作不會產生鎖。
InnoDB通過使用多版本并發(fā)控制(MVCC)來獲得高并發(fā)性,并且實現了SQL標準的4種隔離級別,默認為REPEATABLE級別。
提供了插入緩沖,二次寫,自適應哈希索引,預讀等高性能和高可用的功能。
對于表中數據的存儲,InnoDB存儲引擎采用了聚集的方式,因此每張表數據的存儲都是按照主鍵的順序進行存放(這種表稱為“索引組織表”)。
MyISAM(發(fā)音:my-z[ei]m)不支持事務、表鎖設計,支持全文索引(InnoDB已經支持)。
MyISAM相對簡單,所以在效率上要優(yōu)于InnoDB,小型應用可以考慮使用MyISAM。當你的數據庫有大量的寫入、更新操作而查詢比較少或者數據完整性要求比較高的時候就選擇InnoDB表。當你的數據庫主要以查詢?yōu)橹鳎啾容^而言更新和寫 入比較少,并且業(yè)務方面數據完整性要求不那么嚴格,就選擇MyISAM表。
第二章 InnoDB存儲引擎概述
內存
緩沖池
在數據庫系統中,由于CPU速度與磁盤速度之間的鴻溝,基于磁盤的數據庫系統通常使用緩沖池技術來提高數據庫的整體性能。
(注:上圖中左上角的日志緩沖應該為重做日志緩沖)
需要注意的是,頁從緩沖池刷新回磁盤的操作并不是在每次頁發(fā)生更新時觸發(fā),而是通過一種稱為Checkpoint的機制刷新回磁盤。
重做日志緩沖
重做日志緩沖一般不需要設置的很大,因為一般情況下每一秒鐘會將重做日志緩沖刷新到日志文件,因此用戶只需要保證每秒產生的事務量在這個緩沖大小之內即可。默認為8MB.
系統在以下三種情況下會將重做日志緩沖中的內容刷新到外部磁盤的重做日志文件中:
- Master Thread每一秒將重做日志緩沖刷新到重做日志文件;
- 每個事務提交時會將重做日志緩沖刷新到重做日志文件;
- 當重做日志緩沖池剩余空間小與1/2時,重做日志緩沖刷新到重做日志文件。
Checkpoint技術
為了避免發(fā)生數據丟失的問題,當前事務數據庫系統普遍都采用了Write Ahead Log策略,即當事務提交時,先寫重做日志,再修改頁。當由于宕機而導致數據丟失時,通過重做日志來完成數據的恢復。這也是事務ACID中D(Durability 持久性)的要求。
Checkpoint技術是用來解決以下幾個問題:
- 縮短數據庫的恢復時間;
- 緩沖池不夠用時,將臟頁刷新到磁盤;
- 重做日志不可用時,刷新臟頁。
當數據庫發(fā)生宕機時,數據庫不需要重做所有的日志,因為Checkpoint之前的頁都已經刷新回磁盤。故數據庫只需對Checkpoint后的重做日志進行恢復。這樣就大大縮短了恢復的時間。
此外,當緩沖池不夠用時,根據LRU算法會溢出最近最少使用的頁,若此頁為臟頁,那么需要強制執(zhí)行Checkpoint,將臟頁也就是頁的新版本刷新回磁盤。
重做日志出現不可用的情況是因為當前事務數據庫系統對重做日志的設計都是循環(huán)使用的,并不是讓其無限增大的。重做日志可以被重用的部分是指這部分重做日志已經不再需要,即當數據庫發(fā)生宕機時,數據庫恢復操作不需要這部分的重做日志,因此這部分就可以被覆蓋重用。若此時這部分重做日志還需要使用,那么必須強制產生Checkpoint,將緩沖池中的頁至少刷新到當前重做日志的位置。
對于InnoDB存儲引擎而言,其是通過LSN(Log Sequence Number)來標記版本的。而LSN是8字節(jié)的數字,其單位是字節(jié)。每個頁有LSN,重做日志中也有LSN,Checkpoint也有LSN。
第三章 文件
日志文件
錯誤日志
錯誤日志文件對MySQL的啟動、運行、關閉過程進行了記錄。MySQL DBA 在遇到問題時應該首先查看該文件以便定位問題。該文件不僅記錄了所有的錯誤信息,也記錄了一些警告信息或正確的信息。
慢查詢日志
可以在MySQL啟動時設置一個閾值,將運行時間超過該值的所有SQL語句都記錄到慢查詢日志文件中。該值默認為10秒。
查詢日志
查詢日志記錄了所有對MySQL數據庫請求的信息,無論這些請求是否得到了正確的執(zhí)行。
二進制日志
二進制日志(binary log)記錄了對MySQL數據庫執(zhí)行更改的所有操作。
二進制日志文件默認未開啟。手動開啟后會使系統性能下降大概1%.
但考慮到可以使用復制(replication)和point-in-time的恢復,這些性能損失絕對是可以且應該被接受的。
重做日志文件
在默認情況下,在InnoDB存儲引擎的數據目錄下會有兩個名為ib_logfile0和ib_logfile1的文件。這兩個文件就是重做日志文件,或者事務日志。
重做日志的目的:萬一實例或者介質失敗,重做日志文件就能派上用場。例如,數據庫由于所在主機掉電導致實例失敗,InnoDB存儲引擎會使用重做日志恢復到掉電前的時刻,以此來保證數據的完整性。
每個InnoDB存儲引擎至少有一個重做日志文件組,每個文件組下至少有2個重做日志文件,如默認的ib_logfile0、ib_logfile1。InnoDB存儲引擎先寫重做日志文件1,當達到文件的最后時,會切換至重做日志文件2,當重做日志文件2也被寫滿時,會再被切換到重做日志文件1中。
重做日志與二進制日志的區(qū)別:
二進制日志會記錄所有與mysql數據庫有關的日志記錄,包括InnoDB、MyISAM、Heap等其他存儲引擎的日志,而InnoDB存儲引擎的重做日志只記錄有關其本身的事務日志,
記錄的內容不同,不管你將二進制日志文件記錄的格式設為哪一種,其記錄的都是關于一個事務的具體操作內容,即該日志是邏輯日志;而InnoDB存儲引擎的重做日志文件記錄的關于每個頁的更改的物理情況;
寫入的時間也不同,二進制日志文件是在事務提交前進行提交,即只寫磁盤一次,不論這時該事務多大;而在事務進行的過程中,不斷有重做日志條目被寫入重做日志文件中。
第四章 之一 表
索引組織表
在InnoDB存儲引擎中,表都是根據主鍵順序組織存放的,這種存儲方式的表稱為索引組織表(index organized table)。在InnoDB存儲引擎表中,每張表都有個主鍵,如果在創(chuàng)建表時沒有顯式定義主鍵,則InnoDB存儲引擎會按如下方式選擇或創(chuàng)建主鍵:
- 首先判斷表中是否存在非空的唯一索引(Unique NOT NULL),如果有,則該列即為主鍵;
- 如果不符合上述條件,InnoDB存儲引擎會自動創(chuàng)建一個6字節(jié)大小的指針;
對于其他的一些數據庫,如Microsoft SQL Server數據庫,其中一種稱為堆表的表類型,即行數據的存儲按照插入的順序存放。堆表的特性決定了堆表上的索引都是非聚集的。
需要牢記的是,B+樹索引本身并不能找到具體的一條記錄,能找到的只是該記錄所在的頁。數據庫把頁載入到內存,然后通過Page Directory再進行二叉查找。只不過二叉查找的時間復雜度很低,同時在內存中的查找很快,因此通常忽略這部分查找所用的時間。
從InnoDB存儲引擎的邏輯存儲結構看,所有數據都被邏輯地存放在一個空間中,稱之為表空間(tablespace)。表空間又由段(segment)、區(qū)(extent)、頁(page)組成。頁在一些文檔中有時也稱為(block),InnoDB存儲引擎的邏輯存儲結構大致如圖:
VARCHAR
- MySQL數據庫的VARCHAR類型可以存放65535字節(jié)數據(除去別的開銷,實際最大可以存放65532字節(jié));
- VARCHAR(N)中的N是指字符數;
- 此外,此處65535長度是指所有VARCHAR列的長度總和,如果列的長度總和超出這個長度,依然無法創(chuàng)建,如:
CREATE TABLE test (
a VARCHAR(22000),
b VARCHAR(22000),
c VARCHAR(22000)
) CHARSET = latin1
分區(qū)表
分區(qū)的過程是將一個表或索引分解為多個更小、更可管理的部分。就訪問數據庫的應用而言,從邏輯上講,只有一個表或一個索引,但是在物理上這個表或索引可能由數十個物理分區(qū)組成。每個分區(qū)都是獨立的對象,都可獨自處理,也可以作為一個更大對象的一部分進行處理。
當前MySQL數據庫支持以下幾種類型的分區(qū):
- RANGE分區(qū):行數據基于屬于一個給定連續(xù)區(qū)間的列值放入分區(qū);
- LIST分區(qū):和RANGE類似,只是LIST分區(qū)里面是離散的值;
- HASH分區(qū):根據用戶自定義的表達式的返回值來進行分區(qū),返回值不能為負數;
- KEY分區(qū):根據MySQL數據庫提供的(即內置的)哈希函數進行分區(qū)。
分區(qū)和性能
數據庫應用分為兩類:一類是OLTP(在線事務處理),如Blog,電子商務,網絡游戲等;另一類是OLAP(在線分析處理),如數據倉庫,數據集市。在一個實際的應用環(huán)境中,可能既有OLTP的應用,也有OLAP的應用。如網絡游戲中,玩家的操作的游戲數據庫應用就是OLTP的,但是游戲廠商可能需要對游戲產生的日志進行分析,通過分析得到的結果來更好地服務于游戲,預測玩家的行為等,而這卻是OLAP的應用。
對于OLAP的應用,分區(qū)的確可以很好地提高查詢的性能,因為OLAP應用大多數查詢需要頻繁地掃描一張很大的表。假設有一張1億行的表,其中有一個時間戳屬性列。用戶的查詢需要從這張表中獲取一年的數據。如果按時間戳進行分區(qū),則只需要掃描相應的分區(qū)即可。
然而對于OLTP的應用,分區(qū)應該非常小心。在這種應用下,通常不可能會獲取一張大表中10%的數據,大部分都是通過索引返回幾條記錄即可。而根據B+樹索引的原理可知,對于一張大表,一般的B+樹需要2~3次的磁盤IO。因此B+樹可以很好地完成操作,不需要分區(qū)的幫助,并且設計不好的分區(qū)會帶來嚴重的性能問題。
例如:很多開發(fā)團隊會認為含有1000W行的表是一張非常大的表,所以他們往往會采用分區(qū),如對主鍵做10個HASH的分區(qū),這樣每個分區(qū)就只有100W的數據了,因此查詢應該變快了,如SELECT * FROM TABLE WHERE PK=@pk。但是有沒有考慮過這樣一種情況:100W和1000W行的數據本身構成的B+樹的層次都是一樣的可能都是2層。那么上述走主鍵分區(qū)的索引并不會帶來性能的提高。如果1000W的B+樹高度是3,100W的B+樹的高度是2,那么上述按主鍵分區(qū)的索引可以避免1次IO,從而提高查詢效率。這沒問題,但是這張表只有主鍵索引,沒有任何其他的列需要查詢的,如果還有類似如下的SQL語句:SELECT * FROM TABLE WHERE KEY = @key,這時對于KEY的查詢需要掃描所有的10個分區(qū),即使每個分區(qū)的查詢開銷為2次IO,則一共需要20次IO。而對于原來單表的設計,對于KEY的查詢只需要2~3次IO。
這里,MySQL數據庫的分區(qū)是局部分區(qū)索引,一個分區(qū)中既存放了數據又存放了索引。而全局分區(qū)是指,數據存放在各個分區(qū)中,但是所有數據的索引放在一個對象中。
——沒有全局的索引,所以才需要遍歷每個分區(qū)的索引。
第四章 之二 Schema與數據類型優(yōu)化
選擇優(yōu)化的數據類型
- 更小的通常更好;更小的數據類型通常更快,因為它們占用更少的磁盤、內存和CPU緩存,并且處理時需要的CPU周期也更少;
- 簡單就好;例如,整形比字符串操作代價更低;實用內建類型而不是字符串來存儲日期和時間;用整形存儲IP地址等;
- 盡量避免NULL;如果查詢中包含可為NULL的列,對MySQL來說更難優(yōu)化,因為可為NULL 的列使得索引、索引統計和值比較都更復雜。盡管把可為NULL的列改為NOT NULL帶來的性能提升比較小,但如果計劃在列上創(chuàng)建索引,就應該盡量避免設計成可為NULL的列;
字符串類型
VARCHAR 和 CHAR
VARCHAR是最常見的字符串類型。VARCHAR節(jié)省了存儲空間,所以對性能也有幫助。但是,由于行是可變的,在UPDATE時可能使行變得比原來更長,這就導致需要做額外的工作。如果一個行占用的空間增長,并且在頁內沒有更多的空間可以存儲,MyISAM會將行拆成不同的片段存儲;InnoDB則需要分裂頁來使行可以放進頁內。
下面這些情況使用VARCHAR是合適的:字符串的最大長度比平均長度大很多;列的更新很少,所以碎片不是問題;使用了像UTF-8這樣復雜的字符集,每個字符都使用不同的字節(jié)數進行存儲。
當存儲CHAR值時,MySQL會刪除所有的末尾空格。CHAR值會根據需要采用空格進行填充以方便比較。
CHAR適合存儲很短的字符串,或者所有值都接近同一個長度,如密碼的MD5值。對于經常變更的數據,CHAR也比VARCHAR更好,因為CHAR不容易產生碎片(行間碎片?)。
慷慨是不明智的
使用VARCHAR(5)和VARCHAR(200)存儲"hello"的空間開銷是一樣的。那么使用更短的列有什么優(yōu)勢嗎?
事實證明有很大的優(yōu)勢。更長的列會消耗更多的內存,因為MySQL通常會分配固定大小的內存塊來保存內部值。尤其是使用內存臨時表進行排序或其他操作時會特別糟糕。在利用磁盤臨時表進行排序時也同樣糟糕。
所以最好的策略是只分配真正需要的空間。
BLOB 和 TEXT
BLOB和TEXT都是為存儲很大的數據而設計的數據類型,分別采用二進制和字符方式存儲。
與其他類型不同,MySQL把每個BLOB和TEXT值當做一個獨立的對象去處理。當BLOB和TEXT值太大時,InnoDB會使用專門的“外部”存儲區(qū)域來進行存儲,此時每個值在行內需要1~4個字節(jié)存儲一個指針,然后在外部存儲區(qū)域存儲實際的值。
MySQL對BLOB和TEXT列進行排序與其他類型是不同的:它只對每個列的最前max_sort_length個字節(jié)而不是整個字符串做排序。同樣的,MySQL也不能將BLOB或TEXT列全部長度的字符串進行索引。
選擇表示符(identifier)
整數類型通常是標識列的最佳選擇,因為它們很快并且可以使用AUTO_INCREMENT。
如果可能,應該避免使用字符串類型作為標識列,因為它們很耗空間,并且比數字類型慢。
對于完全隨機的字符串也需要多加注意,例如MD5(),SHA1()或者UUID()產生的字符串。這些函數生成的新值會任意分布在很大的空間內,這會導致INSERT以及一些SELECT語句變得很慢:
- 因為插入值會隨機的寫入到索引的不同位置,所以使得INSERT語句更慢。這會導致葉分裂、磁盤隨機訪問。
- SELECT語句會變的更慢,因為邏輯上相鄰的行會分布在磁盤和內存的不同地方。
- 隨機值導致緩存對所有類型的查詢語句效果都很差,因為會使得緩存賴以工作的局部性原理失效。
第五章 創(chuàng)建高性能的索引 & 索引與算法
B+樹索引在數據庫中有一個特點是高扇出性,因此在數據庫中,B+樹的高度一般都在2~4層,這也就是說查找某一鍵值的行記錄時最多只需要2到4次IO。
數據庫中的B+樹索引可以分為聚集索引和輔助索引。聚集索引的葉子結點存放的是一整行記錄,而輔助索引葉子結點存放的是主鍵值。
許多數據庫的文檔這樣告訴讀者:聚集索引按照順序物理地存儲數據。但是試想一下,如果聚集索引必須按照特定順序存放物理記錄,則維護成本顯得非常之高。所以,聚集索引的存儲并不是物理上連續(xù)的,而是邏輯上連續(xù)的。這其中有兩點:一是前面說過的頁通過雙向鏈表連接,頁按照主鍵的順序排序;另一點是每個頁中的記錄也是通過雙向鏈表進行維護的,物理存儲上可以同樣不按照主鍵存儲。(《MySQL技術內幕 InnoDB存儲引擎》)
InnoDB只聚集在同一個頁面中數據,包含相鄰鍵值的頁面可能相距甚遠。(高性能MySQL)
索引可以包含一個或多個列的值。如果索引包含多個列,那么列的順序也十分重要,因為MySQL只能高效地使用索引的最左前綴列。創(chuàng)建一個包含兩個列的索引,和創(chuàng)建兩個只包含一列的索引是大不相同的。
索引的類型
B-Tree索引
B+樹,所有葉子節(jié)點在同一層,每一個葉子節(jié)點包含指向下一個葉子結點的指針。
B-Tree對索引列是順序組織存儲的,所以很適合查找范圍數據。
其中,索引對多個值進行排序的順序是與定義索引時列的順序一致的。
B-Tree索引適用于全鍵值、鍵值范圍或鍵前綴查找。其中鍵前綴查找只適用于根據最左前綴的查找。
- 全字匹配:和索引中的所有列進行匹配,如查找姓名為Cuba Allen、出生于1960-01-01的人;
- 匹配最左前綴:即只使用索引的第一列,如查找所有姓為Allen的人;
- 匹配列前綴:匹配某一列的值的開頭部分,如查找所有以J開頭的姓的人。這里只使用了索引的第一列;
- 匹配范圍值:如查找姓在Allen和Barrymore之間的人。這里也只使用了索引的第一列;
- 精確匹配某一列并范圍匹配另一列:如查找所有姓為Allen,并且名字是字母K開頭的人。即第一列全匹配,第二列范圍匹配;
- 只訪問索引的查詢:覆蓋索引;
如果不是按照索引的最左列開始查找,則無法使用索引。例如上面例子中的索引無法用于查找名字為Bill的人。類似的,也無法查找姓以某個字母結尾的人。
不能跳過索引中的列。也就是說,上述索引無法用于查找姓為Smith并且在某個特定日期出生的人。
如果查詢中有某個列的范圍查詢,則其右邊所有列都無法使用索引優(yōu)化查找。例如查詢WHERE 姓='Smith' AND 名 LIKE 'J%' AND 出生日期='1976-12-23',這個查詢只能使用索引的前兩列,因為這里LIKE是一個范圍條件。(如果范圍查詢列值的數量有限,那么可以使用多個等于條件來代替范圍條件)
到這里讀者應該可以明白,前面提到的索引列的順序是多么重要:這些限制都和索引列的順序有關。在優(yōu)化性能的時候,可能需要使用相同的列但順序不同的索引來滿足不同類型的查詢需求。
哈希索引
哈希索引基于哈希表實現,只有精確匹配索引所有列的查詢才有效。對于每一行數據,存儲引擎都會對所有的列計算一個哈希碼,哈希索引將所有的哈希碼存儲在索引中,同時保持指向數據行的指針。
在MySQL中,只有Memory引起顯示支持哈希索引。
- 哈希索引數據并不是按照索引數據順序存儲的,所以無法用于排序;
- 哈希索引頁不支持部分索引列匹配查找,因為哈希索引始終是使用索引列的全部內容來計算哈希值的;
- 哈希索引只支持等值比較查詢,不支持任何范圍查詢;
InnoDB引擎有個特殊的功能叫做“自適應哈希索引”。當InnoDB注意到某些索引值被使用得非常頻繁時,它會在內存中基于B-Tree索引之上再創(chuàng)建一個哈希索引。這是一個完全自動的、內部的行為。
索引的優(yōu)點
最常見的B-Tree索引,按照順序存儲數據,所以可以用來做ORDER BY和GROUP BY操作。因為數據是有序的,所以B-Tree也就會將相關的列值都存儲在一起。最后,因為索引中存儲了實際的列值,所以某些查詢只使用索引就能夠完成查詢。據此特性,總結下來索引有如下三大優(yōu)點:
- 索引大大減少了服務器需要掃描的數據量;
- 索引可以幫助服務器避免排序和臨時表;
- 索引可以將隨機IO變?yōu)轫樞騃O;
評價一個索引是否適合某個查詢的“三星系統”:
- 索引將相關的記錄放到一起則獲得一星;
- 索引中的數據順序和查找中的排列順序一致則獲得二星;
- 索引中的列包含了查詢需要的全部列則獲得三星;
高性能的索引策略
獨立的列
索引列不能是表達式的一部分,也不能是函數的參數,否則不會使用索引。
如:SELECT actor_id FROM actor WHERE actor_id + 1 = 5
SELECT ... WHERE TO_DAYS(CURRENT_DATE) - TO_DAYS(date_col) <= 10;
前綴索引和索引選擇性
有時候需要索引很長的字符列,這會讓索引變得大且慢。一個策略是前面提到過的模擬哈希索引。但有時候這樣做還不夠,還可以做些什么呢?
通常可以索引開始的部分字符,這樣可以大大節(jié)約空間,從而提高索引效率。但這樣也會降低索引的選擇性。索引的選擇性是指,不重復的索引值和數據表的記錄總數的比值。索引的選擇性越高則查詢效率越高,因為可以在查詢時過濾掉更多的行。唯一索引的選擇性是1.
訣竅在與既要選擇足夠長的前綴以保證較高的選擇性,同時又不能太長,以便節(jié)約空間。
多列索引
很多人對多列索引的理解都不夠。一個常見的錯誤就是,為每個列創(chuàng)建獨立的索引,或者按照錯誤的順序建立多列索引。
對于如何選擇索引的列順序有一個經驗法則:將選擇性最高的列放到索引最前列(在沒有ORDER BY 或 GROUP BY的情況下)。
例如,在超市的銷售記錄表中:SELECT * FROM payment WHERE staff_id = 2 AND customer_id = 584,很自然的customer_id的選擇性更高些,所以多列索引的順序應該是(customer_id, staff_id)。
這樣做有一個地方需要注意,查詢的結果非常依賴與選定的具體值。例如,一個應用通常都有一個特殊的管理員賬號,系統中所有其他用戶都是這個用戶的好友,所以系統通常通過它向網站的所有其他用戶發(fā)送狀態(tài)和其他消息。這個賬號巨大的好友列表很容易導致網站出現服務器性能問題。
這實際上是一個非常典型的問題。任何的異常用戶,不僅僅是那些用于管理應用的設計糟糕的賬號會有同樣的問題;那些擁有大量好友、圖片、狀態(tài)、收藏的用戶,也會有前面提到的系統賬號同樣的問題。
從這個小案例可以看到經驗法則和推論在多數情況下是有用的,但要注意不要假設平均情況下的性能也能代表特殊情況下的性能,特殊情況可能會摧毀整個應用的性能。
聚簇索引
聚簇索引并不是一種單獨的索引類型,而是一種數據存儲方式。InnoDB的聚簇索引在同一個結構中保存了B-Tree索引和數據行。
在InnoDB中,聚簇索引“就是”表。
當表有聚簇索引時,它的數據行實際上存放在索引的葉子頁中。術語“聚簇”表示數據行和相鄰的鍵值緊湊的存儲在一起。因為無法同時把數據行存放在兩個不同的地方,所以一個表只能有一個聚簇索引。
InnoDB只能通過主鍵聚集索引!
如果沒有定義主鍵,InnoDB會選擇一個唯一的非空索引代替。如果沒有這樣的索引,InnoDB會隱式定義一個主鍵來作為聚簇索引。
MySQL中每個表都有一個聚簇索引(clustered index ),除此之外的表上的每個非聚簇索引都是二級索引,又叫輔助索引(secondary indexes)。
聚簇索引有一些重要的優(yōu)點:
- 可以把相關的數據保存在一起。例如實現電子郵箱時,可以根據用戶ID來聚集數據,這樣只需要從磁盤讀取少數的數據頁就能獲取某個用戶的全部郵件。如果沒有使用聚簇索引,則每封郵件可能都會導致一次磁盤I/O。
- 數據訪問更快。聚簇索引將索引和數據保存在同一個B-Tree中,因此從聚簇索引中獲取數據通常比在非聚簇索引中查找要快。
- 使用覆蓋索引掃描的查詢可以直接使用葉節(jié)點中的主鍵值。
同時,聚簇索引也有一些缺點:
- 聚簇索引最大限度提高了I/O密集型應用的性能,但如果數據全部都放在內存中,則訪問的順序就沒有那么重要了,聚簇索引也就沒什么優(yōu)勢了。
- 插入速度嚴重依賴插入順序。按照主鍵的順序插入式加載數據到InnoDB表中速度最快的方式。但如果不是按照主鍵順序加載數據,那么在加載完成后最好使用OPTIMIZE TABLE命令重新組織一下表。
- 更新聚簇索引列的代價很高,因為會強制InnoDB將每個被更新的行移動到新的位置。
- 基于聚簇索引的表在插入新行,或者主鍵被更新導致移動行的時候,可能面臨葉分裂的問題。
- 二級索引訪問需要兩次索引查找,而不是一次。
MyISAM/InnoDB的主鍵索引和二級索引
MyISAM的主鍵索引和(所有其他的)二級索引的葉子節(jié)點中保存的都是指向行的物理位置的指針。
InnoDB的主鍵索引的葉子結點是數據行;(所有其他的)二級索引的葉子節(jié)點中保存的是主鍵值。
這樣的策略減少了當出現行移動或數據頁分裂時二級索引的維護工作。使用主鍵值當做指針會讓二級索引占用更多的空間,換來的好處是,InnoDB在移動行時無需更新二級索引中的這個“指針”。
如果正在使用的InnoDB表沒有什么數據需要聚集,那么可以定義一個代理鍵作為主鍵,這種主鍵的數據應該和應用無關,最簡單的方法是使用AUTO_INCREMENT自增列。這樣可以保證數據行是按順序寫入,對于根據主鍵做關聯操作的性能也會更好。
最好避免隨機的(不連續(xù)且值的分布范圍非常大)聚簇索引,特別是對于I/O密集型的應用。例如,從性能的角度考慮,使用UUID來作為聚簇索引則會很糟糕:它使得聚簇索引的插入變得完全隨機,這是最壞的情況,使得數據沒有任何聚集特性。
覆蓋索引
如果一個索引包含(或者說覆蓋)所有需要查詢的字段的值,就可以使用索引來直接獲取列的數據,這樣就不再需要讀取數據行。我們稱這樣的索引為覆蓋索引。
例如,表inventory有一個多列索引(store_id, film_id),MySQL如果只需要訪問這兩列,就可以使用這個索引做覆蓋索引,如SELECT store_id, film_id FROM inventory.
利用索引掃描來做排序
只有當索引的列順序和ORDER BY子句的列順序完全一致,并且所有列的排序方向(倒序或正序)都一樣時,MySQL才能使用索引來對結果做排序。如果查詢需要關聯多張表,則只有當ORDER BY子句引用的字段全部為第一個表時,才能使用索引做排序。ORDER BY子句和查找型查詢的限制是一樣的:需要滿足索引的最左前綴的要求。
有一種情況下ORDER BY子句可以不滿足索引的最左前綴的要求,就是前導列為常量的時候。如果WHERE子句或JOIN子句中對這些列指定了常量,就可以“彌補”索引的不足。
例如,索引:UNIQUE KEY idx(rental_date, inventory_id, customer_id)
下面這個查詢?yōu)樗饕牡谝涣刑峁┝顺A織l件,而使用第二列進行排序,將兩列組合在一起,就形成了索引的最左前綴:
WHERE rental_date = '2005-05-25' ORDER BY inventory_id, customer_id DESC;
下面這個查詢也沒問題,因為ORDER BY使用的兩列就是索引的最左前綴:
WHERE rental_date > '2005-05-25' ORDER BY rental_date , inventory_id;
下面是一些不能使用索引做排序的查詢:
- 下面這個查詢使用了兩種不同的排序方向,但是索引列都是正序排序的:
- WHERE rental_date = '2005-05-25' ORDER BY inventory_id DESC, customer_id ASC;
- 下面這個查詢的ORDER BY子句中引用了一個不在索引中的列:
- WHERE rental_date = '2005-05-25' ORDER BY inventory_id, staff_id;
- 下面這個查詢的WHERE和ORDER BY中的列無法組合成索引的最左前綴:
- WHERE rental_date = '2005-05-25' ORDER BY customer_id ;
- 下面這個查詢在索引列的第一列上是范圍條件,所以MySQL無法使用索引的其余列:
- WHERE rental_date > '2005-05-25' ORDER BY inventory_id, customer_id;
冗余和重復索引
重復索引是指在相同的列上按相同的順序創(chuàng)建相同類型的索引。如:
CREATE TABLE test (
ID INT NOT NULL PRIMARY KEY,
A INT NOT NULL,
B INT NOT NULL,
UNIQUE(ID),
INDEX(ID)
) ENGINE=InnoDB;
事實上,MySQL的主鍵約束和唯一約束都是通過索引實現的,因此,上面的寫法實際上在相同的列上創(chuàng)建了三個重復的索引。
冗余索引和重復索引有一些不同。如果創(chuàng)建了索引(A, B),再創(chuàng)建索引(A)就是冗余索引,因為這只是前一個索引的前綴索引,索引(A, B)也可以當做索引(A)來使用。
大多數情況下都不需要冗余索引,應該盡量擴展已有的索引而不是創(chuàng)建新索引(如擴展索引(A)為(A,B))。但也有時候出于性能方面的考慮需要冗余索引,因為擴展已有的索引會導致其變大太大,從而影響其他使用該索引的查詢的性能。
一般來說,增加新索引會導致INSERT、UPDATE、DELETE等操作的速度變慢,特別是當新增索引后導致達到了內存瓶頸的時候。
第六章 鎖
開發(fā)多用戶、數據庫驅動的應用時,最大的一個難點是:一方面要最大程度地利用數據庫的并發(fā)訪問,另一方面還要確保每個用戶能以一致的方式讀取和修改數據。為此就有了鎖的機制,同時這也是數據庫系統區(qū)別于文件系統的一個關鍵特性。
鎖機制用于管理對共享資源的并發(fā)訪問。
InnoDB存儲引擎中的鎖
鎖的類型
InnoDB存儲引擎實現了如下兩種標準的行級鎖:
- 共享鎖(S Lock),允許事務讀一行數據;
- 排它鎖(X Lock),允許事務刪除或更新一行數據。
此外,InnoDB存儲引擎支持多粒度鎖定,這種鎖允許事務在行級上的鎖和表級上的鎖同時存在。為了支持在不同粒度上進行加鎖操作,InnoDB存儲引擎支持一種額外的鎖方式,稱之為意向鎖(Intention Lock)。意向鎖是將鎖定的對象分為多個層次,意向鎖意味著事務希望在更細粒度上進行加鎖。
例如,如果需要對記錄r上X鎖,那么分別需要對數據庫A、表、頁上意向鎖IX,最后對記錄r上X鎖。若其中任何一個步驟導致等待,那么該操作需要等待粗粒度鎖的完成。舉例來說,在對記錄r加X鎖之前,已經有事務對表1進行了S鎖,而當前事務需要對表1上IX鎖,由于不兼容,所以該事務需要等待表鎖操作的完成。
一致性非鎖定讀
一致性的非鎖定讀(consistent nonlocking read)是指InnoDB存儲引擎通過行多版本控制的方式來讀取當前時間數據庫中行的數據。如果讀取的行正在執(zhí)行DELETE或UPDATE操作,這是讀取操作不會因此去等待行上鎖的釋放。相反的,InnoDB存儲引擎會去讀取行的一個快照數據。
快照數據是指該行的之前版本的數據,該實現是通過undo段來完成。而undo用來在事務中回滾數據,因此快照數據本身是沒有額外的開銷。此外,讀取快照數據是不需要上鎖的,因為沒有事務需要對歷史的數據進行修改操作。
可以看到,非鎖定讀機制極大地提高了數據庫的并發(fā)性。在InnoDB存儲引擎的默認設置下,這是默認的讀取方式,即讀取不會占用和等待表上的鎖。
一個行記錄可能有不止一個快照數據,一般稱這種技術為多版本技術。由此帶來的并發(fā)控制,稱之為多版本并發(fā)控制(Multi Version Concurrency Control,MVCC)。
在READ COMMITTED事務隔離級別下,對于快照數據,一致性非鎖定讀總是讀取被鎖定行的最新一份快照數據(違反了事務ACID中的I的特性,即隔離性)。而在REPEATABLE READ事務隔離級別下,一致性非鎖定讀總是讀取事務開始時的行數據版本。
一致性鎖定讀
在某些情況下,用戶需要顯式的對數據庫讀取操作進行加鎖以保證數據邏輯的一致性。而這要求數據庫支持加鎖語句,即使是對于SELECT的只讀操作。InnoDB存儲引擎對于SELECT語句支持兩種一致性的鎖定讀操作:
- SELECT ... FOR UPDATE(X鎖)
- SELECT ... LOCK IN SHARE MODE(S鎖)
鎖的算法
行鎖的3種算法
InnoDB存儲引擎有3種行鎖的算法,分別是:
- Record Lock:單個行記錄上的鎖;
- Gap Lock:間隙鎖,鎖定一個范圍,但不包含記錄本身;
- Next-Key Lock:Gap Lock + Record Lock,鎖定一個范圍,并且鎖定記錄本身;
InnoDB對于行的查詢都是采用這種Next-Key Lock鎖定算法。
但查詢的索引含有唯一屬性時,InnoDB存儲引擎會對Next-Key Lock進行優(yōu)化,將其降級為Record Lock,即僅鎖住記錄本身,而不是范圍。如SELECT * FROM t WHERE pk = 5 FOR UPDATE;
解決Phantom Problem
在默認的事務隔離級別下,即REPEATABLE READ下,InnoDB存儲引擎采用Next-Key Locking機制來避免Phantom Problem(幻讀)。
Phantom Problem是指在同一事務下,連續(xù)兩次執(zhí)行相同的SQL語句可能會導致不同的結果,第二次的SQL語句可能會返回之前不存在的行(重點在記錄數不一樣)。
如下SQL語句:SELECT * FROM t WHERE pk > 2 FOR UPDATE,第一次返回a=5這條記錄;若這時另一個事務插入了4這個值,那么第二次執(zhí)行時將返回4和5.這與第一次得到的結果不同,違反了事務的隔離性,即當前事務能夠看到其他事務的結果。
InnoDB存儲引擎采用Next-Key Locking的算法避免Phantom Problem。對于上述SQL語句,其鎖住的不是5這個單值,而是對(2, +∞)這個范圍加了X鎖。因此任何對于這個范圍的插入都是不被允許的,從而避免Phantom Problem。
鎖問題
臟讀
所謂臟數據是指事務對緩沖池中行記錄的修改,并且還沒有被提交。
臟讀指的就是在不同的事務下,當前事務可以讀到另外事務未提交的數據,簡單來說就是可以讀到臟數據。
臟讀在生產環(huán)境中并不常見。臟讀發(fā)生的條件是需要事務的隔離級別為READ UNCOMMITTED,而目前絕大多數的數據庫都至少設置成READ COMMITTED。
不可重復讀
不可重復讀是指在一個事務內多次讀取同一數據集合。在這個事務還沒有結束時,另外一個事務也訪問該數據集合,并做了一些DML操作。這樣,由于第二個事務的修改,第一個事務兩次讀到的數據可能是不一樣的,這種情況稱為不可重復讀。
不可重復讀和臟讀的區(qū)別是:臟讀是讀到未提交的數據,而不可重復讀讀到的卻是已經提交的數據。
一般來說,不可重復讀的問題是可接受的,因為其讀到的數據是已經提交的,本身不會帶來很大的問題。因此,很多數據庫廠商,如Oracle、Microsoft SQL Server將其數據庫事務的默認隔離級別設置為READ COMMITTED,在這種隔離級別下允許不可重復讀的現象。
幻讀
一個事務按相同的查詢條件查詢之前檢索過的數據,確發(fā)現檢索出來的結果集條數變多或者減少(由其他事務插入、刪除的),類似產生幻覺。
注意到,Repeatable Read(可重復讀)隔離級別仍然避免不了幻讀。
By default, InnoDB operates in REPEATABLE READ transaction isolation level and with the innodb_locks_unsafe_for_binlog system variable disabled. In this case, InnoDB uses next-key locks for searches and index scans, which prevents phantom rows (see Section 13.6.8.5, “Avoidingthe Phantom Problem Using Next-Key Locking”).
13.2.8.5. Avoiding the PhantomProblem Using Next-Key Locking
To prevent phantoms, InnoDB uses an algorithm called next-key locking that combines index-row locking with gap locking.
You can use next-key locking to implement a uniqueness check in your application:If you read your data in share mode and do not see a duplicate for a row you are going to insert, then you can safely insert your row and know that the next-key lock set on the success or of your row during the read prevents anyone mean while inserting a duplicate for your row. Thus, the next-key locking enables you to “l(fā)ock” the nonexistence of something in your table.
我的理解是說,InnoDB提供了next-key locks,但需要應用程序自己去(手動)加鎖。manual里提供一個例子:
SELECT * FROM child WHERE id> 100 FOR UPDATE;
這樣,InnoDB會給id大于100的行(假如child表里有一行id為102),以及100-102,102+的gap都加上鎖。
可以使用show engine innodb status來查看是否給表加上了鎖。
在InnoDB存儲引擎中,通過使用Next-Key Locking算法來避免不可重復讀的問題。
——避免了不可重復讀,即實現了可重復讀,亦即實現了事務的隔離性。——避免了不可重復讀,即實現了可重復讀,亦即實現了事務的隔離性。
——避免了不可重復讀,即實現了可重復讀,亦即實現了事務的隔離性。
第七章 事務
事務(Transaction)是數據庫區(qū)別于文件系統的重要特性之一。
事務概述
理論上說,事務有著極其嚴格的定義,它必須同時滿足四個特性,即通常所說的事務的ACID特性。值得注意的是,雖然理論上定義了嚴格的事務要求,但是數據庫廠商出于各種目的,并沒有嚴格去滿足事務的ACID標準。如Oracle數據庫,其默認的事務隔離級別是READ COMMITTED,不滿足隔離性的要求。對于InnoDB存儲引擎而言,其默認的事務隔離級別是READ REPEATABLE,完全遵循和滿足事務的ACID特性。
A(Atomicity),原子性
原子性指整個數據庫事務是不可分割的工作單位。事務中的所有數據庫操作要么全部成功,要么全部撤銷。
C(Consistency)一致性
一致性指事務將數據庫從一種一致的狀態(tài)轉變?yōu)橄乱环N一致的狀態(tài)。在事務開始之前和結束之后,數據庫的完整性約束沒有破壞。
I(Isolation)隔離性
隔離性還有其他的稱呼,如并發(fā)控制、可串行化、鎖等。
事務的隔離性要求,事務提交前對其他事務不可見。通常這使用鎖來實現。
D(Durability),持久性
事務一旦提交,其結果就是永久性的。即使發(fā)生宕機等事故,數據庫也能將數據恢復。
事務的實現
事務隔離性由鎖來實現。原子性、一致性、持久性通過數據庫的redo log和undo log來完成。redo log稱為重做日志,用來保證事務的原子性和持久性。undo log用來保證事務的一致性。
redo和undo都可以視為一種恢復操作,redo恢復提交事務修改的頁操作,而undo回滾行記錄到某個特定版本。因此兩者記錄的內容不同,redo通常是物理日志,記錄的是頁的物理修改操作。undo是邏輯日志,根據每行記錄進行記錄。
redo
重做日志用來實現事務的持久性(所以關于上面原子性的實現,有待商榷)。其由兩部分組成:一是內存中的重做日志緩沖,其是易失的;二是重做日志文件,其是持久的。
InnoDB是事務的存儲引擎,其通過Force Log at Commit機制實現事務的持久性,即當事務提交時,必須先將該事務的所有日志寫入到重做日志文件進行持久化。
為了確保每次日志都寫入重做日志文件,在每次將重做日志緩沖寫入重做日志文件后,為了確保日志寫入磁盤(因為這里有一個文件系統緩存),必須進行一次fsync操作。由于fsync的效率取決于磁盤的性能,因此磁盤的性能決定了事務提交的性能,也就是數據庫的性能。
InnoDB存儲引擎允許用戶手工設置非持久性的情況發(fā)生,以此提高數據庫的性能。即當事務提交時,日志不寫入重做日志文件,而是等待一個時間周期后再執(zhí)行fsync操作。這可以顯著提高數據庫的性能,但是當數據庫發(fā)生宕機時,由于部分日志文件未寫入磁盤,因此會丟失最后一段時間的事務。
LSN
LSN是Log Sequence Number的縮寫,其代表的是日志序列號。在InnoDB存儲引擎中,LSN占用8字節(jié),并且單調遞增。LSN代表的含義有:
- 重做日志寫入的總量
- checkpoint的位置
- 頁的版本
LSN表示事務寫入重做日志的字節(jié)總量。例如,當前重做日志的LSN是1000,事務T1寫入了100字節(jié)的重做日志,LSN就變成1100,又有事務T2寫入200字節(jié)的重做日志,那么LSN變成:1300。可見LSN記錄的是重做日志的總量,其單位是字節(jié)。
每個頁的頭部也有一個LSN,記錄的是該頁最后刷新時LSN的大小。重做日志記錄的是每個頁的物理更改日志,因此頁中的LSN用來判斷是否需要進行恢復操作。例如:頁的LSN為10000,數據庫啟動時,寫入重做日志的LSN為13000,表明該事務已經提交,數據庫需要恢復;重做日志中的LSN小于頁中的LSN,不需要進行重做,因為頁中的LSN表示已經刷新到該位置。
恢復
InnoDB存儲引擎在啟動時不管上次數據庫運行時是否正常關閉,都會嘗試進行恢復操作。
undo
重做日志記錄了事務的行為,可以很好的通過其對頁進行“重做”操作。但是事務有時還需要進行回滾操作,這時就需要undo。如果用戶執(zhí)行的事務或語句由于某種原因失敗了,又或者用戶用一條ROLLBACK語句請求回滾,就可以利用這些undo信息將數據回滾到修改之前的樣子。
除了回滾操作,undo的另一個作用是MVCC,即在InnoDB存儲引擎中MVCC的實現是通過undo來完成。當用戶讀取一行記錄時,若該記錄已經被其他事務占用,當前事務可以通過讀取之前的行版本信息,以此實現非鎖定讀。**
最后也是最為重要的一點是,undo log也會產生redo log,也就是undo log的產生會伴隨著redo log的產生,這是因為undo log也需要持久性的保護。
purge
purge用于最終完成delete和update操作。這樣設計是因為InnoDB存儲引擎支持MVCC,所以記錄不能在事務提交時立即進行處理。這時其他事務可能正在引用該行,故InnoDB存儲引擎需要保存記錄之前的版本。而是否可以刪除該條記錄通過purge來進行判斷。若該行記錄已不被任何其他事務引用,那么就可以進行真正的delete操作。
group commit
若事務為非只讀事務,則每次事務提交時需要進行一次fsync操作,以此保證重做日志都已經寫入了磁盤。然而磁盤的fsync性能是有限的,為了提高效率,當前數據庫都提供了group commit功能,即一次fsync可以刷新確保多個事務日志被寫入文件。
(實現用了隊列和流水線)
備份與恢復
備份類型
根據備份的方法不同:
- Hot Backup(熱備):在數據庫運行中直接備份
- Cold Backup(冷備):在數據庫停止的情況下備份
- Warm Backup(溫備):同樣是在數據庫運行中進行,但會對當前數據庫的操作有所影響,如加一個全局讀鎖以保證備份數據的一致性
根據備份后文件的內容:
- 邏輯備份:備份出的文件內容是可讀的,一般是文本文件,內容一般是由一條條SQL語句,或者是表內實際數據組成。這類方法的好處是可以觀察導出的文件的內容,一般適用于數據庫的升級、遷移等工作。但其缺點是恢復所需要的時間往往較長。
- 裸文件備份:復制數據庫的物理文件。這類備份的恢復時間往往較邏輯備份短很多。
按照備份數據庫的內容來分:
- 完全備份:對數據庫進行一個完整的備份
- 增量備份:在上次完全備份的基礎上,對于更改的數據進行備份
- 日志備份:對MySQL數據庫二進制日志的備份,通過對一個完全備份進行二進制日志的重做來完成數據庫的point-in-time的恢復工作。
MySQL數據庫復制(replication)的原理就是異步實時地將二進制日志傳送并應用到從(slave/standby)數據庫。
但是對于真正的增量備份來說,只需要記錄當前每頁最后的檢查點的LSN,如果大于之前全備時的LSN,則備份該頁,否則不用備份,這大大加快了備份的速度和恢復的時間。
快照備份
通過寫時復制技術來創(chuàng)建快照。當創(chuàng)建一個快照時,僅復制原始卷中數據的元數據,并不會有數據的物理操作,因此快照的創(chuàng)建過程是非常快的。當快照創(chuàng)建完成,原始卷上有寫操作時,快照會跟蹤原始卷塊的改變,將要改變的數據在改變之前復制到快照預留的空間里,因此這個原理的實現叫做寫時復制。而對于快照的讀取操作,如果讀取的數據塊是創(chuàng)建快照后沒有修改過的,那么會將讀操作直接重定向到原始卷上;如果要讀取的是已經修改過的塊,則將讀取保存在快照中該塊在修改之前的數據。因此,采用寫時復制機制保證了讀取快照時得到的數據與快照創(chuàng)建時一致。
B區(qū)塊被修改了,因此歷史數據放入了快照區(qū)域。讀取快照數據時,A、C、D塊還是從原有卷中讀取,而B塊就需要從快照讀取了。
復制(replication)
復制解決的基本問題是讓一臺服務器的數據與其他服務器保持同步。
MySQL內建的復制功能是構建基于MySQL的大規(guī)模、高性能應用的基礎,這類應用使用所謂的“水平擴展”的架構。我們可以通過為服務器配置一個或多個備庫的方式來進行數據同步。復制功能不僅有利于構建高性能的應用,同時也是高可用性、可擴展性、災難恢復、備份以及數據倉庫等工作的基礎。事實上,可擴展性和高可用性通常是相關聯的話題。
復制(replication)是MySQL數據庫提供的一種高可用高性能的解決方案,一般用來建立大型的應用。總體來說,replication的工作分為以下3個步驟:
- 主服務器(master)將數據更改記錄到二進制日志中
- 從服務器(slave)把主服務器的二進制日志復制到自己的中繼日志(relay log)中。
- 從服務器重做中繼日志的日志,把更改應用到自己的數據庫上,以達到數據的最終一致性。
復制+快照的備份架構
復制可以用來作為備份,但其功能不僅限于備份,其主要功能如下:
- 數據分布。由于MySQL數據庫提供的復制并不需要很大的帶寬,因此可以在不同的數據中心之間實現數據的拷貝。
- 讀取的負載均衡。通過建立多個從服務器,可將讀取平均地分布到這些從服務器中,從而減少主服務器的壓力。一般可以通過DNS的Round-Robin和Linux的LVS功能實現負載平衡。
- 數據庫備份。復制對備份很有幫助,但是從服務器不是備份,不能完全代替?zhèn)浞荨?/li>
- 高可用性和故障轉移。通過復制建立的從服務器有助于故障轉移,減少故障的停機時間和恢復時間。
可見,復制的設計目的不是簡簡單單用來備份的,并且只用復制來進行備份是遠遠不夠的。
假設當前應用采用了主從式的復制架構,從服務器用來作為備份,一個不太有經驗的DBA執(zhí)行了誤操作,如DROP DATABASE或者DROP TABLE,這時從服務器也跟著運行了,那這時如何從服務器進行恢復呢?一種比較好的方法是通過對從服務器上的數據庫所在的分區(qū)做快照,以此來避免復制對誤操作的處理能力。當主服務器上發(fā)生誤操作時,只需要恢復從服務器上的快照,然后再根據二進制日志執(zhí)行point-in-time的恢復即可。因此,快照+復制的備份架構如下圖所示:
發(fā)送復制事件到其他備庫
log_slave_updates選項可以讓備庫變成其他服務器的主庫。在設置該選項后,MySQL會將其執(zhí)行過的事件記錄到它自己的二進制日志中。
為什么要指定服務器ID,難道MySQL在不知道復制命令來源的情況下不能執(zhí)行嗎?為什么MySQL要在意服務器ID是全局唯一的。問題的答案在于MySQL在復制過程中如何防止無限循環(huán)。當復制SQL線程讀中繼日志時,會丟棄事件中記錄的服務器ID和該服務器ID相同的事件,從而打破了復制過程中的無限循環(huán)。在某些復制拓撲結構下打破無限循環(huán)非常重要,例如主-主復制結構。
復制拓撲
幾個基本原則:
- 一個備庫只能有一個主庫
- 每個備庫必須有一個唯一的服務器ID
- 一個主庫可以有多個備庫
- 如果打開了log_slave_updates選項,一個備庫可以把其主庫上的數據變化傳播到其他備庫
一主庫多備庫
這是最簡單的拓撲結構。在有少量寫和大量讀時,這種配置是非常有用的。可以把讀分攤到多個備庫上,直到備庫給主庫造成了太大的負擔,或者主備之間的帶寬成為瓶頸為止。
盡管這是非常簡單的拓撲結構,但它非常靈活,能滿足多種需求:
- 為不同的角色使用不同的備庫(如添加不同的索引或使用不同的存儲引擎)
- 把一臺備庫當做待用的主庫
- 將一臺備庫放到遠程數據中心,用作災難恢復
- 使用其中一個備庫,作為備份、培訓、開發(fā)或者測試使用服務器
這種結構流行的原因是它避免了很多其他拓撲結構的復雜性。例如在同一個邏輯點停止所有備庫的復制,它們正在讀取的是主庫上同一個日志文件的相同物理位置。這是個很好的物理特性,可以減輕管理員的許多工作,例如把備庫提升為主庫。
主動-主動模式下的主-主復制
主-主復制(也叫雙主復制或雙向復制)包含兩臺服務器,每一個都被配置成對方的主庫和備庫,換句話說,它們是一對主庫。這樣,任何一方所做的變更,都會通過復制應用到另外一方的數據庫中。
主動-主動模式下的主-主復制有一些應用場景,但通常用于特殊的目的。一個可能的應用場景是兩個處于不同地理位置的辦公室,并且都需要一份可寫的數據拷貝。
這種配置最大的問題是如何解決沖突,兩個可寫的互主服務器導致的問題非常多。例如兩臺服務器同時修改一行記錄,或同時在兩臺服務器上向一個包含AUTO_INCREMENT列的表里插入數據。
總的來說,允許向兩臺服務器上寫入所帶來的麻煩遠遠大于其帶來的好處,但下面描述的主動-被動模式則會非常有用。
主動-被動模式下的主-主復制
這和上面的主要區(qū)別在于其中一臺服務器是只讀的被動服務器。
這種方式使得反復切換主動和被動服務器非常方便,因為服務器是配置是對稱的。這使得故障轉移和故障恢復很容易。它也可以讓你在不關閉服務器的情況下執(zhí)行維護,優(yōu)化表,升級操作系統(或者應用程序、硬件等)或其他任務。
例如,執(zhí)行ALTER TABLE操作可能會鎖住整個表,阻塞對表的讀和寫,則可能會花費很長時間并導致服務中斷。然而在主-主配置下,可以先停止主動服務器上的備庫復制線程(這樣就不會在被動服務器上執(zhí)行任何更新),然后在被動服務器上執(zhí)行ALTER操作,交換角色,最后在先前的主動服務器上啟動復制線程。這個服務器將會讀取中繼日志并執(zhí)行相同的ALTER語句。這可能花費很長時間,但不要緊,因為該服務器沒有為任何活躍查詢提供服務。
擁有備庫的主-主結構
這種配置的優(yōu)點是增加了冗余,對于不同地理位置的復制拓撲,能夠消除站點單點失效的問題。你也可以像平常一樣,將讀查詢分配到備庫上。
主庫、分發(fā)主庫以及備庫
當備庫足夠多時,會對主庫造成很大的負擔。因此,如果需要多個備庫,一個好辦法是從主庫移除負載并使用分發(fā)主庫。分發(fā)主庫也是一個備庫,它的唯一目的就是提取和提供主庫的二進制日志。多個備庫連接到分發(fā)主庫,這使原來的主庫擺脫了負擔。為了避免在分發(fā)主庫上做實際的查詢,可以將它的表修改為blackhole存儲引擎。
樹或金字塔形(或稱級聯復制架構)
如果正在將主庫復制到大量的備庫中,不管是把數據分發(fā)到不同的地方,還是提供更高的讀性能,使用金字塔結構都能夠更好地管理。
這樣設計的好處是減輕了主庫的負擔,就像前面提到的分發(fā)主庫一樣。它的缺點是中間層出現任何錯誤都會影響到多個服務器。如果每個備庫和主庫直接相連就不會出現這樣的問題。同樣,中間層次越多,處理故障會更困難、更復雜。
定制的復制方案
選擇性復制
為了利用訪問局部性原理,并將需要讀的工作集駐留在內存中,可以復制少量數據到備庫中。如果每個備庫只擁有主庫的一部分數據,并且將讀分配給備庫,就可以更好的利用備庫的內存。并且每個備庫也只有主庫一部分的寫入負載,這樣主庫的能力更強并能保證備庫延遲。
這個方案有點類似于下面我們會討論到的水平數據劃分,但它的優(yōu)勢在于主庫包含了所有的數據集,這意味著無須為了一條查詢去訪問多個服務器。如果讀操作無法在備庫上找到數據,還可以通過主庫來查詢。即使不能從備庫上讀取所有數據,也可以移除大量的主庫讀負擔。
最簡單的方法是在主庫上將數據劃分到不同的數據庫里,然后將每個數據庫復制到不同的備庫上。
分離功能
許多應用都混合了在線事務處理(OLTP)和在線分析處理(OLAP),OLTP查詢比較短并且是事務型的,OLAP查詢則通常很大,也很慢,并且不要求絕對最新的數據。這兩種查詢給服務器帶來的負擔完全不同,因此它們需要不同的配置,甚至使用不同的存儲引擎或者硬件。
一個常見的辦法是將OLTP的數據復制到專門為OLAP工作準備的備庫上。這些備庫可以有不同的硬件、配置、索引或者不同的存儲引擎。
復制和容量規(guī)劃
寫操作通常是復制的瓶頸,并且很難通過復制來擴展寫操作。當計劃為系統增加復制容量時,需要確保進行了正確的計算,否則很容易犯一些復制相關的錯誤。
例如,假設工作負載為20%的寫以及80%的讀,服務器支持每秒1000次查詢,那么應該增加多少備庫才能處理當前兩倍的負載,并將所有的讀查詢分配給備庫?四倍呢?
看上去應該增加兩個備庫并將1600次讀操作平分給它們,但不要忘記,主庫的寫操作同樣會在備庫上執(zhí)行。400次寫操作,只剩600次讀操作,所以需要三臺備庫。
四倍負載時,將有800次寫入,這時備庫只有200次讀每秒,就需要16臺備庫來處理3200次讀查詢。
這遠遠不是線性擴展,查詢數量增加4倍,卻需要增加17倍的服務器。這說明當為單臺主庫增加備庫時,將很快達到投入遠高于回報的地步。
復制只能擴展讀操作,無法擴展寫操作。對數據進行分片是唯一可以擴展寫操作的方法。
測量備庫延遲
一個比較普遍的問題是如何監(jiān)控備庫落后主庫的延遲有多大(SHOW SLAVE STATUS輸出的Seconds_behind_master由于各種原因/缺陷幾乎不可用)。
最好的解決方案是使用heartbeat record(心跳記錄),這是一個在主庫上會每秒更新一次的時間戳。為了計算延時,可以直接用備庫當前的時間戳減去心跳記錄的值。
MySQL二進制日志轉儲線程并沒有通過輪詢的方式從主庫請求事件,而是由主庫來通知備庫新的事件,因為前者低效且緩慢。從主庫讀取一個二進制日志事件是一個阻塞型網絡調用,當主庫記錄事件后,馬上就開始發(fā)送。因此可以說,只要復制線程被喚醒并且能夠通過網絡傳輸數據,事件就會很快到達備庫。
可擴展的MySQL
什么是可擴展性
人們常常把諸如“可擴展性”、“高可用性”以及性能等術語在一些非正式場合用作同義詞,但事實上它們是完全不同的。
性能:響應時間
可擴展性:當增加資源以處理負載和增加容量時系統能夠獲得的投資產出率
Scale Up:向上擴展/垂直擴展,購買更多強悍的硬件,以增加已有服務器的性能;
Scale Out:向外擴展/水平擴展,將任務分配到多臺計算機上;
向外擴展/水平擴展
最簡單也最常見的向外擴展的方法是通過復制將數據分發(fā)到多個服務器上,然后將備庫用于讀查詢。這種技術對于以讀為主的應用很有效。它也有一些缺點,例如重復緩存。
另外一個比較常見的向外擴展方法是將工作負載分布到多個“節(jié)點”。
在MySQL架構中,一個節(jié)點(node)就是一個功能部件。如果沒有規(guī)劃冗余和高可用性,那么一個節(jié)點可能就是一臺服務器。如果設計的是能夠故障轉移的冗余系統,那么一個節(jié)點通常可能是下面的某一種:
- 一個主-被模式下的主-主復制雙機結構
- 一個組庫和多個備庫
- 一個主動服務器,并使用分布式復制塊設備(DRBD)作為備用服務器
- 一個基于存儲區(qū)域網絡(SAN)的“集群”
1,按功能拆分
按功能拆分,或者說按職責拆分,意味著不同的節(jié)點執(zhí)行不同的任務。將獨立的服務器或節(jié)點分配給不同的應用,這樣每個節(jié)點只包含它的特定應用所需要的數據。
例如,在門戶網站,可以瀏覽網站新聞、論壇,尋求支持和訪問知識庫等,這些不同功能區(qū)域的數據可以放到專用的MySQL服務器中。
歸根結底,還是不能通過功能劃分來無限地進行擴展,因為如果一個功能區(qū)域被捆綁到單個MySQL節(jié)點,就只能進行垂直擴展。其中的一個應用或者功能區(qū)域最終增長到非常龐大時,都會迫使你去尋求一個不同的策略。如果進行了太多的功能劃分,以后就很難采用更具擴展性的設計了。
2,數據分片
在目前用于擴展大型MySQL應用的方案中,數據分片是最通用且最成功的的方法。它把數據分割成一小片,或者說一小塊,然后存儲到不同的節(jié)點中。
數據分片在和某些類型的按功能劃分聯合使用時非常有用。大多數分片系統也有一些“全局的”數據不會被分片(例如城市列表或者登錄數據)。全局數據一般存儲在單個節(jié)點上,并且通常保存在類似memcached這樣的緩存里。
事實上,大多數應用只會對需要的數據做分片——通常是那些將會增長得非常龐大的數據。假設正在構建的博客服務,預計會有1000萬用戶,這時候就無需對注冊用戶進行分片,因為完全可以將所有的用戶(或者其中的活躍用戶)放到內存中。假如用戶數達到5億,那么就可能需要對用戶數據分片。用戶產生的內容、例如發(fā)表的文章和評論,幾乎肯定需要進行數據分片,因為這些數據非常龐大,而且會越來越多。
分片技術和大多數應用的最初設計有著顯著的差異,并且很難將應用從單一數據存儲轉換為分片架構。如果在應用設計初期就已經預計到分片,那實現起來就容易得多。
許多一開始沒有建立分片架構的應用都會碰到規(guī)模擴大的情形。例如,可以使用復制來擴展博客服務的讀查詢,直到它不再奏效。然后可以把服務器劃分為三個部分:用戶信息、文章,以及評論。可以將這些數據放到不同的服務器上(按功能劃分)。
最后,可以通過用戶ID來對文章和評論進行分片,而將用戶信息保留在單個節(jié)點上。
如果事先知道應用會擴大到很大的規(guī)模,并且清楚按功能劃分的局限性,就可以跳過中間步驟,直接從單個節(jié)點升級為分片數據存儲。
為什么選擇數據分片存儲?
因為如果想擴展寫容量,就必須切分數據。如果只有單臺主庫,那么不管有多少備庫,寫容量都是無法擴展的。對于上述缺點而言,數據分片是我們的首選解決方案。
3,選擇分區(qū)鍵
一個好的分區(qū)鍵常常是數據庫中一個非常重要的實體的主鍵。這些鍵值決定了分片單元。例如,如果通過用戶ID或客戶端ID來分割數據,分片單元就是用戶或者客戶端。
選擇分區(qū)鍵的時候,盡量選擇那些能夠避免跨分片查詢的,但同時也要讓分片足夠小,以免過大的數據片導致問題。
4,多個分區(qū)鍵
許多應用擁有多個分區(qū)鍵,換句話說,應用需要從不同的角度看到有效且連貫的數據視圖。這意味著某些數據在系統內至少需要存儲兩份。
例如,需要將博客應用的數據按照用戶ID和文章ID進行分片,因為這兩者都是應用查詢數據時使用比較普遍的方式。試想一下這種情形:頻繁的讀取某個用戶的所有文章,以及某個文章的所有評論。如果按照用戶分片就無法找到某篇文章的所有評論(需要遍歷所有分片查詢),而按文章分片則無法找到某個用戶的所有文章。
需要多個分區(qū)鍵并不意味著需要去設計兩個完全冗余的數據存儲。
例如,假設為用戶數據和書籍數據都設計了分片數據存儲。而評論同時擁有用戶ID和評論ID,這樣就跨越了兩個分片的邊界。實際上卻無需冗余存儲兩份評論數據。替代方案是,將評論和用戶數據一起存儲,然后把每個評論的標題和ID與書籍數據存儲在一起。這樣在渲染大多數關于某本書的評論的視圖時無須同時訪問用戶和書籍數據存儲,如果需要顯示完整的評論內容,可以再從用戶數據存儲中獲得。
5,跨分片查詢
大多數分片應用多少都有一些查詢需要對多個分片的數據進行聚合或關聯操作。例如,一個讀書俱樂部網站要顯示最受歡迎或最活躍的用戶,就必須訪問每一個分片。如何讓這類查詢很好的執(zhí)行,是實現數據分片的架構中最困難的部分。雖然從應用的角度來看,這是一條查詢,但實際上需要拆分成多條并行執(zhí)行的查詢,每個分片上執(zhí)行一條。
普遍的做法是使用C或Java編寫一個輔助應用來執(zhí)行查詢并聚合結果集。也可以借助匯總表來實現。
跨分片查詢并不是分片面臨的唯一難題。維護數據一致性同樣困難。外鍵無法在分片間工作,因此需要由應用來檢查參照一致性,或者只在分片內使用外鍵,因為分片的內部一致性可能是最重要的。還可以使用XA事務,但由于開銷太大,現實中使用很少。
6,分配數據、分片和節(jié)點
應盡可能的讓分片的大小比節(jié)點容量小很多,這樣就可以在單個節(jié)點上存儲多個分片。
保持分片足夠小更容易管理。這將使數據的備份和恢復更加容易,如果表很小,那么像更改表結構這樣的操作會更加容易。例如,有一個100GB的表,你可以直接存儲,也可以將其劃分成100個1GB的分片,并存儲在單個節(jié)點上。現在假如要向表上增加一個索引,在單個100GB的表上的執(zhí)行時間會比100個1GB分片上執(zhí)行的總時間更長,因為1GB的分片更容易全部加載到內存中。并且在執(zhí)行ALTER TABLE時還會導致數據不可用,阻塞1GB的數據比阻塞100GB的數據要好得多。
小一點的分片也便于轉移。這有助于重新分配容量,平衡各個節(jié)點的分片。
8,固定分配
將數據分配到分片中有兩種主要的方法:固定分配和動態(tài)分配。兩種方法都需要一個分區(qū)函數,使用行的分區(qū)鍵作為輸入,返回存儲該行的分片。
固定分配使用的分區(qū)函數僅僅依賴于分區(qū)鍵的值。哈希函數和取模運算就是很好的例子。
9,動態(tài)分配
假設有一個表,包括用戶ID和分片ID:
CREATE TABLE user_to_shard (
user_id INT NOT NULL,
shard_id INT NOT NULL,
PRIMARY KEY (user_id)
);
這個表本身就是分區(qū)函數。
負載均衡
負載均衡的基本思路很簡單:在一個服務器集群中盡可能地平均負載量。通常的做法是在服務器前端設置一個負載均衡器。然后負載均衡器負責將請求路由到最空閑的服務器。
在與MySQL相關的領域里,負載均衡架構通常和數據分片及復制緊密相關。例如,可以在MySQL Cluster集群的多個SQL節(jié)點上做負載均衡,也可以在多個數據中心間做負載均衡,其中每個數據中心又可以使用數據分片架構,每個節(jié)點實際上是擁有多個備庫的主-主復制對結構,這里又可以做負載均衡。
復制上的讀寫分離
MySQL復制產生了多個數據副本,你可以選擇在備庫還是主庫上執(zhí)行查詢。由于備庫復制是異步的,因此主要的難點是如何處理備庫上的臟數據(主庫已修改,而備庫沒有來得及同步更新的數據)。
如果不太關心數據是否是臟的,可以使用備庫,而對需要即時數據的請求則使用主庫。我們將這稱為讀寫分離。
常見的讀寫分離方法如下:
- 基于查詢分離
- 將所有不能容忍臟數據的讀和寫查詢分配到主庫或主動服務器上,其他的讀查詢分配到備庫或被動服務器上。(然而只有很少的查詢能夠容忍臟數據)
- 基于臟數據分離
- 讓應用檢查復制延遲,以確定備庫數據是否太舊
- 基于會話分離
- 判斷用戶是否修改了數據。用戶不需要看到其他用戶的最新數據,但需要看到自己的更新。可以在會話層設置一個標志位,標記是否做了更新,若是則將該用戶的查詢在一段時間內總是指向主庫。這是我們通常推薦的策略,因為它是在簡單和有效之間的一種很好的妥協。如果有足夠的想象力,可以把基于會話的分離方法和復制延遲監(jiān)控結合起來。如果用戶在10秒前更新了數據,而所有備庫的延遲在5秒內,就可以安全地從備庫中讀取數據。
- 基于版本分離
- 這和基于會話的分離方法相似:你可以跟蹤對象的版本號或時間戳,通過比較從備庫讀取的對象版本或時間戳來判斷數據是否足夠新。例如,用戶發(fā)表了一篇文章后,可以更新用戶的版本,這樣就會從主庫去讀取數據了。
大多數讀寫分離解決方案都需要監(jiān)控復制延遲來決策讀查詢的分配。