關係數據庫的事務隔離、鎖定與並發控制

事務隔離(Isolation),指的是在數據庫系統中並發事務(Transaction)之間的可見性,以及如何相互影響的定義。事務隔離是ACID的四個特性(原子性、一致性、隔離性、持久性)之一。在ANSI/ISO的SQL標準中,定義了四個事務隔離級別,分別用於不同的場合。在傳統的數據庫系統的實現中,一般實現不同等級的隔離性的方法是使用鎖定(lock),根據具體需求可細分爲讀取鎖(read lock)寫入鎖(write lock)範圍鎖(range-locks)

可序列化

可序列化(SERIALIZABLE)最高的隔離級別。在此級別下,所有事務的完整性都被保留,這意味着所有的事務可以被序列化地執行。當只有兩個事務之前沒有任何衝突時,纔能並發地執行。

可重複讀取

可重複讀取(REPEATABLE READS)級別下,數據庫系統會在在整個事務期間保持所有讀取鎖寫入鎖,但相較於可序列化範圍鎖不會被管理,所以幻象讀取(phantom reads)可能會出現。

授權讀取

授權讀取(READ COMMITTED)級別下,數據庫系統在整個事務期間保持寫入鎖,但讀取鎖會在SELECT執行後立即釋放,所以不可重複讀取(non-repeatable reads)可能會出現。

未授權讀取

未授權讀取(READ UNCOMMITTED)最低的隔離級別。這個級別允許出現骯髒讀取(dirty reads)

下列的示例解釋了幻象讀取(phantom reads)不可重複讀取(non-repeatable reads)骯髒讀取(dirty reads)。數據在下表中定義:

users
id name age
1 Joe 20
2 Jill 25

骯髒讀取

當一個事務試圖讀取另一個還未提交的事務正在修改的某一行數據時,骯髒讀取(dirty reads)就會發生。

下列示例中,事務2正在修改某行,但還沒有提交。事務1試圖讀取這一行。如果事務2回滾了(rolls back)變更,或者後面又進行了其他的修改,那麼事務1就獲得了骯髒(dirty)的數據。

事務1 事務2
sql /* Query 1 */ SELECT * FROM users WHERE id = 1;
sql /* Query 2 */ UPDATE users SET age = 21 WHERE id = 1; /* No commit here */
sql /* Query 1 */ SELECT * FROM users WHERE id = 1;
sql ROLLBACK; /* lock-based DIRTY READ */

不可重複讀取

當一個事務正在執行的時候,對某一行兩次讀取的結果不一致,則稱發生了不可重複讀取(non-repeatable reads)

以下示例中事務1讀取了某行,之後事務2立刻修改了這一行並提交了結果,事務1再讀取這一行的時候,結果就不一致了。

事務1 事務2
sql /* Query 1 */ SELECT * FROM users WHERE id = 1;
sql /* Query 2 */ UPDATE users SET age = 21 WHERE id = 1; COMMIT; /* in multiversion concurrency control, or lock-based READ COMMITTED */
sql /* Query 1 */ SELECT * FROM users WHERE id = 1; COMMIT; /* lock-based REPEATABLE READ */

幻象讀取

幻象讀取(phantom reads)指的是兩次集合查詢之間返回了不一致的結果。以下示例展現了這一現象。

事務1 事務2
sql /* Query 1 */ SELECT * FROM users WHERE age BETWEEN 10 AND 30;
sql /* Query 2 */ INSERT INTO users VALUES ( 3, 'Bob', 27 ); COMMIT;
sql /* Query 1 */ SELECT * FROM users WHERE age BETWEEN 10 AND 30;

隔離級別與讀取現象

隔離級別 骯髒讀取 不可重複讀取 幻象讀取
未授權讀取 可能發生 可能發生 可能發生
授權讀取 - 可能發生 可能發生
可重複讀取 - - 可能發生
可序列化 - - -

隔離級別與鎖定

隔離級別 寫入鎖 讀取鎖 範圍鎖
未授權讀取 - - -
授權讀取 需要 - -
可重複讀取 需要 需要 -
可序列化 需要 需要 需要

樂觀鎖與多版本並發控制

在鎖定控制的數據庫系統中,死鎖(dead lock)指的是兩個以上的事務互相依賴等待,從而都被阻塞的現象。與多線程程序的設計不同,鎖定控制的數據庫中死鎖出現是很正常現象,而且是無法根本上避免的。當死鎖出現並且被數據庫系統檢測到時,所有死鎖的事務都會被駁回,用戶不得不根據需要進行重提交。然而死鎖不僅檢測代價是很高昂的,而且還會浪費大量資源,如果死鎖頻繁出現,會大大降低數據庫系統的並發性能。但我們不能爲了避免死鎖而降低隔離等級,而且有一點可以肯定的是,事務隔離級別越高,死鎖出現的概率就越大。

爲了提高性能,樂觀鎖(optimistic locking)機制被提出。與傳統的悲觀鎖(optimistic locking)「先取鎖再訪問」的保守策略不同,樂觀鎖相信事物之間的數據競爭(data race)的概率是比較小的,因此盡可能直接做下去,直到提交的時候纔去鎖定。但如果直接簡單這麼做,還是有可能會遇到不可預期的結果,例如兩個事務都讀取了數據庫的某一行,經過修改以後寫回數據庫,這時就遇到了問題。

一種可靠的樂觀鎖的實現是使用「多版本控制(multi-version control)」,即在每一行加一個version屬性。修改這一行時將version增加1,寫回數據庫要檢查當前的version值是否還是獲取時的那個值了。如果還是,說明期間沒有其他事務對其修改,直接提交即可,如果已經不是了,說明期間已經有別的事務修改了這一行,當前事務獲取的數據已經過期了,事務失敗。

在PostgreSQL及MySQL的InnoDB引擎的實現中,多版本控制的樂觀鎖是內置的(build-in),所以這個無需手動添加version字段。默認情況下,PostgreSQL/MySQL會盡可能使用樂觀鎖,除非遇到顯式的鎖定命令,如"select * from sometable for update"這樣的語句,纔會主動使用悲觀鎖。<o

參考資料

http://en.wikipedia.org/wiki/Isolation_(database_systems) http://www.blogjava.net/loocky/archive/2006/11/15/81138.html

相關日誌