高性能MySQL(一)——邏輯架構、鎖、事務和併發控制

 

一、MySQL邏輯架構

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

MySQL的邏輯架構圖如下圖所示。它有助於深入理解MySQL服務器。

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

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

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

 

二、鎖

無論何時,只要有多個查詢需要在同一時刻修改數據,都會產生併發控制的問題。MySQL在兩個層面進行併發控制:服務器層與存儲引擎層

 

2.1 讀寫鎖

在處理併發讀或寫時,可以通過實現一個由兩種類型的鎖組成的鎖系統來解決問題。這兩種類型的鎖通常稱爲共享鎖(shared  lock)和排他鎖(exclusive  lock),也叫讀鎖(read  lock)和寫鎖(write  lock)。

2.1.1 共享鎖(shared  lock)/ 讀鎖(read  lock)

讀鎖是共享的,或說是互相不阻塞的。多個客戶在同一時間可以同時讀取同一個資源,而互不干擾。

 

2.1.2 排他鎖(exclusive  lock) / 寫鎖(write  lock)

寫鎖是排他的。也就是說一個寫鎖會阻塞其他的寫鎖和讀鎖,這是出於安全策略的考慮。只有這樣,才能確保在給定的時間裏,只有一個用戶能執行寫入,並防止其他用戶讀取正在寫入的同一資源。

 

2.2 鎖粒度

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

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

所謂的鎖策略,就是在鎖的開銷和數據的安全性之間尋求平衡,這種平衡當然也會影響到性能。一般都是在表上施加行級鎖(row  level  lock),並以各種複雜的方式來實現,以便在鎖比較多的情況下儘可能地提供更好的性能。

MySQL則提供了多種選擇。每種存儲引擎都可以實現自己的鎖策略和鎖粒度。將鎖粒度固定在某個級別,可以爲某些特定的應用場景提供更好的性能,但同時卻會失去對另外一些應用場景的良好支持。好在MySQL支持多個存儲引擎的架構,所以不需要單一的通用解決方案。下面介紹最重要的幾種鎖策略。

存儲引擎的鎖粒度如下:

  • MyISAM 和 MEMORY 存儲引擎採用的是表級鎖(table-level locking);
  • BDB 存儲引擎採用的是頁面鎖(page-level locking),但也支持表級鎖;
  • InnoDB 存儲引擎既支持行級鎖(row-level locking),也支持表級鎖,但默認情況下是採用行級鎖。

2.3.1 表鎖(table lock)

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

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

 

2.3.2 頁鎖(page  lock)

頁銷的加鎖時間界於表鎖和行鎖之間,會出現死鎖,鎖的粒度界於表鎖和行鎖之間,併發度一般。應用於BDB引擎。

 

2.3.3 行級鎖(row lock)

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

 

2.3.4 不同鎖粒度的比較

鎖粒度 特點
表鎖 開銷小,加鎖快,不會出現死鎖。但鎖粒度大,發生鎖衝突的概率最高,併發度最低。存儲引擎總是一次性同時獲得所需要的鎖以及總是按相同的順序獲得表鎖來避免死鎖;更適合於以查詢爲主,併發用戶少,只有少量按索引條件更新數據的應用。
頁鎖 開銷的加鎖時間界於表鎖和行鎖之間,會出現死鎖,鎖的粒度界於表鎖和行鎖之間,併發度一般。
行鎖 開銷大,加鎖慢,會出現死鎖。但鎖粒度最小,發生鎖衝突的概率也最低,併發度最高。可以最大程序的支持併發,同時也帶來了最大的鎖開銷。在InnoDB中,除了單個SQL組成的事務外,鎖是逐步獲得的,這就決定了在InnoDB中發生死鎖是可能的。行鎖只在存儲引擎層實現,而在MySQL服務器層沒有實現。行級鎖更適合於有大量按索引條件併發更新少量不同數據,同時又有併發查詢的應用,如一些事務處理(OLTP)系統。

 

三、事務

3.1 事務概述

事務就是一組原子性的SQL查詢,或說是一個獨立的工作單元。事務內的語句,要麼全部執行,要麼全部執行失敗。

銀行應用是解釋事務必要性的一個經典例子。假設一個銀行的數據庫有兩張表:支票(checking)表和儲蓄(savings)表。現在要從用戶張三的支票賬戶轉移1000元到他的儲蓄賬戶,那需要至少三個步驟:

  1. 檢查賬戶的餘額高於1000元;
  2. 從支票賬戶餘額中減去1000元;
  3. 在儲蓄賬戶餘額中增加1000元。

上述三個步驟的操作必須打包在一個事務中,任何一個步驟失敗,則必須回滾所有的步驟。即三個步驟要麼全部執行成功,要不全部執行失敗,不能存在3步中有成功和不成功同時存在的情況。

可以用START  TRANSACTION語句開始一個事務,然後要麼使用COMMIT提交事務將修改的數據持久保留,要麼使用ROLLBACK撤銷所有的修改。事務SQL的樣本如下:

1   START TRANSACTION;
2   SELECT  balance  FROM checking where customer_id = 10233276;
3   UPDATE checking SET balance = balance - 1000 WHERE customer_id = 10233276;
4   UPDATE savings SET balance = balance + 1000 WHERE customer_id = 10233276;
5   COMMIT;

即提交事務的語句是:

START TRANSACTION;
UPDATE ......;      -- 修改語句,插入、刪除、修改都可以
COMMIT;

回滾的語句如下。ROLLBACK只能在一個事務處理內使用(在執行一條START  TRANSACTION命令後)。

START TRANSACTION;
UPDATE ......;      -- 修改數據語句,如插入、刪除和更新等
ROLLBACK;

 

3.2 事務中常見的概念

髒讀(Dirty  Read):一個事務正對一條記錄進行修改,在這個事務完成並提交前,這條記錄的數據就處於不一致的狀態,且這條數據對其它事務是可見的。這時,另一個事務也來讀取同一條記錄,若不加控制,第二個事務讀取了這些髒的數據,並進數據進行處理,就會產生未提交的數據依賴關係。主要是因爲其它事務讀取了未提交的數據

不可重複讀(Non-Repeatable  Read):一個事務在讀取某些數據後的某個時間,再次讀取之前讀取過的數據,即發現後一次讀取的數據已經發生了改變。兩次執行同樣的查詢,得到的卻是不一樣的結果。被其它事務修改並且提交

可重複讀(Repeatable  Read):同一個事務多次執行同樣的查詢,讀取到的數據是一樣的。

幻讀(Phantom  Read):當某個事務在讀取某個範圍內的記錄時,另一個事務又在該範圍插入了新的記錄,當之前的事務再次讀取該範圍的記錄時,會產生幻行(Phantom  Row)。這裏主要是說行數發生了變化,而非是數據值變化了,就是讀取到的記錄數發生了變化。被其它事務刪除或插入了數據

髒讀、不可重複讀和幻讀,其實都是數據庫一致性的問題,必須由數據庫提供一定的事務隔離級別來解決。一是可以讀取數據前對其加鎖,阻止其它事務對數據進行修改。另一種方法是不加任何鎖,通過一定機制生成一個數據庫請求時間點的一致性快照(Snapshot),並用這個快照來提供一定級別的一致性讀取。

不可重複讀的重點是被其它事務修改了數據,這樣導致讀取到的數據值變化了,而記錄數並沒有變化。而幻讀的重點是被其它事務刪除或新增了數據,讀取的記錄數發生了變化。而髒讀是其它事務讀取了前一個事務執行的修改操作,這個修改操作完成了但未提交,兩個事務間的數據又是可見的

更新丟失(Lost  Update):當兩個或多個事務選擇同一行數據,然後基於最初選定的值更新該行的數據。由於每個事務都不知道其他事務的存在,就會發生更新丟失的問題——最後的更新操作覆蓋了前面的更新。

更新丟失通常是應該完全避免的。但防止丟失更新,不能僅靠數據庫事務控制器來解決,需要應用程序對要更新的數據加必要的鎖來解決。可以說,防止丟失更新是應該處理的責任。

 

3.3 事務的ACID特性

ACID表示原子性(atomicity)、一致性(consistency)、隔離性(isolation)和持久性(durability)。一個運行良好的事務處理系統,必須具備這些標準特徵。

原子性(atomicity)

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

一致性(consistency)

數據庫總是從一個一致性的狀態轉換到另一個一致性的狀態。在前面的銀行例子中,一致性得到了確保。即使在第三、四條語句之間時系統崩潰,支票賬戶中也不會損失1000元,因爲事務最終沒有提交,所以事務中所做的修改不會保存到數據庫中。

隔離性(isolation)

一個事務所做的修改在最終提交以前,對其他事務是不可見的。在前面的例子中,當執行完第三條語句、第四條語句還未開始時,此時另一個賬戶彙總程序開始運行,則它看到支票賬戶的餘額並沒有被減去1000元。

持久性(durability)

一旦事務提交,則其所做的修改就會永遠保存到數據庫中。此時即使系統崩潰,修改的數據也不會丟失。

 

事務的ACID特性可以確保銀行不會弄丟你的錢(在數據庫層面來說的)。而在實際的應用邏輯中,要實現這一點非常難,甚至可以說是不可能完成的任務。一個兼容ACID的數據庫系統,要做很多複雜但可能用戶並沒有覺察到的工作,才能確保ACID的實現。

就像鎖粒度的升級會增加系統開銷一樣,這種事務處理過程中額外的安全性,也需要數據庫系統做更多的額外工作。對MySQL,用戶可以根據業務是否需要事務,來選擇合適的存儲引擎。對一些不需要事務的查詢類應用,選擇一個非事務類型的存儲引擎,可以獲得更高的性能。即使存儲引擎不支持事務,也可以通過LOCK  TABLES語句爲應用提供一定程序的保護。

 

3.4 隔離級別

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

每種存儲引擎實現的隔離級別也不盡相同。若熟悉其它的數據庫產品,可能會發現某些特性和期望的會有些不一樣。這裏不做詳細的討論,有興趣可查閱相關手冊。

READ  UNCOMMITTED(未提交讀)

在READ  UNCOMMITTED級別,事務中的修改,即使沒有提交,對其它事務也是可見的。事務可以讀取未提交的數據,這也稱爲髒讀(Dirty  Read)。這個級別會導致很多問題,從性能上說,READ  UNCOMMITED不會比其他級別好太多,但卻缺乏其他級別事務的很多好處,除非真的有非常必要的理由,在實際應用中一般很少使用

READ  COMMITTED(提交讀,簡稱RC)

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

REPEATABLE  READ(可重複讀,簡稱RR)

REPEATABLE  READ解決了髒讀的問題。這是MySQL默認的隔離級別。該級別保證了在同一個事務中多次讀取同樣的記錄的結果是一致的。但在理解上,可重複讀隔離級別還是無法解決另一個幻讀(Phantom  Read)的問題。InnoDB和XtraDB存儲引擎通過多版本併發控制(MVCC,Multiversion  Concurrency  Control)解決了幻讀的問題。

SERIALIZABLE(可串行化)

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

 

3.5 死鎖

死鎖是指兩個或多個事務在同一資源上相互佔用,並請求鎖定對方佔用的資源,從而導致惡性循環的現象。當多個事務試圖以不同的順序鎖定資源時,就可能產生死鎖。多個事務同時鎖定同一資源時,也會產生死鎖。比如,設想下面兩個事務同時處理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.6 事務日誌

事務日誌可幫助提高事務的效率。使用事務日誌,存儲引擎在修改表的數據時只需要修改其內存拷貝,再把該修改行爲記錄到持久在磁盤上的事務日誌中,而不用每次都將修改的數據本身持久到磁盤中

事務日誌採用的是追加的方式,因此寫日誌的操作是磁盤上一小塊區域內的順序I / O,而不像隨機I / O需要在磁盤的多個地方移動磁頭,所以採用事務日誌的方式相對來說要快得多。

目前大多數存儲引擎都是這樣實現的,通常稱之爲預寫式日誌(Write-Ahead  Logging),修改數據需要寫兩次磁盤。

若數據的修改已經記錄到事務日誌中並持久了,但數據本身還沒有寫回磁盤,此時系統存儲引擎在重啓時能夠自動恢復這部分修改的數據。具體的恢復方式要視存儲引擎來定。

 

3.6.1 redo  log和undo  log

InnoDB的事務日誌包含redo  log和undo  log。redo  log是記錄修改的日誌,提供非回滾的數據恢復操作,undo  log是回滾日誌,提供回滾操作。undo  log並不是redo  log的逆向過程,它們都是用來恢復數據的日誌。

redo  log

若系統突然崩潰,一些還存在於緩存中的修改還未來的及同步到磁盤中,此時可以用redo  log來恢復這些數據。redo  log就是記錄這些修改的日誌。redo  log通常是物理日誌,記錄的是數據頁的物理修改,而不是將某一行或某幾行修改成什麼樣。它是用來恢復提交後的物理數據頁(恢復數據頁,且只能恢復到最後一次提交的位置)。

redo  log包含兩部分內容:內存中的日誌緩衝(redo  log  buffer,該部分數據是容易丟失的)和磁盤上的重做日誌文件(redo  log  file,該部分日誌內容是持久的)。InnoDB在事務提交時,必須先將事務的所有日誌寫入到磁盤上的redo  log file和undo  log  file中進行持久化。爲了確保每次事務操作時日誌都能寫入到事務日誌文件中,在每次將log  buffer中的日誌寫入到日誌文件的過程中,都會調用一次操作系統的fsync操作(即fsync()系統調用)。需要注意的是,一般所說的log  file並不是磁盤上的物理日誌文件,而是操作系統 的緩存中的log  file。

redo  log不是二進制日誌,雖然二進制日誌中也記錄了InnoDB的很多操作。redo  log是記錄數據庫中每個而的修改,是物理格式上的日誌。在數據準備修改前寫入緩存中的redo  log中,然後纔對緩存中的數據進行修改,且保存在發出事務提指令時,先向緩存中的redo  log寫入日誌,寫入完成後再執行提交操作。在redo  log中,同一個事務可能會有多次記錄,最後一個提交的事務記錄會覆蓋所有未提交的事務記錄。是併發寫入的,所以不同事務間的不同版本的記錄會穿插寫入到redo  log文件中。它具有冪等性,因此記錄日誌的方式很簡練。冪等性的是指多次操作前後狀態是一樣的,比如插入一行數據後又把它刪除,前後狀態沒有變化。

redo  log是以塊爲單位來進行存儲的,每個塊佔用512字節,稱爲redo  log  block。不管是log  buffer、os  buffer還是redo  log  file  disk,都是這樣以512字節的塊來存儲的。

undo  log

undo  log是爲了事務的回滾而記錄的日誌信息,用來回滾行記錄到某個版本。undo  log一般是邏輯日誌,根據每行記錄進行日誌記錄。undo  log有插入、刪除、更新數據三種類型,每種類型都會產生不同的undo  log。undo  log主要用兩個作用:提供回滾和多版本併發控制(MVCC)。

在修改數據時,不僅記錄了redo  log,還記錄了相對應的undo  log。若因爲某些原因導致事務失敗或回滾了,那可藉助undo  log進行回滾。說undo  log記錄的是邏輯日誌,可以認爲當delete一條數據時,undo  log中會記錄一條對應的insert記錄,反之亦然;當update一條數據時,它記錄一條相應的update記錄。當執行回滾操作時,就可以根據undo  log的邏輯記錄讀取到相應的內容進行回滾。不是怎麼操作的就怎麼記錄,這樣的記錄在回滾時會很麻煩。

應用到多版本併發控制時,當讀取的一行數據被其它事務鎖定時,它可以從undo  log中分析出該行記錄的以前的數據是什麼樣的,從而提供該行版本信息,記用記實現非鎖定一致性讀取。

undo  log是採用段(segment)的方式來記錄的,在記錄每個undo操作時會佔用一個undo  log  segment。

另外,undo  log也會產生redo  log,因爲undo  log也要實現持久性保護。

如想了解更多,可查看詳細分析MySQL事務日誌(redo log和undo log)

 

3.7 MySQL中的事務

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

在5.0前,MySQL的binlog格式只有statement一種格式,而主從複製存在了大量的不一致,故選用REPEATABLE爲MySQL的隔離級別

MySQL默認採用自動提交(AUTOCOMMIT)模式。就是若不是顯式地開始一個事務,則每個查詢都被當作一個事務執行提交操作。在當前連接中,可通過設置AUTOCOMMIT變量來啓用或禁用自動提交模式。1或ON表示開啓,0或OFF表示禁用。當禁用自動提交時,所有的查詢都是在一個事務中,直到顯式的執行COMMIT提交或ROLLBAK回滾,該事務結束。

MySQL通過SET  TRANSACTION  ISOLATION  LEVEL命令來設置隔離級別。新的隔離級別會在下一個事務開始時生效

SET  SESSION  TRANSACTION  ISOLATION  LEVEL READ  COMMITTED;

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

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,不管用的是什麼存儲引擎

 

四、多版本併發控制(MVCC)

4.1 MVCC

MySQL大多數事務型存儲引擎實現的都不是簡單的行級鎖。基於提升性能的考慮,一般都同時使用了多版本併發控制(MVCC,Multiversion  Concurrency Control)。Oracle、PostgreSQL也是如此,但各自的實現機制不盡相同,因爲MVCC沒有統一的實現標準。

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

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

 

4.2 悲觀鎖與樂觀鎖

樂觀鎖(樂觀併發控制)與悲觀鎖(悲觀併發控制)是併發控制主要採用的手段。無論是悲觀鎖還是樂觀鎖,都是人們定義出來的概念,可認爲是一種思想。其實不僅僅是關係型數據庫,像memcache、hibernate、tair等都有類似的概念。

對不同業務場景,應該選用不同的併發控制方式。不要把這兩種鎖狹義的理解 爲DBMS中的概念,更不要把數據庫中提供的鎖機制(行鎖、表鎖、排他鎖、共享鎖)混爲一談。其實,在DBMS中,悲觀鎖正是利用數據庫本身提供的鎖機制來實現的

 

4.2.1 悲觀鎖

當對數據庫的一條數據進行修改時,爲避免同時被其他人修改,最直接的辦法就是對該數據加鎖以防止併發。這種藉助數據庫鎖機制在修改數據之前先鎖定,再修改的方式稱之爲悲觀併發控制,也叫悲觀鎖(Pessimistic  Concurrency  Control,PCC)。

總是假設是最壞的情況,持一種悲觀的態度,每次獲得數據時都認爲別人會修改這個數據,所以每次都對要拿的數據加鎖,這樣要拿這個數據時,因爲這個已經上了鎖,所以會阻塞,直接別人拿到鎖。

它可以阻止一個事務以影響其他用戶的方式來修改數據。如果一個事務執行的操作都某行數據應用了鎖,那只有當這個事務把鎖釋放,其他事務才能夠執行與該鎖衝突的操作。悲觀併發控制主要用於數據爭用激烈的環境,以及發生併發衝突時使用鎖保護數據的成本要低於回滾事務的成本的環境中

悲觀鎖的流程

  • 在對任意記錄進行修改前,先嚐試爲該記錄加上排他鎖(exclusive locking);
  • 如果加鎖失敗,說明該記錄正在被修改,那麼當前查詢可能要等待或者拋出異常。 具體響應方式由開發者根據實際需要決定;如果成功加鎖,那麼就可以對記錄做修改,事務完成後就會解鎖了;
  • 其間如果有其他對該記錄做修改或加排他鎖的操作,都會等待解鎖或直接拋出異常。

MySQL InnoDB中使用的悲觀鎖

要使用悲觀鎖,我們必須關閉mysql數據庫的自動提交屬性,因爲MySQL默認使用autocommit模式,也就是說,當你執行一個更新操作後,MySQL會立刻將結果進行提交。set autocommit=0。

//0.開始事務
begin;/begin work;/start transaction; (三者選一就可以)
//1.查詢出商品信息
select status from t_goods where id=1 for update;
//2.根據商品信息生成訂單
insert into t_orders (id,goods_id) values (null,1);
//3.修改商品status爲2
update t_goods set status=2;
//4.提交事務
commit;/commit work;

在以上SQL中,對id = 1的記錄修改前,先通過for  update的方式進行加鎖。這就是典型的悲觀鎖策略。若以上修改庫存的代碼併發,同一時間只有一個線程可以開啓事務並獲得id = 1的鎖,其它事務必須等待本次事務提交後才能執行。這樣可以保證當前的數據不會被其他事務修改。 

上面的查詢中,使用select ... for  update的方式,這樣就通過開啓排他鎖的方式實現了悲觀鎖。此時在t_goods表中,id爲1的數據就被鎖定了,其它事務必須等本次事務提交了後才能執行。

優點與不足

悲觀併發控制實際上是“先取鎖再訪問”的保守策略,爲數據庫處理的安全提供了保證。但在效率方面,處理加鎖的機制會讓數據庫產生額外的開銷,還有增加產生死鎖的機會。另外,在只讀型事務處理場景中由於不會產生衝突,也沒有必要加鎖,這樣做只會增加系統負載,降低系統的吞吐量。

 

4.2.2 樂觀鎖

在關係型數據庫系統中,樂觀併發控制又叫樂觀鎖(Optimistic  Concurrency  Control,OCC),是一種併發控制方法。它假設多用戶併發情況下,事務在處理時不會彼此影響,各事務能在不產生鎖的情況下處理各自影響的那部分數據。在提交數據更新前,每個事務會先檢查在該事務讀取數據後,有沒有其他事務又修改了該數據。若其他事務有更新的話,正在提交的事務會進行回滾。樂觀事務控制最早由孫祥重(H.T.Kung)教授提出。

樂觀鎖相對悲觀鎖而言,樂觀鎖假設數據一般情況下不會造成衝突,所以在數據進行提交更新時,纔會正式對數據庫的衝突與否進行檢查,若發生衝突了,則返回錯誤信息,讓用戶決定如何去做。在對數據庫進行處理時,樂觀鎖並不會使用提供的鎖機制。

樂觀鎖總是假設是最樂觀的情況,持樂觀的態度,每次拿數據時都認爲別人不會修改要拿的數據,所以不上鎖。但會在修改時判斷在此期間這個數據有沒有被別人修改過。具體實現有版本號控制和CAS

數據版本,爲數據增加的一個版本標識。當讀取數據時,將版本標識的值一同讀出,數據每更新一次,同時對版本標識進行更新。當提交更新時,會判斷數據庫表對應記錄的當前版本信息與第一次取出的版本標識進行比對,若一致則更新,否則認爲是過期數據,不做更新。

實現數據版本的方式有兩種,一是使用版本號,二是使用時間戳。使用版本號時,會在表中增加一個版本號字段,它是一個整數,初始值爲0,每更新一次加1。比如說當前讀取了數據,該條數據的版本號是3,過了一會更新該數據,此時再從表中取出該數據,若重新取出的數據的版本號還是3,則就更新,若不是3(比如是4,那說明有人已經更新過該條數據了)則不更新,說明當前要更新的數據已經過期了。

1.查詢出商品信息
select (status,status,version) from t_goods where id=#{id}
2.根據商品信息生成訂單
3.修改商品status爲2
update t_goods set status=2,version=version+1 where id=#{id} and version=#{version};

上面的SQL還有一定的問題,一旦發生高併發,只有一個線程可以修改成功,那就會存在大量的失敗。對像淘寶這樣的電商網站,高併發是常有的事,總讓用戶感知到失敗顯然是不合理的。所以,還是要想辦法減少樂觀鎖的粒度。

有一條比較好的建議,可減少樂觀鎖的力度,最大程度上提升吞吐率,提高併發能力。如下:

//修改商品庫存 
update item set quantity=quantity - 1 where id = 1 and quantity - 1 > 0

在上面的SQL中,若用戶下單數爲1,則通過 quantity - 1 > 0 的方式進行樂觀鎖控制。此update語句,在執行過程中,會在一次原子操作中自己查詢一遍quantity的值,並將其減1。

它適用於多讀的場合,這樣能提高吞吐量。像數據庫提供的類似write_condition機制,其實就是樂觀鎖思想的體現。若對數據庫事務要求很高的場合。

優點與不足

樂觀鎖認爲事務間的數據競爭的概率比較小,因此儘可能直接做下去,直到提交時纔去處理,所以不會產生任何鎖和死鎖

如何選擇

在不同場合,是用悲觀鎖還是樂觀鎖呢?

1,樂觀鎖並未真正加鎖,效率高。一旦鎖的粒度掌握不好,更新失敗的概念就會比較高,容易發生業務失敗;

2,悲觀鎖依賴數據庫鎖,效率低,更新失敗的概念較低。

隨着互聯網高併發、高性能、高可用三高架構的提出,悲觀鎖已經越來越少的被使用到生產環境中了,尤其是併發量比較大的業務場景。

 

4.3 InnoDB的併發控制

前面提到不同存儲引擎的MVCC實現是不同的,典型的有樂觀(optimistic)併發控制和悲觀(pessimistic)併發控制。下面通過InnoDB的簡單版行爲來說明MVCC是如何工作的。

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

SELECT(查詢)

InnoDB會根據以下兩個條件檢查每行記錄,只有符號下面兩個條件的記錄,才能返回作爲查詢結果。

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

INSERT(插入)

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

DELETE(刪除)

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

UPDATE(更新)

爲插入一行新記錄,保存當前系統版本號作爲行版本號,同時保存當前系統版本號到原來的行作爲行刪除標識(更新就是把舊數據刪除,同時插入一條新的數據)。

 

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

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

 

參考:

1,《高性能MySQL》第三版

2,詳細分析MySQL事務日誌(redo log和undo log)

3,https://www.hollischuang.com/archives/934

 

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