MySQL總結之MySQL架構

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

MySQL 的邏輯架構


MySQL 邏輯架構圖

最上層的服務並不是MySQL 所獨有的,大多數基於網絡的客戶端/服務器的工具或者服務都有類似的架構。比如連接處理、授權驗證、安全等。

第二層架構是MySQL 比較有意思的部分。大多數MySQL的核心服務功能都在這一層,包括查詢解析、分析、優化、緩存以及所有的內置函數(例如,日期、時間、數學和加密函數),所有跨存儲引擎的功能都在這一層實現:存儲過程、觸發器、視圖等。

第三層包含了存儲引擎。存儲引擎負責MySQL中數據的存儲和提取。和GNU/Linux下的各種文件系統一樣,每個存儲引擎都有它的優勢和劣勢。服務器通過API於存儲引擎進行通信。這些接口屏蔽了不同存儲引擎之間的差異,使得這些差異對上層的查詢過程透明。存儲引擎API包含了幾十個底層函數,用於執行諸如“開始一個事務”或者“根據主鍵提取一行記錄”等操作。但存儲引擎不會去解析SQL,不同存儲引擎之間也不會相互通信,而只是簡單地響應上層服務器的請求。

連接管理與安全性

每個客戶端連接都會在服務器進行中擁有一個線程,這個連接的查詢只會在這個單獨的線程中執行,該線程只能輪流在某個CPU核心或者CPU中運行。服務器會負責緩存線程,因此不需要爲每一個新建的連接創建或者銷燬線程。

當客戶端(應用)連接到MySQL 服務器時,服務器需要對其進行認證。認證基於用戶名、原始主機信息和密碼。如果使用了安全套接字(SSL)的方式連接,還可以使用X.509證書認證。一旦客戶端連接成功,服務器會繼續認證該客戶端是否具有執行某個特定查詢對方的權限(例如,是否允許客戶端對world數據庫的Country表執行SELECT語句)。

優化與執行

MySQL 會解析查詢,並創建內部數據結構(解析樹),然後對其進行各種優化,包括重寫查詢、決定表的讀寫順序,以及選擇合適的索引等。用戶可以通過特殊的關鍵字提示(hint)優化器,影響它的決策過程。也可以請求優化解釋器(explain)優化過程的各個因素,使用戶可以知道服務器是如何進行優化決策的,並提供一個參考基準,便於用戶重構查詢和schema、修改相關配置,使應用盡可能高效運行。

優化器並不關心表使用的是什麼存儲引擎,但存儲引擎對優化查詢是有影響的。優化器會請求存儲引擎提供容量或某個操作的開銷信息,以及表數據的統計信息等。例如,某些存儲引擎的某種索引,可能對一些特定的查詢有優化。

對於SELECT語句,在解析查詢之前,服務器會線檢查查詢緩存(Query Cache),如果能夠在其中找到對應的查詢,服務器就不必再執行查詢解析、優化和執行的整個過程,而是直接返回查詢緩存中的結果集。

併發控制

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

以Unix 系統的email box爲例,典型的mbox文件格式是非常簡單的。一個mbox郵箱中所有的郵件都串行在一起,彼此首位相連。這種格式對於讀取和分析郵件信息非常友好,同時投遞郵件也很容易,只要在文件末尾加新的郵件內容即可。

但如果兩個進程在同一時刻對同一個郵箱投遞郵件,會發生什麼情況?顯然,郵箱的數據會被破壞,兩封郵件的內容會交叉地附加在郵箱文件的莫問。設計良好的郵箱投遞系統會通過鎖(lock)來防止數據損壞。如果客戶駛入投遞郵件,而郵箱已經被其他客戶鎖住,那就必須等待,直到鎖釋放才能進行投遞。

這種鎖的方案在實際應用環境中雖然工作良好,但並不支持併發處理。因爲在任意一個時刻,只有一個進程可以修改郵箱數據,這在大容量的郵箱系統中是個問題。

讀寫鎖

從郵箱中讀取數據沒有這樣的麻煩,即使同一時刻多個用戶併發讀取也不會有什麼問題。因爲讀取不會修改數據,所以不會出錯。但如果某個客戶正在讀取郵箱,同時另一個用戶試圖刪除編號爲25的郵件,會產生什麼結果?結論是不確定,讀的客戶可能會報錯退出,也可能讀到不一致的郵箱數據。所以,爲安全起見,即使讀取郵箱也需要特別注意。

如果把上述的郵箱當成數據庫中的一張表,把郵件當成表中的一行記錄,就很容易看出,同樣的問題依然存在。從很多方面來說,郵箱就是一張簡單的數據庫表。修改數據庫表中的記錄,和刪除或者修改郵箱中的郵件信息,十分類似。

解決這類經典問題的方法就是併發控制,其實非常簡單。在處理併發讀或者寫時,可以通過實現一個由兩種類型的鎖組成的鎖系統來解決問題。這兩種類型的鎖通常被稱爲共享鎖(shared lock)和排他鎖(exclusive lock),也叫讀鎖(read lock)和寫鎖(write lock)。

鎖的概念如下:讀鎖是共享的,或者說是相互不阻塞的。多個客戶在同一時刻可以同時讀取同一個資源,而互不干擾。寫鎖則是排他的,也就是說一個寫鎖會阻塞其他的寫鎖和讀鎖,這是處於安全策略的考慮,只有這樣,才能確保在給定的時間裏,只有一個用戶能執行寫入,並防止其他用戶讀取正在寫入的同一資源。

在實際的數據庫系統中,每時每刻都在發生鎖定,當某個用戶在修改某一部分數據時,MySQL 會通過鎖定防止其他用戶讀取同一數據。大多數時候,MySQL 鎖的內部管理都是透明的。

鎖粒度

一種提高共享資源併發性的方式就是讓鎖定對象更有選擇性。儘量只鎖定需要修改的部分數據,而不是所有的資源。更理想的方式是,只對會修改的數據片進行精確的鎖定。任何時候,在給定的資源上,鎖定的數據量越少,則系統的併發程度越高,只要相互之間不發生衝突即可。

問題是加鎖也需要消耗資源。鎖的各種操作,包括獲得鎖、檢查鎖是否已經解除、釋放鎖等,都會增加系統的開銷。如果系統話費大量的時間來管理鎖,而不是存取數據,那麼系統的性能可能會因此受到影響。

所謂的鎖策略,就是在鎖的開銷和數據安全性之間尋求平衡,這種平衡當然也會影響到性能。

MySQL 數據庫提供了多種選擇。每種MySQL存儲引擎都可以實現自己的鎖策略和鎖粒度。在存儲引擎設計中,鎖管理是個非常重要的決定。將鎖粒度固定在某個級別,可以爲某些特定的應用場景提供更好的性能,但同時會失去對另外一些應用場景的良好支持。

表鎖(table lock)

表鎖是 MySQL中最基本的鎖策略,並且是開銷最小的策略。表鎖非常類似於前文中描述的郵箱加鎖機制:它會鎖定整張表。一個用戶對錶進行寫操作(插入、刪除、更新等)前,需要先獲得寫鎖,這回阻塞其他用戶對該表的所有讀寫操作。只有沒有寫鎖時,其他讀取的用戶才能獲得讀鎖,讀鎖之間是不互相阻塞的。

在特定的場景中,表鎖也可能有良好的性能。例如,READ LOCK 表鎖支持某些類型的併發寫操作。另外,寫鎖也比讀鎖有更高的優先級,因此一個寫鎖的請求可能會被插入到讀鎖隊列的前面(寫鎖可以插入到鎖隊列的前面,反之讀鎖則不能插入到寫鎖的前面)。

進過存儲引擎可以管理自己的鎖,MySQL 本身還是會使用各種有效的表鎖來實現不同的目的。例如,服務器會爲諸如ALTER TABLE之類的語句使用表鎖,而忽略存儲引擎的鎖機制。

行級鎖(row lock)

行級鎖可以最大成都的支持併發處理(同時也帶來了最大的開銷)。衆所周知,在InnoDB和XtraDB,以及其他一些存儲引擎中實現了行級鎖。行級鎖只在存儲引擎層實現,而MySQL服務器沒有實現。服務器層完全不瞭解存儲引擎中的鎖實現。

事務

事務就是一組原子性的SQL查詢,或者說是一個獨立的工作單元。如果數據庫存儲引擎能夠成功地對接數據庫應用該組查詢的全部語句,那麼就執行該組查詢。如果其中有任何一條語句因爲崩潰或者其他原因無法執行,那麼所有的語句都不會執行。也就是說,事務內的語句,要麼全部執行成功,要麼全部執行失敗。

原子性(atomicity)

一是個事務必須被視爲一個不可分割的最小工作單元,整個事務中的所有操作要麼全部提交成功,要麼全部失敗回滾,對於一個事務來說,不可能只執行其中的一部分操作,這就是事務的原子性。

一致性(consistency)

數據庫總是從一個一致性狀態轉換到另一個一致性的狀態。

隔離性(isolation)

通常來說,一個事務所做的修改在最終提交以前,對其他事務是不可見的。

持久性(durability)

一旦事務提交,則其所做的修改就會永久保存到數據庫中。

隔離級別

READ UNCOMMITTED(未提交讀)

在READ UNCOMMITED級別,事務中的修改,即使沒有提交,對其他事務也都是可見的。事務可以讀取未提交的數據,這也被稱爲脹讀(Dirty Read)。

READ COMMITED(提交讀)

大多數數據庫系統的默認隔離級別都是READ COMMITED(但MySQL不是)。READ COMMITED滿足前面提到的隔離性的簡單定義:一個事務開始時,只能“看見”已經提交的事務所做的修改。換句話說,一個事務從開始直到提交之前,所做的任何修改對其他事務都是不可見的。這個級別有時候也叫做不可重複讀,因爲兩次執行同樣的查詢,可能會得到不一樣的結果。

REPEATABLE READ(可重複讀)

REPEATABLE READ 解決了髒讀的問題。該級別保證了在同一個事務中多次讀取同樣記錄的結果是一致的。但是理論上,可重複讀隔離級別還是無法解決另一個幻讀的問題。所謂幻讀,指的是當某個事務在讀取某個範圍內的記錄時,另一個事務又在該範圍內插入了新的記錄,當之前的事務再次讀取該範圍內的記錄時,會產生幻行。InnoDB和XtraDB 存儲引擎通過過版本併發控制(MVCC)解決了幻讀的問題。

SERIALIZABLE(可串行化)

SERIALIZABLE 是最高的隔離級別。它通過強制事務串行化執行,避免了幻讀問題。簡單來說,SERIALIZABLE 會在讀取的每一行數據上都加鎖,所以可能導致大量的超時和鎖爭用問題。實際應用中也很少用到這個隔離級別,只有在非常需要確保數據的一致性而且可以接受沒有併發的情況下,才考慮採用該級別。



ANSI SQL 隔離級別

死鎖

死鎖是指兩個或者多個事務在同一資源上互相佔用,並請求鎖定對方佔用的資源,從而導致而行循環的現象。當多個事務試圖以不同順序鎖定資源時,就可能會產生死鎖。多個事務同時鎖定同一個資源時,也會產生死鎖。

多版本併發控制

MySQL 的大多數事務型存儲引擎實現的都不是簡單的行級鎖。基於提升併發性能的考慮,它們一般都同時實現了多版本併發控制(MVCC)。不僅是MySQL,包括Oracle、PostgreSQL 等其他數據庫系統也都實現了MVCC,但各自的實現機制不盡相同,因爲MVCC沒有一個統一的實現標準。

可以認爲MVCC是行級鎖的一個變種,但是它在很多情況下避免了加鎖操作,因此開銷更低。雖然實現機制有所不同,但大都實現了非阻塞的讀操作,寫操作也只鎖定必要的行。

MVCC的實現,是通過保存數據在某一個時間點的快照來實現的。也就是說,不管需要執行多長時間,每個事務看到的數據都是一致的。根據事務開始的時間不同,每個事務對同一張表,同一時刻看到的數據可能是不一樣的。

不同存儲引擎的MVCC的實現是不同的,典型的有樂觀併發控制和悲觀併發控制。

InnoDB的MVCC,是通過在每行記錄後面保存兩個隱藏的列來實現的。這兩個列,一個保存了行的創建時間,一個保存了行的過期時間(或刪除時間)。當然存儲的並不是實際的時間值,而是系統版本號。每開始一個新的事務,系統版本號都會自動遞增。事務開始時刻的系統版本號會作爲事務的版本號,用來和查詢到的每行記錄的版本號進行比較。下面是在REPPEATABLE READ隔離級別下,MVCC具體是如何操作的。

SELECT

InnoDB 會根據以下兩個條件去檢查每行記錄:

  1. InnoDB只查找版本早於當前事務版本的數據行(也就是,行的系統版本號小於或等於事務的系統版本號),這樣可以確保事務讀取的行,要麼是在事務開始前已經存在的,要麼是事務自身插入或者修改過的。
  2. 行的刪除版本要麼未定義,要麼大於當前事務版本號。這可以確保事務讀取到的行,在事務開始之前未被刪除。

INSERT

InnoDB 爲新插入的每一行保存當前系統版本號作爲行版本號。

DELETE

InnoDB 爲刪除的每一行保存當前系統版本號作爲刪除標識。

UPDATE

InnoDB 爲插入一行新紀錄,保存當前系統版本號作爲行版本號,同時保存當前系統版本號到原來的行作爲刪除標識。

保存這兩個額外系統版本號,使大多數讀操作都可以不通加鎖。這樣設計使得讀操作很簡單,性能很好,並且也能保證只會讀取到符合標準的行。不足之處是每行記錄都需要額外的存儲空間,需要做很多的行檢查工作,以及一些額外的維護工作。

MVCC只在REPEATABLE READ和READ COMMITED兩個隔離級別下工作。其他兩個隔離級別都和MVCC不兼容,因爲READ UNCOMMITED總是讀取最新的數據行,而不是符合當前事務版本的數據行。而SERIALIZABLE則會對所有讀取的行都加鎖。

總結

MySQL 擁有分層的架構。上層是服務器層的服務和查詢執行引擎,下層則是存儲引擎。雖然有很多不同作用的插件API,但存儲引擎API還是最重要的。如果能理解MySQL 在存儲引擎和服務層之間處理查詢時如何通過API來回交互,就能抓住MySQL 的核心基礎架構的精髓。

書讀百遍,其義自現。

有時候不得不承認自己的無知。從大二就開始接觸MySQL,但是一直都在嘗試着去用。隨着時間的流逝,如今用MySQL 對我來說問題已經不大了,到了學習MySQL是如何實現的階段了。

這裏面的InnoDB的MVCC給我十分深刻的印象,因爲我最近做的一個供應商信息版本控制,實現的設計和這裏大致相同,這種思想上的共通,給人以啓迪。



本文作者: 荒古
本文鏈接: https://haxianhe.com/2019/08/06/MySQL學習筆記之MySQL架構/
版權聲明: 本博客所有文章除特別聲明外,均採用 CC BY-NC-SA 3.0 許可協議。轉載請註明出處!


歡迎關注我的公衆號:荒古傳說



荒古傳說

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