MySQL的事務(ACID)和MySQL的隔離級別
在理解事務的概念之前,接觸資料庫系統的其他高階特性還言之過早。事務就是一組原 子性的SQL査詢,或者說一個獨立的工作單元。如果資料庫引擎能夠成功地對資料庫應用該組査詢的全部語句,那麼就執行該組査詢。如果其中有任何一條語句因為崩潰或其他原因無法執行,那麼所有的語句都不會執行。也就是說,事務內的語句,要麼全部執行成功,要麼全部執行失敗。
這裡的內容並非專屬於MySQL,ACID概念可以自己去了解。
銀行應用是解釋事務必要性的一個經典例子。假設一個銀行的資料庫有兩張表:支票 (checking)表和儲蓄(savings)表。現在要從使用者Jane的支票賬戶轉移200美元到她的儲蓄賬戶,那麼需要至少三個步驟:
1.檢査支票賬戶的餘額高於200美元。
2.從支票賬戶餘額中減去200美元。
3.在儲蓄賬戶餘額中增加200美元。
上述三個步驟的操作必須打包在一個事務中,任何一個步驟失敗,則必須回滾所有的步 驟。可以用STARTTRANSACTION語句開始一個事務,然後要麼使用COMMIT提交事務將修改的資料持久保留,要麼使用ROLLBACK撒銷所有的修改。事務SQL的樣本如下:
1 START TRANSACTION; 2 SELECT balance FROM checking WHERE customer_id = 10233276; 3 UPDATE checking SET balance = balance - 200.00 WHERE customer_id = 10233276;4 UPDATE savings SET balance = balance + 200.00 WHERE customer_id = 10233276; 5 COMMIT;
單純的事務概念並不是故事的全部。試想一下,如果執行到第四條語句時伺服器崩潰了, 會發生什麼?天知道,使用者可能會損失200美元。再假如,在執行到第三條語句和第四條語句之間時,另外一個程序要刪除支票賬戶的所有餘額,那麼結果可能就是銀行在不知道這個邏輯的情況下白白給了Jane 200美元。
除非系統通過嚴格的ACID測試,否則空談事務的概念是不夠的。ACID表示原子性 (atomicity)、一致性(consistency)、隔離性(isolation)和永續性(durability)。一個執行良好的事務處理系統,必須具備這些標準特徵。
原子性(atomicity)
一個事務必須被視為一個不可分割的最小工作單元,整個事務中的所有操作要麼全部提交成功,要麼全部失敗回滾,對於一個事務來說,不可能只執行其中的一部分操作,這就是事務的原子性。
一致性(consistency)
資料庫總是從一個一致性的狀態轉換到另外一個一致性的狀態。在前面的例子中,一致性確保了,即使在執行第三、四條語句之間時系統崩潰,支票賬戶中也不會損失200美元,因為事務最終沒有提交,所以事務中所做的修改也不會儲存到資料庫中。
隔離性(isolation)
通常來說,一個事務所做的修改在最終提交以前,對其他事務是不可見的。在前面的例子中,當執行完第三條語句、第四條語句還未開始時,此時有另外一個賬戶彙總程式開始執行,則其看到的支票賬戶的餘額並沒有被減去200美元。後面我們討論隔離級別(Isolationlevel)的時候,會發現為什麼我們要說“通常來說”是不可見的。
永續性(durability)
一旦事務提交,則其所做的修改就會永久儲存到資料庫中。此時即使系統崩潰,修改的資料也不會丟失。永續性是個有點模糊的概念,因為實際上永續性也分很多不同的級別。有些永續性策略能夠提供非常強的安全保障,而有些則未必。且不可能有能做到100%的永續性保證的策略(如果資料庫本身就能做到真正的久性,那麼備份又怎麼能增加永續性呢?)。在其它章節會繼續討論MySQL中永續性的真正含義。
事務的ACID特性可以確保銀行不會弄丟你的錢。而在應用邏輯中,要實現這一點非常難,甚至可以說是不可能完成的任務。一個相容ACID的資料庫系統,需要做很多複雜但可能使用者並沒有覺察到的工作,才能確保ACID的實現。
就像鎖粒度的升級會增加系統開銷一樣,這種事務處理過程中額外的安全性,也會需要 資料庫系統做更多的額外工作。一個實現了ACID的資料庫,相比沒有實現ACID的資料庫,通常會需要更強的CPU處理能力、更大的記憶體和更多的磁碟空間。正如本章不斷重複的,這也正是MySQL的儲存引擎架構可以發揮優勢的地方。使用者可以根據業務是否需要事務處理,來選擇合適的儲存引擎。對於一些不需要事務的査詢類應用,選擇一個非事務型的儲存引擎,可以獲得更高的效能。即使儲存引擎不支援事務,也可以通過LOCKTABLES語句為應用提供一定程度的保護,這些選擇使用者都可以自主決定。
1.隔離級別
隔離性其實比想象的要複雜。在SQL標準中定義了四種隔離級別,每一種級別都規定了一個事務中所做的修改,哪些在事務內和事務間是可見的,哪些是不可見的。較低級別的隔離通常可以執行更高的併發,系統的開銷也更低。
提示:每種儲存引擎實現的隔離級別不盡相同。如果熟悉其他的資料庫產品,可能會發現 某些特性和你期望的會有些不一樣(但本節不打算討論更詳細的內容)。讀者可以根據所選擇的儲存引擎,査閱相關的手冊。
下面簡單地介紹一下四種隔離級別。
READUNCOMMITTED(未提交讀)
在READUNCOMMITTED級別,事務中的修改,即使沒有提交,對其他事務也都是可見 的。事務可以讀取未提交的資料,這也被稱為髒讀(DirtyRead)。這個級別會導致很多問題,從效能上來說,READUNCOMMITTED不會比其他的級別好太多,但卻缺乏其他級別的很多好處,除非真的有非常必要的理由,在實際應用中一般很少使用。
READCOMMITTED(提交讀)
大多數資料庫系統的預設隔離級別都是READCOMMITTED(但MySQL不是)。READ COMMITTED滿足前面提到的隔離性的簡單定義:一個事務開始時,只能“看見”已經提交的事務所做的修改。換句話說,一個事務從開始直到提交之前,所做的任何修改對其他事務都是不可見的。這個級別有時候也叫做不可重複讀(nonrepeatableread),因為兩次執行同樣的査詢,可能會得到不一樣的結果。
REPEATABLEREAD(可重複讀)
REPEATABLEREAD解決了髒讀的問題。該級別保證了在同一個事務中多次讀取同樣記錄的結果是一致的。但是理論上,可重複讀隔離級別還是無法解決另外一個幻讀(PhantomRead)的問題。所謂幻讀,指的是當某個事務在讀取某個範圍內的記錄時,另外一個事務又在該範圍內插入了新的記錄,當之前的事務再次讀取該範圍的記錄時,會產生幻行(PhantomRow)。InnoDB和XtraDB儲存引擎通過多版本併發控制(MVCC,MultiversionConcurrencyControl)解決了幻讀的問題。本章稍後會做進一步的討論。
可重複讀是MySQL的預設事務隔離級別。
SERIALIZABLE(可序列化)
SERIALIZABLE是最髙的隔離級別。它通過強制事務序列執行,避免了前面說的幻讀的問題。簡單來說,SERIALIZABLE會在讀取的每一行資料上都加鎖,所以可能導致大量的超時和鎖爭用的問題。實際應用中也很少用到這個隔離級別,只有在非常需要確保資料的一致性而且可以接受沒有併發的情況下,才考慮採用該級別。
表1-1:ANSISQL隔離級別 | ||||
隔離級別 | 髒讀可能性 | 不可重複讀可能性 | 幻讀可能性 | 加鎖讀 |
READUNCOMMITTED | Yes | Yes | Yes | No |
READCOMMITTED | No | Yes | Yes | No |
REPEATABLEREAD | 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/0,而不像隨機I/O需要在磁碟的多個地方移動磁頭,所以採用事務日誌的方式相對來說要快得多。事務日誌持久以後,記憶體中被修改的資料在後臺可以慢慢地刷回到磁碟。目前大多數儲存引擎都是這樣實現的,我們通常稱之為預寫式日誌(Write-AheadLogging),修改資料需要寫兩次磁碟。
如果資料的修改已經記錄到事務日誌並持久化,但資料本身還沒有寫回磁碟,此時系統 崩潰,儲存引擎在重啟時能夠自動恢復這部分修改的資料。具體的恢復方式則視儲存引擎而定。
4.MySQL中的事務
MySQL提供了兩種事務型的儲存引擎:InnoDB和NDBCluster。另外還有一些第三方 儲存引擎也支援事務,比較知名的包括XtraDB和PBXT。後面將詳細討論它們各自的一些特點。
自動提交(AUTOCOMMIT)
MySQL預設採用自動提交(AUT0C0MMIT)模式。也就是說,如果不是顯式地開始一個事務,則每個查詢都被當作一個事務執行提交操作。在當前連線中,可以通過設定AUTOCOMMIT變數來啟用或者禁用自動提交模式:
mysql> show variables like "AUTOCOMMIT"; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | autocommit | ON | +---------------+-------+ 1 row in set (0.00 sec) mysql> SET AUTOCOMMIT =1;
1或者ON表示啟用,0或者OFF表示禁用。當AUTOCOMMIT=0時,所有的查詢都是在一個事務中,直到顯式地執行COMMIT提交或者ROLLBACK回滾,該事務結束,同時又開始了另一個新事務。修改AUTOCOMMIT對非事務型的表,比如MylSAM或者記憶體表,不會有任何影響。對這類表來說,沒有COMMIT或者ROLLBACK的概念,也可以說是相當於一直處於AUT0C0MMIT啟用的模式。
另外還有一些命令,在執行之前會強制執行COMMIT提交當前的活動事務。典型的例子,在資料定義語言(DDL)中,如果是會導致大量資料改變的操作,比如ALTERTABLE,就是如此。另外還有LOCKTABLES等其他語句也會導致同樣的結果。如果有需要,請檢査對應版本的官方文件來確認所有可能導致自動提交的語句列表。
MySQL可以通過執行SETTRANSACTIONISOLATIONLEVEL命令來設定隔離級別。新的隔離級別會在下一個事務開始的時候生效。可以在配置檔案中設定整個資料庫的隔離級 別,也可以只改變當前會話的隔離級別:
mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
MySQL能夠識別所有的4個ANSI隔離級別,InnoDB引擎也支援所有的隔離級別。
在事務中混合使用儲存引擎
MySQL伺服器層不管理事務,事務是由下層的儲存引擎實現的。所以在同一個事務中, 使用多種儲存引擎是不可靠的。
如果在事務中混合使用了事務型和非事務型的表(例如InnoDB和MyISAM表),在正常提交的情況下不會有什麼問題。
但如果該事務需要回滾,非事務型的表上的變更就無法撤銷,這會導致資料庫處於不一 致的狀態,這種情況很難修復,事務的最終結果將無法確定。所以,為每張表選擇合適的儲存引擎非常重要。
在非事務型的表上執行事務相關操作的時候,MySQL通常不會發出提醒,也不會報錯。 有時候只有回滾的時候才會發出一個警告:“某些非事務型的表上的變更不能被回滾”。但大多數情況下,對非事務型表的操作都不會有提示。
隱式和顯式鎖定
InnoDB採用的是兩階段鎖定協議(two-phaselockingprotocol)。在事務執行過程中,隨時都可以執行鎖定,鎖只有在執行COMMIT或者ROLLBACK的時候才會釋放,並且所有的鎖是在同一時刻被釋放。前面描述的鎖定都是隱式鎖定,InnoDB會根據隔離級別在需要的時候自動加鎖。
另外,InnoDB也支援通過特定的語句進行顯式鎖定,這些語句不屬於SQL規範(這些鎖定提示經常被濫用,實際上應當儘量避免使用):
• SELECT ... LOCK IN SHARE MODE
• SELECT ... FOR UPDATE
MySQL也支援LOCKTABLES和UNLOCKTABLES語句,這是在伺服器層實現的,和儲存引擎無關。它們有自己的用途,但並不能替代事務處理。如果應用需要用到事務,還是應該選擇事務型儲存引擎。
經常可以發現,應用已經將表從MylSAM轉換到InnoDB,但還是顯式地使用LOCK TABLES語句。這不但沒有必要,還會嚴重影響效能,實際上InnoDB的行級鎖工作得更好。
提示:LOCKTABLES和事務之間相互影響的話,情況會變得非常複雜,在某些MySQL版本中甚至會產生無法預料的結果。因此,建議除了事務中禁用了AUT0C0MMIT,可以使用LOCKTABLES之外,其他任何時候都不要顯式地執行LOCKTABLES,不管使用的是什麼儲存引擎。
作者:小家電維修
相見有時,後會無期。