高性能MySQL讀書筆記-事務

1、MySQL邏輯架構

爲了充分發揮MySQL的性能並順利地使用,就必須理解其設計。mysql

1. 邏輯架構

最上層的服務並非MySQL所獨有的,大多數基於網絡的客戶端/服務器的工具或者服務都有相似的架構。好比鏈接處理、受權認證、安全等等。算法

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

第三層包含了存儲引擎。存儲引擎負責MySQL中數據的存儲和提取。和GNU/Linux下的各類文件系統同樣,每一個存儲引擎都有它的優點和劣勢。服務器經過API與存儲引擎進行通訊。這些接口屏蔽了不一樣存儲引擎之間的差別,使得這些差別對上層的查詢過程透明。存儲引擎不會去解析SQL(InnoDB是一個例外,它會去解析外鍵定義,由於MySQL服務器自己沒有實現該功能),不一樣存儲引擎之間也不會相互通訊,而只是簡單的響應上層服務器的請求。數據庫

來源於百度圖片: MySQL邏輯架構圖

2. 鏈接管理和安全性

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

認證基於用戶名、原始主機信息和密碼。若是使用了安全套接字(SSL)的方式鏈接,還可使用X.509證書認證。一旦客戶端鏈接成功,服務器會繼續驗證該客戶端是否具備執行某個特定查詢的權限。安全

3. 優化與執行

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

優化器並不關心表使用的是什麼存儲引擎,但存儲引擎對於優化查詢是有影響的。優化器會請求存儲引擎提供容量或某個具體操做的開銷信息,以及表數據的統計信息等。服務器

對於SELECT語句,在解析查詢以前,服務器會先檢查查詢緩存(Query Cache),若是可以在其中找到對應的查詢,服務器就沒必要再執行查詢解析、優化和執行的整個過程,而是直接返回查詢緩存中的結果集。網絡

高性能MySQL: 第3版/(美)施瓦茨等著, 寧海元等譯, 北京: 電子工業出版社, 2013.5, 第1-3頁。數據結構

2、併發控制

不管什麼時候,只要有多個查詢須要在同一時刻修改數據,都會產生併發控制問題。

1. 讀寫鎖

同一時刻多個用戶併發讀取也不會有什麼問題,由於讀取不會修改數據,因此不會出錯。但讀取的過程當中若是有修改,也可能會讀取到不一致的數據,爲了安全起見,即便是讀取也須要特別注意。

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

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

2. 鎖粒度

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

問題是加鎖也須要消耗資源。鎖的各類操做,包括得到鎖、檢查鎖是否已經解除、釋放鎖等,都會增長系統的開銷。若是系統花費大量的時間來管理鎖,而不是存取數據,那麼系統的性能可能會所以受到影響。

所謂的鎖策略,就是在鎖的開銷和數據的安全性之間尋求平衡,這種平衡固然也會影響到性能。大多數商業數據庫系統沒有提供更多的選擇,通常都是在表上施加行級鎖(row-level lock),並以各類複雜的方式來實現,以便在鎖比較多的狀況下儘量地提供更好的性能。

而MySQL則提供了多種選擇。每種MySQL存儲引擎均可以實現本身的鎖策略和鎖粒度。在存儲引擎的設計中,鎖管理是個很是重要的決定。將鎖粒度固定在某個級別,能夠爲某些特定的應用場景提供更好的性能,但同時卻會失去對另一些應用場景的良好支持。好在MySQL支持多個存儲引擎的架構,因此不須要單一的通用解決方案。

2.1 表鎖(table lock)

表鎖是MySQL中最基本的鎖策略,而且是開銷最小的策略。一個用戶在對錶進行寫操做(插入、刪除、更新等)前,須要先得到寫鎖,這會阻塞其餘用戶對該表的全部讀操做。只有沒有寫鎖時,其餘讀取的用戶才能得到讀鎖,讀鎖之間是不相互阻塞的。

在特定的場景中,表鎖也可能有良好的性能。寫鎖也比讀鎖有更高的優先級,所以一個寫鎖請求可能會被插入到讀鎖隊列的前面。

服務器會爲諸如 ALTER TABLE 之類的語句使用表鎖,而忽略存儲引擎的鎖機制。

2.2 行級鎖(row lock)

行級鎖能夠最大程度地支持併發處理(同時也帶來了最大的鎖開銷)。行級鎖只在存儲引擎層實現,而MySQL服務器層沒有實現。服務器層徹底不瞭解存儲引擎中的鎖實現。

高性能MySQL: 第3版/(美)施瓦茨等著, 寧海元等譯, 北京: 電子工業出版社, 2013.5, 第3-5頁。

3、事務

事務就是一組原子性的SQL查詢,或者說一個獨立的工做單元。若是數據庫引擎可以成功地對數據庫應用該組查詢的所有語句,那麼就執行該組查詢。若是其中有任何一條語句由於崩潰或者其它緣由沒法執行,那麼全部的語句都不會執行。也就是說,事務內的語句,要麼所有執行成功,要麼所有執行失敗。

單純的事務概念並非故事的所有。除非系統經過嚴格的ACID測試,不然空談事務的概念是不夠的。ACID表示原子性(atomicity)、一致性(consistency)、隔離性(isolation)和持久性(durability)。一個運行良好的事務處理系統,必須具有這些標準特徵。

原子性(atomicity)

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

一致性(consistency)

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

隔離性(isolation)

一般來講,一個事務所作的修改在最終提交之前,對其它事務是不可見的。

持久性(durability)

一旦事務提交,則其所作的修改就會永久保存到數據庫中。此時即便系統崩潰,修改的數據也不會丟失。持久性是個有點模糊的概念,由於實際上持久性也分不少不一樣的級別。有些持久性策略可以提供很是強的安全保障,而有些則未必。並且不可能有能作到100%的持久性保證的策略。

一個兼容ACID的數據庫系統,須要作不少複雜但可能用戶並無察覺到的工做,才能確保ACID的實現。

就像鎖粒度的升級會增長系統的開銷同樣,這種事務處理過程當中額外的安全性,也會須要數據庫系統作更多的額外工做。一個實現了ACID的數據庫,相比沒有實現ACID的數據庫,一般須要更強的CPU處理能力,更大的內存和更多的磁盤空間。用戶能夠根據業務是否須要事務處理,來選擇合適的存儲引擎。對於一些不須要事務的查詢類應用,選擇一個非事務型的存儲引擎,能夠得到更高的性能。即便存儲引擎不支持事務,也能夠經過 LOCK TABLES 語句爲應用提供必定程度的保護,這些選擇用戶均可以自主決定。

1. 隔離級別

隔離性其實比想象的要複雜。在SQL標準中定義了四種隔離級別,每一種級別都規定了一個事務中所作的修改,哪些在事務內和事務間是可見的,哪些是不可見的。較低級別的隔離一般能夠執行更高的併發,系統的開銷也更低。

每種存儲引擎實現的隔離級別不盡相同。

READ UNCOMMITTED(未提交讀)

在 READ UNCOMMITTED 級別,事務中的修改,即便沒有提交,對其它事務也都是可見的。事務能夠讀取未提交的數據,這也被稱爲髒讀(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 會在讀取的每一行數據上都加鎖,因此可能致使大量的超時和鎖爭用的問題。實際應用中也不多用到這個隔離級別,只有在很是須要確保數據的一致性並且能夠接受沒有併發的狀況下,才考慮採用該級別。

表 ANSI SQL 隔離級別

隔離級別 髒讀可能性 不可重複讀可能性 幻讀可能性 加鎖讀
READ UNCOMMITTED Yes Yes Yes No
READ COMMITTED No Yes Yes No
REPEATABLE READ No No Yes No
SERIALIZABLE No No No Yes

2. 死鎖

死鎖是指兩個或者多個事務在同一資源上相互佔用,並請求鎖定對方佔用的資源,從而致使惡性循環的現象。當多個事務試圖以不一樣的順序鎖定資源時,就可能會產生死鎖。多個事務同時鎖定同一個資源時,也會產生死鎖。例如,設想下面兩個事務同時處理 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目前處理死鎖的方法是,將持有最少行級排他鎖的事務進行回滾(這是相對比較簡單的死鎖回滾算法)。

鎖的行爲和順序是和存儲引擎相關的。以一樣的順序執行語句,有些存儲引擎會產生死鎖,有些則不會。死鎖的產生有雙重緣由:有些是由於真正的數據衝突,這種狀況一般很難避免,但有些則徹底是因爲存儲引擎的實現方式致使的。

死鎖發生之後,只有部分或者徹底回滾其中一個事務,才能打破死鎖。對於事務型的系統,這是沒法避免的,因此應用程序在設計時必須考慮如何處理死鎖。大多數狀況下只須要從新執行因死鎖回滾的事務便可。

3. 事務日誌

事務日誌能夠幫助提升事務的效率。使用事務日誌,存儲引擎在修改表的數據時只須要修改其內存拷貝,再把該修改行爲記錄到持久在磁盤上的事務日誌中,而不用每次都將修改的數據自己持久到磁盤。事務日誌採用的是追加的方式,所以寫日誌的操做是磁盤上一小塊區域內的順序 I/O,而不像隨機 I/O 須要在磁盤的多個地方移動磁頭,因此採用事務日誌的方法相對來講要快得多。事務日誌持久之後,內存中被修改的數據在後臺能夠慢慢地刷回到磁盤。目前大多數存儲引擎都是這樣實現的,咱們一般稱之爲預寫式日誌(Write-Ahead Logging),修改數據須要寫兩次磁盤。

若是數據的修改已經記錄到事務日誌並持久化,但數據自己尚未寫回磁盤,此時系統崩潰,存儲引擎在重啓時可以自動恢復這部分修改的數據。

4. MySQL 中的事務

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

自動提交(AUTOCOMMIT)

MySQL 默認採起了自動提交(AUTOCOMMIT)模式。也就是說,若是不是顯式地開始一個事務,則每一個查詢都被當作一個事務執行提交操做。在當前鏈接中,能夠經過設置 AUTOCOMMIT 變量來啓用或者禁用自動提交模式:

zhgxun-pro:notes zhgxun$ mysql.server start
Starting MySQL
 SUCCESS! 
zhgxun-pro:notes zhgxun$ 
zhgxun-pro:notes zhgxun$ mysql -uroot -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 3
Server version: 5.7.18 Homebrew

Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

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

mysql> SET AUTOCOMMIT = 1;
Query OK, 0 rows affected (0.00 sec)

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

mysql> SET AUTOCOMMIT = 0;
Query OK, 0 rows affected (0.00 sec)

mysql> show variables like 'AUTOCOMMIT';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | OFF   |
+---------------+-------+
1 row in set (0.01 sec)

mysql> SET AUTOCOMMIT = 1;
Query OK, 0 rows affected (0.00 sec)

mysql> quit
Bye
zhgxun-pro:notes zhgxun$ mysql.server stop
Shutting down MySQL
.. SUCCESS! 
zhgxun-pro:notes zhgxun$

1或者 ON 表示啓用,0或者 OFF 表示禁用。當 AUTOCOMMIT=0 時,全部的查詢都是在一個事務中,直到顯示地執行 COMMIT 提交或者 ROLLBACK 回滾,該事務結束,同時又開始了另外一個新事務。修改 AUTOCOMMIT 對非事務型的表,好比 MyISAM 或者內存表,不會有任何影響。對這類表來講,沒有 COMMIT 或者 ROLLBACK 的概念,也能夠說是至關於一直處於 AUTOCOMMIT 啓用的模式。

另外還有一些命令,在執行以前會強制執行 COMMIT 提交當前的活動事務。典型的例子,在數據定義語言(DDL)中,若是是會致使大量數據改變的操做,好比 ALTER TABLE,就是如此。另外還有 LOCK TABLES 等其餘語句也會致使一樣的結果。若是有須要,請檢查對應版本的官方文檔來確認全部可能致使自動提交的語句列表。

MySQL 能夠經過執行 SET TRANSCTION ISOLATION LEVEL 命令來設置隔離級別。新的隔離級別會在下一個事務開始的時候生效。能夠在配置文件中設置整個數據庫的隔離級別,也能夠只改變當前會話的隔離級別:

mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

MySQL 可以識別全部的4個 ANSI 隔離級別,InnoDB 引擎也支持全部的隔離級別。

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

MySQL 服務器層無論理事務,事務是由下層的存儲引擎實現的。因此在同一個事務中,使用多種存儲引擎是不可靠的。

若是在事務中混合使用了事務型和非事務型的表(例如 InnoDB 和 MyISAM 表),在正常提交的狀況下不會有什麼問題。

但若是該事務須要回滾,非事務型的表上的變動就沒法撤銷,這會致使數據庫處於不一致的狀態,這種狀況很難修復,事務的最終結果將沒法肯定。因此,爲每張表選擇合適的存儲引擎很是重要。

在非事務型的表上執行事務相關的操做的時候,MySQL 一般不會發出提醒,也不會報錯。有時候只有回滾的時候纔會發出一個警告:「某些非事務型的表上的變動不能被回滾」。但大多數狀況下,對非事務型表的操做都不會有提示。

隱式和顯示鎖定

InnoDB 採用的是兩階段鎖定協議(two-phase locking protocol)。在事務執行過程當中,隨時均可以執行鎖定,鎖只有在執行 COMMIT 或者 ROLLBACK 的時候纔會釋放,而且全部的鎖是在同一時刻被釋放。

另外,InnoDB 也支持經過特定的語句進行顯式鎖定,這些語句不屬於SQL規範,這些鎖定提示常常被濫用,實際上應當儘可能避免使用。

  • SELECT ...... LOCK IN SHARE MODE
  • SELECT ...... FOR UPDATE

MySQL 也支持 LOCK TABLES 和 UNLOCK TABLES 語句,這是在服務器層實現的,和存儲引擎無關。它們有本身的用途,但並不能替代事務處理。若是應用須要用到事務,仍是應該選擇事務型存儲引擎。

常常能夠發現,應用已經將表從 MyISAM 轉換到 InnoDB ,但仍是顯示地使用 LOCK TABLES 語句。這不但沒有必要,還會嚴重影響性能,實際上 InnoDB 在行級鎖工做得更好。

LOCK TABLES 和事務之間相互影響的話,狀況會變得很是複雜,在某些 MySQL 版本中甚至會產生沒法預料的結果。所以,除了事務中禁用 AUTOCOMMIT,也可使用 LOCK TALBES 外,其它任什麼時候候都不要顯式地執行 LOCK TALBES,無論使用的是什麼存儲引擎。

高性能MySQL: 第3版/(美)施瓦茨等著, 寧海元等譯, 北京: 電子工業出版社, 2013.5, 第6-12頁。

4、多版本併發控制

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

能夠認爲 MVCC 是行級鎖的一個變種,可是它在不少狀況下避免了加鎖操做,所以開銷更低。雖然實現機制有所不一樣,但大都實現了非阻塞的讀操做,寫操做也只鎖定必要的行。

MVCC 的實現,是經過保存數據在某個時間點的快照來實現的。也就是說,無論須要執行多長時間,每一個事務看到的數據都是一致的。根據事務開始的時間不一樣,每一個事務對同一張表,,同一時刻看到的數據多是不同的。若是以前沒有這方面的概念,這句話聽起來就有點迷惑。熟悉了之後會發現,這句話其實仍是很容易理解的。

前面說道不一樣存儲引擎的 MVCC 實現是不一樣的,典型的有樂觀(optimistic)併發控制和悲觀(pessimistic)併發控制。下面咱們經過 InnoDB 的簡化版行爲來講明 MVCC 是如何工做的。

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

SELECT

InnoDB 會根據如下兩個條件檢索每行記錄:

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

只有符合上述兩個條件的記錄,才能返回做爲查詢結果。

INSERT

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

DELETE

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

UPDATE

InnoDB 爲插入一行新記錄,保存當前系統版本號做爲行版本號,一樣保存當前系統版本號到原來的行做爲行刪除標識。

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

MVCC 只在 REPEATABLE READ 和 READ COMMITTED 兩個隔離級別下工做。其它兩個隔離級別都和 MVCC 不兼容,由於 READ UNCOMMITTED 老是讀取最新的數據行,而不是符合當前事務版本的數據行。而 SERIALIZATION 則會對全部讀取的行都加鎖。

高性能MySQL: 第3版/(美)施瓦茨等著, 寧海元等譯, 北京: 電子工業出版社, 2013.5, 第12-13頁。

相關文章
相關標籤/搜索