引言
之前關於事務的文章已介紹了事務的概念以及事務的四個屬性(ACID),相信你對事務應該有所認識和了解。
本篇文章是關於事務的隔離性,介紹數據庫提供的多種隔離級別。
數據庫訪問的並發性問題
所謂事務的隔離性,其實事務的這個屬性是針對數據庫訪問的並發性問題而言的。
那何謂數據庫訪問的並發性問題呢?
所謂數據庫訪問的並發性問題是指多個事務可以同時訪問數據庫中的數據,而當多個事務在數據庫中並發執行(同時執行)時,數據的一致性可能受到破壞,從而導致數據出現問題。
還是舉上次轉賬那個例子吧!
假設你的賬號上有 1000 元,你轉賬給朋友 100 元,然后又向賬號匯入 100 元,請問你的賬號上余額是多少?是不是太簡單了,小學生都會算,當然還是 1000 元,對吧。 整個流程如下:
- 查看賬號余額為 1000 元
- 轉賬給朋友 100 元,賬號余額為 900 元
- 再查看賬號余額為 900 元
- 匯入100 元到賬號,賬號余額為 1000 元
現在,假設你向朋友的轉賬和匯款是同時(並發)進行的,整個流程可能如下:
- 查看賬號余額為 1000 元(轉賬查看)
- 查看賬號余額為 1000 元(匯款查看)
- 轉賬給朋友 100 元,賬號余額為 900 元
- 匯入100 元到賬號,賬號余額為 1100 元
那么,結果是現在賬號余額居然是 1100 元。顯然,正確的結果應該是 1000 元,呵呵,這就是數據庫訪問的並發性問題。
其實,數據庫訪問的並發性問題有很多種情況,以上這種情況只是其中的一種叫更新丟失。
更新丟失
如果多個事務同時(並發)對數據庫表中的同一條記錄進行修改,那么后修改的記錄將會覆蓋前面修改的記錄,前面的修改就丟失掉了,這就叫做更新丟失。如下圖:
那遇到這種情況怎么解決呢,其實更新丟失和多線程同步很相似,所以解決方法也是一樣的,那就是對行加鎖,同時只允許一個事務訪問數據庫。什么意思呀?很簡單,就是加鎖以后,兩個事務即使同時訪問數據庫,也只允許加鎖的事務先訪問,另一個未加鎖的在外等待,直到釋放鎖后才能訪問數據庫。這樣,其實就是將並行訪問數據庫變成了串行訪問數據庫,是不是和多線程同步加同步鎖一個道理呀。
臟讀
所謂臟讀就是一個事務 A 讀取另一個事務 B 修改但尚未提交的數據並在此基礎上操作,而事務 B 又執行事務回滾(也就是撤銷了事務),那么事務 A 讀取到的數據就是臟數據,如下圖:
想一想怎么解決這個問題呢?解決辦法很簡單,就是在第一個事務提交前,任何其他事務不可讀取其修改過的值,則可以避免該問題。
不可重復讀
所謂不可重復讀就是一個事務對同一行數據重復讀取兩次,但是卻得到了不同的結果。事務T1讀取某一數據后,事務T2對其做了修改,當事務T1再次讀該數據時得到與前一次不同的值,如下圖:
解決辦法也很簡單,如果只有在修改事務完全提交之后才可以讀取數據,則可以避免該問題。
幻象讀
所謂幻象讀指兩次執行同一條 select 語句會出現不同的結果。這個很好理解,當第一次執行 select 語句后,接着另一個事務執行了 insert 語句(也就是插入了一條記錄),這時第二次執行相同的 select 語句,返回的結果自然與第一次不同,這就是幻象讀。再舉個例子吧,目前工資為 10000 元 的員工有 10 人。那么事務 A 中讀取所有工資為 10000 元的員工,得到了 10 條記錄;這時事務 B 向員工表插入了一條員工記錄,工資也為 10000 元;那么事務 A 再次讀取所有工資為 10000 元的員工共讀取到了 11 條記錄,如下圖:
解決辦法就是如果在操作事務完成數據處理之前,任何其他事務都不可以添加新數據,則可避免該問題。
事務的隔離級別
為了解決以上各種數據庫訪問的並發性問題(更新丟失、臟讀、不可重復讀、幻象讀),為此數據庫提供了4種隔離級別。
Read uncommitted(未授權讀取、讀未提交)
如果一個事務已經開始寫數據,則另外一個事務則不允許同時進行寫操作,但允許其他事務讀此行數據。該隔離級別可以通過“排他寫鎖”實現。這樣就避免了更新丟失,卻可能出現臟讀。也就是說事務B讀取到了事務A未提交的數據。
Read committed(授權讀取、讀提交)
讀取數據的事務允許其他事務繼續訪問該行數據,但是未提交的寫事務將會禁止其他事務訪問該行。該隔離級別避免了臟讀,但是卻可能出現不可重復讀。事務A事先讀取了數據,事務B緊接了更新了數據,並提交了事務,而事務A再次讀取該數據時,數據已經發生了改變。
Repeatable read(可重復讀取)
可重復讀是指在一個事務內,多次讀同一數據。在這個事務還沒有結束時,另外一個事務也訪問該同一數據。那么,在第一個事務中的兩次讀數據之間,即使第二個事務對數據進行修改,第一個事務兩次讀到的的數據是一樣的。這樣就發生了在一個事務內兩次讀到的數據是一樣的,因此稱為是可重復讀。讀取數據的事務將會禁止寫事務(但允許讀事務),寫事務則禁止任何其他事務。這樣避免了不可重復讀取和臟讀,但是有時可能出現幻象讀。(讀取數據的事務)這可以通過“共享讀鎖”和“排他寫鎖”實現。
Serializable(序列化)
提供嚴格的事務隔離。它要求事務序列化執行,事務只能一個接着一個地執行,但不能並發執行。如果僅僅通過“行級鎖”是無法實現事務序列化的,必須通過其他機制保證新插入的數據不會被剛執行查詢操作的事務訪問到。序列化是最高的事務隔離級別,同時代價也花費最高,性能很低,一般很少使用,在該級別下,事務順序執行,不僅可以避免臟讀、不可重復讀,還避免了幻像讀。
隔離級別越高,越能保證數據的完整性和一致性,但是對並發性能的影響也越大。
對於多數應用程序,可以優先考慮把數據庫系統的隔離級別設為 Read Committed(授權讀取、讀提交)。它能夠避免臟讀取,而且具有較好的並發性能。盡管它會導致不可重復讀、幻讀和丟失更新這些並發性問題,在可能出現這類問題的個別場合,可以由應用程序采用悲觀鎖或樂觀鎖來控制。
大多數數據庫的默認級別就是 Read committed(授權讀取、讀提交),比如Sql Server , Oracle。MySQL的默認隔離級別就是 Repeatable read。
悲觀鎖和樂觀鎖
雖然數據庫的隔離級別可以解決大多數問題,但是靈活度較差,為此又提出了悲觀鎖和樂觀鎖的概念。
悲觀鎖
悲觀鎖就是某事務在更新數據過程中將數據鎖定,其他任何事務都不能讀取或修改,必須修改完成后才能訪問數據(類似於Java的線程同步機制)。悲觀鎖的特點是具有排他性,通常依賴於數據庫的鎖機制,一般適合短事務處理。
可能你會想,說了半天也沒說為何叫悲觀鎖呀,到底悲觀在哪里呀?這個問題問得很好。根據悲觀鎖的定義可知,當一個事務加了悲觀鎖,其他任何事務是不能讀取或修改數據,也就是只能在外面等待,什么事也干不了,直到悲觀鎖被釋放為止。那么,想象一下,如果有很多事務都要訪問數據庫(高並發的情況),加了悲觀鎖就意味所有事務需要排着長長的隊,一個一個訪問數據庫,那么訪問數據庫的效率是不是非常低呀,你說悲觀不悲觀呀。
樂觀鎖
樂觀鎖相對悲觀鎖而言,樂觀鎖假設認為數據一般情況下不會造成沖突,所以只會在數據進行提交更新的時候,才會正式對數據的沖突與否進行檢測,如果發現沖突了,則返回用戶錯誤的信息,讓用戶決定如何去做。樂觀鎖的特點是並發性較好,事務修改數據時,其他事務仍可以修改數據。
實現樂觀鎖一般來說有以下2種方式:
使用版本號
使用數據版本(Version)記錄機制實現,這是樂觀鎖最常用的一種實現方式。何謂數據版本?即為數據增加一個版本標識,一般是通過為數據庫表增加一個數字類型的 “version” 字段來實現。當讀取數據時,將version字段的值一同讀出,數據每更新一次,對此version值加一。當我們提交更新的時候,判斷數據庫表對應記錄的當前版本信息與第一次取出來的version值進行比對,如果數據庫表當前版本號與第一次取出來的version值相等,則予以更新,否則認為是過期數據。
使用時間戳
樂觀鎖定的第二種實現方式和第一種差不多,同樣是在需要樂觀鎖控制的table中增加一個字段,名稱無所謂,字段類型使用時間戳(timestamp), 和上面的version類似,也是在更新提交的時候檢查當前數據庫中數據的時間戳和自己更新前取到的時間戳進行對比,如果一致則OK,否則就是版本沖突。
說白了,樂觀鎖其實根本不是一種數據庫鎖機制,而是一種沖突檢測機制,這種沖突檢測機制是依賴軟件或應用程序實現的。
那樂觀鎖為何樂觀呀,樂觀在它的並發性比悲觀鎖好,一個事務在修改數據時,其他事務仍然可以修改數據。
悲觀鎖與樂觀鎖的優缺點及使用場景
悲觀鎖的優點是可以保障數據庫的數據是絕對安全的,它是依賴數據庫的鎖機制,能很好的解決數據庫訪問的並發性問題,但是缺點就是會導致數據庫訪問性能低下,所以適合短事務(也就是事務執行時間很短)的情況。你想一想,如果事務執行時間很長,那么后面的事務就得一直排隊等待嘛。它的使用場景是對數據安全性要求非常高的場景,比如銀行系統、金融系統等。
樂觀鎖的優點是可以保障並發性比較好,也就景數據庫訪問性能可以,它是依賴軟件的沖突檢測機制實現的,但是缺點就是並沒徹底解決數據庫訪問的並發性問題,所以數據庫的數據不是絕對安全的。它的使用場景是對數據安全性要求不高而對性能要求很高的場景,比如各種信息管理系統等。
總結
-
數據庫訪問的並發性問題(更新丟失、臟讀、不可重復讀、幻象讀)會導致的數據的一致性被破壞。
-
數據庫指定了4種事務的隔離級別,目的是為了解決數據庫訪問的並發性問題(更新丟失、臟讀、不可重復讀、幻象讀)導致的數據的一致性被破壞。
-
Read uncommitted(未授權讀取、讀未提交)
-
Read committed(授權讀取、讀提交)
-
Repeatable read(可重復讀取) MySQL 默認隔離級別
-
Serializable(序列化)
-
-
由於數據庫的隔離級別靈活度較差,所以又有了悲觀鎖和樂觀鎖,也是用於解決數據庫訪問的並發性問題。
-
悲觀鎖
-
樂觀鎖
-