1. 程式人生 > >MySQL之架構與歷史

MySQL之架構與歷史

MySQL最重要,最與眾不同的特性是它的儲存引擎架構,這種架構的設計將查詢處理(Query Processing)及其他系統任務(Server Task)和資料的儲存/提取相分離。這種處理和儲存相分離的設計可以在使用時根據效能、特性和其他需求來選擇資料儲存的方式。

MySQL邏輯架構

MySQL邏輯架構圖

  • 最上層的服務包括連線處理、授權認證、安全等等。
  • 第二層架構是MySQL的核心服務層。包括查詢解析、分析、優化、快取以及所有的內建函式(例如,日期、時間、數學和加密函式),所有跨儲存引擎的功能都在這一層實現:儲存過程、觸發器、檢視等。
  • 第三層包含了儲存引擎。儲存引擎負責MySQL中資料的儲存和提取。每個儲存引擎都有它的優勢和劣勢。伺服器通過API與儲存引擎進行通訊。這些介面遮蔽了不同儲存引擎直接的差異。儲存引擎API包含幾十個底層函式,用於執行諸如“開始一個事務”或者“根據主鍵提取一行記錄”等操作。但儲存引擎不會去解析SQL (InnoDB是一個例外,它會解析外來鍵定義,因為MySQL伺服器本身沒有實現該功能) ,不同儲存引擎之間也不會相互通訊,而只是簡單地響應上層伺服器的請求。

連線管理與安全性

  • 每個客戶端連線都會在伺服器程序中擁有一個執行緒,這個連線的查詢只會在這個單獨的執行緒中執行,該執行緒只能輪流在某個CPU核心或CPU中執行。伺服器會快取執行緒,因此不需要為每一個建立的連線建立或銷燬執行緒。(MySQL5.5或更高的版本提供了一個API,支援執行緒池(Thread-Pooling)外掛,可以使用池中少量的執行緒來提供大量的連線。)
  • 當客戶端連線到MySQL伺服器時,伺服器需要對其進行認證。認證時基於使用者名稱、原始主機資訊和密碼。如果使用了安全套接字(SSL)方式連線,還可以使用X.509證書認證。一旦客戶端連線成功,伺服器會繼續驗證該客戶端是否具有執行某個特定查詢的許可權。

優化與執行

  • MySQL會解析查詢,並建立內部資料結構(解析樹),然後對其進行各種優化,如重寫查詢、決定表的讀取順序,以及選擇合適的索引等。使用者可以通過特殊的關鍵字提示(hint)優化器,影響它的決策過程。也可以請求優化器解釋(explain)優化過程的各個因素,使使用者知道伺服器時如何進行優化決策的,並提供一個參考基準,便於使用者重新構造查詢和schema,修改相關配置,使應用盡可能高效執行。
  • 優化器並不關心使用的是什麼儲存引擎,但儲存引擎對於優化查詢是有影響的。優化器會要求儲存引擎提供容量或某個操作的開銷資訊、以及表資料的統計資訊等。
  • 對於SELECT語句,在解析查詢之前,伺服器會先檢查查詢快取(Query Cache),如果能在其中找到對應的查詢,伺服器就不會執行查詢解析、優化和執行的整個過程,而是直接返回查詢快取中的結果集。

併發控制

無論何時,只要多個查詢在同一時刻修改資料,都會發生併發控制的問題。

讀寫鎖

  • 包括共享鎖(shared lock)和排它鎖(exclusive lock),也叫讀鎖(read lock)和寫鎖(write lock)。
  • 讀鎖是共享的,或者說是相互不阻塞的。多個客戶可以在同一時刻讀取同一資源,而互補干擾。寫鎖是排他的,也就是說一個寫鎖會阻塞其他的讀鎖或寫鎖,這是出於安全策略的考慮,只有這樣,才能確保在給定的時間裡,只有一個使用者能執行寫入,並防止其他使用者讀取正在寫入的同一資源。
  • 在實際的資料庫系統中,每時每刻都在發生鎖定,當某個使用者在修改某一部分資料時,MySQL會通過鎖定防止其他使用者讀取同一資料。大多數時候,MySQL鎖的內部管理都是透明的。

鎖粒度

  • 減少鎖的粒度也有助於提高哦共享資源併發性。儘量只鎖定需要修改的部分資料,而不是所有的資源。
  • 問題是加鎖也需要消耗資源。鎖的各種操作,包括獲得鎖、檢查鎖是否已經解除、釋放鎖等,都會增加系統的開銷。如果系統花費大量的時間來管理鎖,而不是存取資料,那麼系統的效能可能會因此受到影響。
  • 所謂的鎖策略,就是在鎖的開銷和資料的安全性之間尋求平衡,這種平衡當然也會影響到效能。大多數商業資料庫系統沒有提供更多的選擇,一般都是在表上施加行級鎖(row-level lock),並以各種複雜的方式來實現,以便在鎖比較多的情況下儘可能地提供更好的效能。

表鎖

  • 表鎖是MySQL中最基本的鎖策略,並且是開銷最小的策略。它會鎖定整張表。一個使用者在對錶進行寫操作(插入、刪除、更新等)前,需要獲取寫鎖,這會阻塞其他使用者對該表的所有讀寫操作。只有沒有寫鎖時,其他使用者才能 獲取讀鎖,讀鎖之間是不相互阻塞的。
  • 在特定的場景中,表鎖也可能有良好的效能。例如,READ LOCAL表鎖支援某些型別的併發寫操作。另外,寫鎖比讀鎖具有更高的優先順序,因此一個寫鎖請求可能會被插入到對鎖佇列的前面 。
  • 儘管儲存引擎可以管理自己的鎖,MySQL本身還是會使用各種有效的表鎖來實現不同 的目的。例如,伺服器會為諸如ALTER TABLE 之類的語句使用表鎖,而忽略儲存引擎的鎖機制。

行級鎖

  • 行級鎖可以最大程度地支援併發處理(同時也帶來了最大的鎖開銷)。在InnoDB和XtraDB,以及其他一下儲存引擎中實現了行級鎖。行級鎖只在儲存引擎層實現,而MySQL伺服器層沒有實現。伺服器完全不瞭解儲存引擎中的鎖實現。所有的儲存引擎都以自己的方式實現了鎖機制。

事務

  • 事物是一組原子性的SQL查詢,或者說是一個獨立的工作單元。事物內的語句,要麼全部執行成功,要麼全部執行失敗。
  • 假如有兩張表,假設一個銀行的資料庫有兩張表:支票(checking )表和儲蓄(savings )表。現在要從使用者Jane的支票賬戶轉移200美元到她的儲蓄賬戶,如下 可以使用START TRANSACTION語句開啟一個事物,然後要麼使用COMMIT提交事務將修改的資料持久保留,要麼使用ROLLBACK撤銷所有的修改。如下
START TRANSACTION;
SELECT balance FROM checking WHERE customer_id = 10233276;
UPDATE checking SET balance = balance - 200.00 WHERE customer_id = 10233276;
UPDATE savings SET balance = balance + 200.00 WHERE customer_id = 10233276;
COMMIT;
  • ACID
  • 原子性(atomicity) 一個事物必須被視為一個不可分割的最小工作單元,整個事務中的所有操作要麼全部提交成功,要麼全部失敗回滾。對應一個事物來說,不可能只窒息感其中的一部分操作,這就是事務的原子性。
  • 一致性(consistency) 資料庫總是從一個一致性的狀態轉移到另一個一致性的狀態。在前面的例子中,一致性確保了,即使在執行第三、四條語句之間時系統崩潰,支票賬戶中也不會損失200美元,因為事務最終沒有提交,所以事務中所做的修改也不會儲存到資料庫中。
  • 隔離性(isolation) 通常來說,一個事物所做的修改在最終提交之前,對其他事物是不可見的。
  • 永續性(durability) 一旦事物提交,其所做的修改就會永久的儲存到資料庫中,此時即使系統奔潰,修改的資料也不會丟失。
  • 就像鎖粒度的升級會增加系統開銷一樣,這種事務處理過程中額外的安全性,也會需要資料庫系統做更多的額外工作。一個實現了ACID的資料庫,相比沒有實現ACID的資料庫,通常會需要更強的CPU處理能力、更大的記憶體和更多的磁碟空間。

隔離級別

  • 在SQL標準中定義了四種隔離級別,每一種級別都規定了一個事物中所做的修改,那些在事務內和事務間是可見的,那些是不可見的。較低基本的隔離通常可以執行更高的併發,系統的開銷也更低。
  • READ UNCOMMITTED(未提交讀) 在READ UNCOMMINTTED級別,事務中的修改,即使沒有提交,對其他事務也是看見的。事務可以讀取未提交的資料,這也被稱為髒讀(Dirty Read)。這個級別會導致很多問題,從效能上來說,READ UNCOMMITTED 會比其他的級別好太多,但卻缺乏其他級別的很多好處,除非真的有非常必要的理由,在實際應用中一般很少使用。
  • READ COMMITTED(提交讀) 大多數資料庫系統的預設隔離基本都是READ COMMITTED(但MySQL不是)。READ COMMITTED滿足簽名提到的隔離性的簡單定義:一個事務開始時,只能看見已經提交的事物所做的修改。換句話說,一個事物從開始到結束前,所做的修改對其他事物都是不可見的。這個級別也叫不可重複讀(nonrepeatable read),因為兩次執行同樣的查詢,可能會得到不一樣的結果。
  • REPEATABLE READ(可重複讀) REPEATABLE READ解決了髒讀的問題。該級別保證了在同一個事物中多次讀取同樣的記錄的結果是一致的。但是理論上,可重複讀隔離級別還是無法解決另外一個幻讀(Phantom Read)的問題。所謂幻讀,指的是當某個事務在讀取某個範圍內的記錄時,另外一個事務又在該範圍內插入了新的記錄,當之前的事務再次讀取該範圍的記錄時,會產生幻行(Phantom Row)。InnoDB和XtraDB儲存引擎通過多版本併發控制(MVCC,Multiversion Concurrency Control)解決了幻讀的問題。 可重複讀是MySQL的預設事務隔離級別。
  • SERIALIZABLE(可序列化) SERIALIZABLE 是最高的隔離級別。它通過強制事務序列執行,避免了前面說的幻讀的問題。簡單來說,SERIALIZABLE 會在讀取的每一行資料上都加鎖,所以可能導致大量的超時和鎖爭用的問題。實際應用中也很少用到這個隔離級別,只有在非常需要確保資料的一致性而且可以接受沒有併發的情況下,才考慮採用該級別。 隔離級別

死鎖

死鎖指兩個或更多的事物在同一資源上相互佔用,並請求鎖定對方持有的資源,從而導致惡性迴圈的現象。當多個事務試圖以不同的順序鎖定資源時,就可能會產生死鎖。多個事務同時鎖定同一個資源時,也會產生死鎖。 例如,設想下面兩個事務同時處理StockPrice 表:

事務1
START TRANSACTION;
UPDATE StockPrice SET close = 45.50 WHERE stock_id = 4 and date = '2002-05-01';
UPDATE StockPrice SET close = 19.80 WHERE stock_id = 3 and date = '2002-05-02';
COMMIT; 

事務2
START TRANSACTION;
UPDATE StockPrice SET high = 20.12 WHERE stock_id = 3 and date = '2002-05-02';
UPDATE StockPrice SET high = 47.20 WHERE stock_id = 4 and date = '2002-05-01';
COMMIT;
  • 如果湊巧,兩個事務都執行了第一條UPDATE 語句,更新了一行資料,同時也鎖定了該行資料,接著每個事務都嘗試去執行第二條UPDATE 語句,卻發現該行已經被對方鎖定,然後兩個事務都等待對方釋放鎖,同時又持有對方需要的鎖,則陷入死迴圈。除非有外部因素介入才可能解除死鎖。

  • 為了解決這種問題,資料庫系統實現了各種死鎖檢測和死鎖超時機制。越複雜的系統,比如InnoDB儲存引擎,越能檢測到死鎖的迴圈依賴,並立即返回一個錯誤。這種解決方式很有效,否則死鎖會導致出現非常慢的查詢。還有一種解決方式,就是當查詢的時間達到鎖等待超時的設定後放棄鎖請求,這種方式通常來說不太好。InnoDB目前處理死鎖的方法是,將持有最少行級排他鎖的事務進行回滾(這是相對比較簡單的死鎖回滾演算法)。

  • 鎖的行為和順序是和儲存引擎相關的。以同樣的順序執行語句,有些儲存引擎會產生死鎖,有些則不會。死鎖的產生有雙重原因:有些是因為真正的資料衝突,這種情況通常很難避免,但有些則完全是由於儲存引擎的實現方式導致的。

  • 死鎖發生以後,只有部分或者完全回滾其中一個事務,才能打破死鎖。對於事務型的系統,這是無法避免的,所以應用程式在設計時必須考慮如何處理死鎖。大多數情況下只需要重新執行因死鎖回滾的事務即可。

事務日誌

  • 事務日誌可以幫助提高事務的效率。
  • 使用事務日誌,儲存引擎在修改表的資料時只需要修改其記憶體拷貝,再把修改行為記錄到持久在磁碟上的事務日誌中,而不用每次將修改的資料本身持久到磁碟上。
  • 事務日誌採用的是追加的方式,因此寫日誌的操作時磁碟上一小塊區域的順序IO,而不想隨機IO需要在磁碟的多個地方移動磁頭,所有采用事務日誌的方式相對來說要快的多。
  • 事務日誌持久之後,記憶體中被修改的資料在後臺可以慢慢的刷回到磁碟。目前大多數儲存引擎都是這樣實現的,我們通常稱之為預寫式日誌(Write-Ahead Logging),修改資料需要寫兩次磁碟。
  • 如果資料的修改已經記錄到事務日誌並持久化,但資料本身還沒有寫回到磁碟,此時系統崩潰,儲存引擎子在重啟時能夠自動恢復這部分修改的資料。具體的恢復方式視儲存引擎而定。

MySQL中的事務

MySQL提供了兩種事務型的儲存引擎:InnoDB和NDB Cluster。另外還有一些第三方儲存引擎也支援事務,比較知名的包括XtraDB和PBXT。

自動提交(AUTOCOMMIT)

MySQL預設採用自動提交模式。也就是說,如果不是顯式的開啟一個事物,則每個查詢都被當作一個事務執行提交操作。在當前連線中,可以通過設定AUTOCOMMIT變數來啟用或禁用自動提交模式。

mysql> show variables like 'AUTOCOMMIT';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set, 1 warning (0.02 sec)

mysql> set autocommit = 1;
Query OK, 0 rows affected (0.00 sec)
  • 1ON表示啟用,0OFF表示禁用。當AUTOCOMMIT=0時,所有的查詢都在一個事務中,知道顯式地執行COMMIT或ROLLBACK,該事務結束,同時又開啟了另一個事務。修改AUTOCOMMIT對非事務型的表,比如MyISAM或記憶體表,不會有任何影響。對這類表來說,沒有COMMIT或ROLLBACK的概念,也可以說是相當於一直處於AUTOCOMMIT 啟用的模式。
  • 另外還有一些命令,在執行之前會強制執行COMMIT提交當前的活動事務。典型的例子,在資料定義語言(DDL)中,如果是導致大量資料改變的操作,如ALTER TABLE,就是如此。另外還有LOCK TABLES 等其他語句也會導致同樣的結果。如果有需要,請檢查對應版本的官方文件來確認所有可能導致自動提交的語句列表。
  • MySQL可以通過執行SET TRANSACTION ISOLATION LEVEL命令來設定隔離級別。新的隔離級別會在下一個事務開始的時候生效。可以在配置檔案找那個設定整個資料庫的隔離基本,也可以只改變當前會話的隔離級別:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

MySQL能識別所有的4個ANSI隔離級別,InnoDB引擎也支援所有的隔離級別。

在事務中混合使用儲存引擎

  • MySQL服務層不管理事務,事務是由下層的儲存引擎實現的。所以在同一個事物中,使用多種儲存引擎是不可靠的。
  • 如果在事務中混合使用了事務型和非事務型的表(例如InnoDB和MyISAM表),在正常提交的情況下不會有什麼問題。但如果該事務需要回滾,非事務型的表上的變更就無法撤銷,這會導致資料庫處於不一致的狀態,這種情況很難修復,事務的最終結果將無法確定。所以,為每張表選擇合適的儲存引擎非常重要。
  • 在非事務型的表上執行事務相關操作的時候,MySQL通常不會發出提醒,也不會報錯。有時候只有回滾的時候才會發出一個警告:“某些非事務型的表上的變更不能被回滾”。但大多數情況下,對非事務型表的操作都不會有提示。

隱式和顯式鎖定

  • InnoDB採用的是兩階段鎖定協議(two-phase locking protocol)。在事務執行的過程中,隨時都可以執行鎖定,鎖只有在執行COMMIT或ROLLBACK的時候才會釋放,並且所有的鎖是在同一時刻被釋放。這些鎖都是隱式鎖定,InnoDB會根據隔離級別在需要的時候自動加鎖。
  • 另外,InnoDB也支援通過特定的語句進行顯式的鎖定,這些語句不屬於SQL規範:
SELECT ... LOCK IN SHARE MODE
SELECT ... FOR UPDATE
  • 這些鎖定經常被濫用,實際上應該儘量避免使用。
  • MySQL也只支援Lock Tables和Unlock Tables語句,這是在服務層實現的,和儲存引擎無關。它們有自己的用途,並不能代替事務處理。如果應用需要用到事務,還是應該選擇事務型儲存引擎。
  • 經常可以發現,應用語句將表從MyISAM轉換到InnoDB,但哈市顯示地使用LOCK TABLES語句,這非常沒有必要,會嚴重影響效能,實際上InnoDB的行級鎖工作得更好。
  • LOCK TABLES和事務直接相互影響的話,情況會變得非常複雜。除了在事務中禁用AUTOCOMMIT,可以使用LOCK TABLES外,其他任何時候不要顯式地執行LOCK TABLES,不管使用的是什麼儲存引擎。

多版本併發控制

  • MySQL的大多數事務型儲存引擎實現的都不是簡單的行級鎖。它們一般都同時實現了多版本併發控制(MVCC)。MVCC沒有統一的實現標準,各個資料庫系統的實現方式都不一樣。
  • MVCC:可以認為是行級鎖的一個變種,但是它在很多情況下避免了加鎖操作,因此開銷更低。雖然實現機制不同,但大都實現了非阻塞的讀操作,寫操作也只鎖定了必要的行。
  • MVCC的實現,是通過儲存資料在某個時間點的快照來實現的。也就是說,不管需要執行多長時間,每個事務看到的資料都是一致的。根據事務開始的時間不同,每個事務對同一張表,同一時刻看到的資料可能是不一樣的
  • 前面說到不同儲存引擎的MVCC實現是不同的,典型的有樂觀(optimistic)併發控制和悲觀(pessimistic)併發控制
  • InnoDB的MVVC,是通過在每行記錄後面儲存兩個隱藏列來實現的。一個儲存了行的建立時間,一個儲存了行的過期時間(或刪除時間)。當然儲存的並不是實際的時間值,而是系統版本號(system version number)。每開啟一個新的事務,系統版本號都會自動遞增。事務開始時刻的系統版本號會作為當前事務的版本號,用來和查詢到的每行記錄的版本號進行比較。下面看一下在REPEATABLE READ 隔離級別下,MVCC具體是如何操作的:
  • SELECT InnoDB會根據一下兩個條件檢查每行記錄:
  1. InnoDB只查詢版本早於當前事務版本的資料行(也就是,行的系統版本號小於等於當前事務的系統版本號),這樣可以確保事務讀取的行,要麼實在事務開始前就已經存在的,要麼是事務自身插入或修改過的。
  2. 行的刪除版本要麼未定義,要麼大於當前事務版本號。這可以確保事務讀取的行,在事務開始之前未被刪除。 只有符合上述兩個條件的記錄,才能作為返回結果。
  • INSERT InnoDB為新插入的每一行儲存當前的系統版本號作為行版本號。
  • DELETE InnoDB為刪除的每一行儲存當前系統版本號作為行刪除標識。只有commit的時候才會真正刪除。
  • UPDATE InnoDB為插入一行新記錄,儲存當前系統版本號作為行版本號,同時儲存當前系統版本號到原來的行作為行刪除標識。
  • 儲存這兩個額外系統版本號,使大多數讀操作都可以不用加鎖。這樣設計使得讀資料操作很簡單,效能很好,並且也能保證只會讀取到符合標準的行。不足之處是每行記錄都需要額外的儲存空間,需要做更多的行檢查工作,以及一些額外的維護工作。
  • MVCC只在REPEATABLE READREAD COMMITTED 兩個隔離級別下工作。其他兩個隔離級別都和MVCC不相容 ,因為READ UNCOMMITTED 總是讀取最新的資料行,而不是符合當前事務版本的資料行。而SERIALIZABLE 則會對所有讀取的行都加鎖。

參考<高效能MySQL>