1. 程式人生 > >髒讀、不可重複讀 共享鎖、悲觀鎖 和 事務五種隔離級別

髒讀、不可重複讀 共享鎖、悲觀鎖 和 事務五種隔離級別

一、髒讀、不可重複讀、幻讀

1、髒讀:髒讀就是指當一個事務正在訪問資料,並且對資料進行了修改,而這種修改還沒有提交到資料庫中,這時,另外一個事務也訪問這個資料,然後使用了這個資料。
例如:
  張三的工資為5000,事務A中把他的工資改為8000,但事務A尚未提交。
  與此同時,
  事務B正在讀取張三的工資,讀取到張三的工資為8000。
  隨後,
  事務A發生異常,而回滾了事務。張三的工資又回滾為5000。
  最後,
  事務B讀取到的張三工資為8000的資料即為髒資料,事務B做了一次髒讀。

2、不可重複讀:是指在一個事務內,多次讀同一資料。在這個事務還沒有結束時,另外一個事務也訪問該同一資料。那麼,在第一個事務中的兩次讀資料之間,由於第二個事務的修改,那麼第一個事務兩次讀到的的資料可能是不一樣的。這樣就發生了在一個事務內兩次讀到的資料是不一樣的,因此稱為是不可重複讀。
例如:
  在事務A中,讀取到張三的工資為5000,操作沒有完成,事務還沒提交。
  與此同時,
  事務B把張三的工資改為8000,並提交了事務。
  隨後,
  在事務A中,再次讀取張三的工資,此時工資變為8000。在一個事務中前後兩次讀取的結果並不致,導致了不可重複讀。

3、幻讀:是指當事務不是獨立執行時發生的一種現象,例如第一個事務對一個表中的資料進行了修改,這種修改涉及到表中的全部資料行。同時,第二個事務也修改這個表中的資料,這種修改是向表中插入一行新資料。那麼,以後就會發生操作第一個事務的使用者發現表中還有沒有修改的資料行,就好象發生了幻覺一樣。
例如:
  目前工資為5000的員工有10人,事務A讀取所有工資為5000的人數為10人。
  此時,
  事務B插入一條工資也為5000的記錄。
  這是,事務A再次讀取工資為5000的員工,記錄為11人。此時產生了幻讀。

4、提醒
不可重複讀的重點是修改:
同樣的條件,你讀取過的資料,再次讀取出來發現值不一樣了
幻讀的重點在於新增或者刪除:


同樣的條件,第 1 次和第 2 次讀出來的記錄數不一樣

二、獨佔鎖、共享鎖、更新鎖,樂觀鎖、悲觀鎖

1、鎖的兩種分類方式

(1)從資料庫系統的角度來看,鎖分為以下三種類型:

 獨佔鎖(Exclusive Lock)
      獨佔鎖鎖定的資源只允許進行鎖定操作的程式使用,其它任何對它的操作均不會被接受。執行資料更新命令,即INSERT、 UPDATE 或DELETE 命令時,SQL Server 會自動使用獨佔鎖。但當物件上有其它鎖存在時,無法對其加獨佔鎖。獨佔鎖一直到事務結束才能被釋放。
 共享鎖(Shared Lock)
      共享鎖鎖定的資源可以被其它使用者讀取,但其它使用者不能修改它。在SELECT 命令執行時,SQL Server 通常會對物件進行共享鎖鎖定。通常加共享鎖的資料頁被讀取完畢後,共享鎖就會立即被釋放。
 更新鎖

(Update Lock)
      更新鎖是為了防止死鎖而設立的。當SQL Server 準備更新資料時,它首先對資料物件作更新鎖鎖定,這樣資料將不能被修改,但可以讀取。等到SQL Server 確定要進行更新資料操作時,它會自動將更新鎖換為獨佔鎖。但當物件上有其它鎖存在時,無法對其作更新鎖鎖定。

(2)從程式設計師的角度看,鎖分為以下兩種型別:

悲觀鎖(Pessimistic Lock)
      悲觀鎖,正如其名,它指的是對資料被外界(包括本系統當前的其他事務,以及來自外部系統的事務處理)修改持保守態度,因此在整個資料處理過程中,將資料處於鎖定狀態。悲觀鎖的實現,往往依靠資料庫提供的鎖機制(也只有資料庫層提供的鎖機制才能真正保證資料訪問的排他性,否則,即使在本系統中實現了加鎖機制,也無法保證外部系統不會修改資料)。

樂觀鎖(Optimistic Lock)
      相對悲觀鎖而言,樂觀鎖機制採取了更加寬鬆的加鎖機制。悲觀鎖大多數情況下依靠資料庫的鎖機制實現,以保證操作最大程度的獨佔性。但隨之而來的就是資料庫效能的大量開銷,特別是對長事務而言,這樣的開銷往往無法承受。
      而樂觀鎖機制在一定程度上解決了這個問題。樂觀鎖,大多是基於資料版本( Version )記錄機制實現。何謂資料版本?即為資料增加一個版本標識,在基於資料庫表的版本解決方案中,一般是通過為資料庫表增加一個 “version” 欄位來實現。讀取出資料時,將此版本號一同讀出,之後更新時,對此版本號加一。此時,將提交資料的版本資料與資料庫表對應記錄的當前版本資訊進行比對,如果提交的資料版本號大於資料庫表當前版本號,則予以更新,否則認為是過期資料。

2、資料庫中如何使用鎖

首先從悲觀鎖開始說。在SqlServer等其餘很多資料庫中,資料的鎖定通常採用頁級鎖的方式,也就是說對一張表內的資料是一種序列化的更新插入機制,在任何時間同一張表只會插1條資料,別的想插入的資料要等到這一條資料插完以後才能依次插入。帶來的後果就是效能的降低,在多使用者併發訪問的時候,當對一張表進行頻繁操作時,會發現響應效率很低,資料庫經常處於一種假死狀態。而Oracle用的是行級鎖,只是對想鎖定的資料才進行鎖定,其餘的資料不相干,所以在對Oracle表中併發插資料的時候,基本上不會有任何影響。

注:對於悲觀鎖是針對併發的可能性比較大,而一般在我們的應用中用樂觀鎖足以。

Oracle的悲觀鎖需要利用一條現有的連線,分成兩種方式,從SQL語句的區別來看,就是一種是for update,一種是for update nowait的形式。

比如我們看一個例子。首先建立測試用的資料庫表:

CREATE TABLE TEST(ID,NAME,LOCATION,VALUE,CONSTRAINT test_pk PRIMARY KEY(ID))AS SELECT deptno, dname, loc, 1 FROM scott.dept

這裡我們利用了Oracle的Sample的scott使用者的表,把資料copy到我們的test表中。

(1)for update 形式介紹

然後我們看一下for update鎖定方式。我們執行如下的select for update語句:

select * from test where id = 10 for update

通過這條檢索語句鎖定以後,再開另外一個sql*plus視窗進行操作,再把上面這條sql語句執行一便,你會發現sqlplus好像死在那裡了,好像檢索不到資料的樣子,但是也不返回任何結果,就屬於卡在那裡的感覺。這個時候是什麼原因呢,就是一開始的第一個Session中的select for update語句把資料鎖定住了。由於這裡鎖定的機制是wait的狀態(只要不表示nowait那就是wait),所以第二個Session(也就是卡住的那個sql*plus)中當前這個檢索就處於等待狀態。當第一個session最後commit或者rollback之後,第二個session中的檢索結果就是自動跳出來,並且也把資料鎖定住。

不過如果你第二個session中你的檢索語句如下所示:select * from test where id = 10,也就是沒有for update這種鎖定資料的語句的話,就不會造成阻塞了。

(2)for update nowait 形式介紹

另外一種情況,就是當資料庫資料被鎖定的時候,也就是執行剛才for update那條sql以後,我們在另外一個session中執行for update nowait後又是什麼樣呢。
比如如下的sql語句:

select * from test where id = 10 for update nowait

由於這條語句中是制定採用nowait方式來進行檢索,所以當發現數據被別的session鎖定中的時候,就會迅速返回ORA-00054錯誤,內容是資源正忙, 但指定以 NOWAIT 方式獲取資源。所以在程式中我們可以採用nowait方式迅速判斷當前資料是否被鎖定中,如果鎖定中的話,就要採取相應的業務措施進行處理。

那這裡另外一個問題,就是當我們鎖定住資料的時候,我們對資料進行更新和刪除的話會是什麼樣呢。

比如同樣,我們讓第一個Session鎖定住id=10的那條資料,我們在第二個session中執行如下語句:

update test set value=2 where id = 10

這個時候我們發現update語句就好像select for update語句一樣也停住卡在這裡,當你第一個session放開鎖定以後update才能正常執行。當你update執行後,資料又被你update 語句鎖定住了,這個時候只要你update後還沒有commit,別的session照樣不能對資料進行鎖定更新等等。

總之,Oracle中的悲觀鎖就是利用Oracle的Connection對資料進行鎖定。在Oracle中,用這種行級鎖帶來的效能損失是很小的,只是要注意程式邏輯,不要給你一不小心搞成死鎖了就好。而且由於資料的及時鎖定,在資料提交時候就不撥出現衝突,可以省去很多惱人的資料衝突處理。缺點就是你必須要始終有一條資料庫連線,就是說在整個鎖定到最後放開鎖的過程中,你的資料庫聯接要始終保持住。

與悲觀鎖相對的,我們有了樂觀鎖。樂觀鎖一開始也說了,就是一開始假設不會造成資料衝突,在最後提交的時候再進行資料衝突檢測。

在樂觀鎖中,我們有3種常用的做法來實現:

a. 在資料取得的時候把整個資料都copy到應用中,在進行提交的時候比對當前資料庫中的資料和開始的時候更新前取得的資料。
    當發現兩個資料一模一樣以後,就表示沒有衝突可以提交,否則則是併發衝突,需要去用業務邏輯進行解決。

b. 樂觀鎖的做法就是採用版本戳,這個在Hibernate中得到了使用。
    採用版本戳的話,首先需要在你有樂觀鎖的資料庫table上建立一個新的column,比如為number型,當你資料每更新一次的時候,版本數就會往上增加1。
    比如同樣有2個session同樣對某條資料進行操作。兩者都取到當前的資料的版本號為1,當第一個session進行資料更新後,在提交的時候檢視到當前資料的版本還為1,和自己一開始取到的版本相同。就正式提交,然後把版本號增加1,這個時候當前資料的版本為2。當第二個session也更新了資料提交的時候,發現數據庫中版本為2,和一開始這個session取到的版本號不一致,就知道別人更新過此條資料,這個時候再進行業務處理,比如整個Transaction都Rollback等等操作。
    在用版本戳的時候,可以在應用程式側使用版本戳的驗證,也可以在資料庫側採用Trigger(觸發器)來進行驗證。不過資料庫的Trigger的效能開銷還是比較的大,所以能在應用側進行驗證的話還是推薦不用Trigger。

c. 第三種做法和第二種做法有點類似,就是也新增一個Table的Column,不過這次這個column是採用timestamp型,儲存資料最後更新的時間。
    在Oracle9i以後可以採用新的資料型別,也就是timestamp with time zone型別來做時間戳。這種Timestamp的資料精度在Oracle的時間型別中是最高的,精確到微秒(還沒與到納秒的級別),一般來說,加上資料庫處理時間和人的思考動作時間,微秒級別是非常非常夠了,其實只要精確到毫秒甚至秒都應該沒有什麼問題。
    和剛才的版本戳類似,也是在更新提交的時候檢查當前資料庫中資料的時間戳和自己更新前取到的時間戳進行對比,如果一致則OK,否則就是版本衝突。如果不想把程式碼寫在程式中或者由於別的原因無法把程式碼寫在現有的程式中,也可以把這個時間戳樂觀鎖邏輯寫在Trigger或者儲存過程中。

三、事務五種隔離級別

Isolation 屬性一共支援五種事務設定,具體介紹如下:
(1)DEFAULT
  使用資料庫設定的隔離級別(預設),由DBA 預設的設定來決定隔離級別。
(2)READ_UNCOMMITTED
  這是事務最低的隔離級別,它充許別外一個事務可以看到這個事務未提交的資料。
  會出現髒讀、不可重複讀、幻讀 (隔離級別最低,併發效能高)。
(3)READ_COMMITTED
  保證一個事務修改的資料提交後才能被另外一個事務讀取。另外一個事務不能讀取該事務未提交的資料。
  可以避免髒讀,但會出現不可重複讀、幻讀問題(鎖定正在讀取的行)。
(4)REPEATABLE_READ
  可以防止髒讀、不可重複讀,但會出幻讀(鎖定所讀取的所有行)。
(5)SERIALIZABLE
  這是花費最高代價但是最可靠的事務隔離級別,事務被處理為順序執行。
  保證所有的情況不會發生(鎖表)。

四、c# 事務原理

企業級的資料庫每一秒鐘都可能應付成千上萬的併發訪問,因而帶來了併發控制的問題。由資料庫理論可知,由於併發訪問,在不可預料的時刻可能引發如下幾個可以預料的問題:
  髒讀:包含未提交資料的讀取。例如,事務1 更改了某行。事務2 在事務1 提交更改之前讀取已更改的行。如果事務1 回滾更改,則事務2 便讀取了邏輯上從未存在過的行。
  不可重複讀取:當某個事務不止一次讀取同一行,並且一個單獨的事務在兩次(或多次)讀取之間修改該行時,因為在同一個事務內的多次讀取之間修改了該行,所以每次讀取都生成不同值,從而引發不一致問題。
  幻象:通過一個任務,在以前由另一個尚未提交其事務的任務讀取的行的範圍中插入新行或刪除現有行。帶有未提交事務的任務由於該範圍中行數的更改而無法重複其原始讀取。

如你所想,這些情況發生的根本原因都是因為在併發訪問的時候,沒有一個機制避免交叉存取所造成的。而隔離級別的設定,正是為了避免這些情況的發生。事務準備接受不一致資料的級別稱為隔離級別。隔離級別是一個事務必須與其它事務進行隔離的程度。較低的隔離級別可以增加併發,但代價是降低資料的正確性。相反,較高的隔離級別可以確保資料的正確性,但可能對併發產生負面影響。

根據隔離級別的不同,DBMS為並行訪問提供不同的互斥保證。在SQL Server資料庫中,提供四種隔離級別:未提交讀、提交讀、可重複讀、可序列讀。這四種隔離級別可以不同程度地保證併發的資料完整性: 

隔離級別 髒 讀 不可重複讀取 幻 像
未提交讀
提交讀
可重複讀
可序列讀

可以看出,“可序列讀”提供了最高級別的隔離,這時併發事務的執行結果將與序列執行的完全一致。如前所述,最高級別的隔離也就意味著最低程度的併發,因此,在此隔離級別下,資料庫的服務效率事實上是比較低的。儘管可序列性對於事務確保資料庫中的資料在所有時間內的正確性相當重要,然而許多事務並不總是要求完全的隔離。例如,多個作者工作於同一本書的不同章節。新章節可以在任意時候提交到專案中。但是,對於已經編輯過的章節,沒有編輯人員的批准,作者不能對此章節進行任何更改。這樣,儘管有未編輯的新章節,但編輯人員仍可以確保在任意時間該書籍專案的正確性。編輯人員可以檢視以前編輯的章節以及最近提交的章節。這樣,其它的幾種隔離級別也有其存在的意義。

在.net框架中,事務的隔離級別是由列舉System.Data.IsolationLevel所定義的:

[Flags]
[Serializable]
public enum IsolationLevel

其成員及相應的含義如下:

成 員 含 義
Chaos 無法改寫隔離級別更高的事務中的掛起的更改。
ReadCommitted 在正在讀取資料時保持共享鎖,以避免髒讀,但是在事務結束之前可以更改資料,從而導致不可重複的讀取或幻像資料。
ReadUncommitted 可以進行髒讀,意思是說,不釋出共享鎖,也不接受獨佔鎖。
RepeatableRead 在查詢中使用的所有資料上放置鎖,以防止其他使用者更新這些資料。防止不可重複的讀取,但是仍可以有幻像行。
Serializable 在DataSet上放置範圍鎖,以防止在事務完成之前由其他使用者更新行或向資料集中插入行。
Unspecified 正在使用與指定隔離級別不同的隔離級別,但是無法確定該級別。

顯而意見,資料庫的四個隔離級別在這裡都有對映。

預設的情況下,SQL Server使用ReadCommitted(提交讀)隔離級別。

關於隔離級別的最後一點就是如果你在事務執行的過程中改變了隔離級別,那麼後面的命名都在最新的隔離級別下執行——隔離級別的改變是立即生效的。有了這一點,你可以在你的事務中更靈活地使用隔離級別從而達到更高的效率和併發安全性。

轉載:http://cupoy.iteye.com/blog/251796
        http://luijnijei.blog.163.com/blog/static/35024594201061485547777/
        http://hi.baidu.com/eredlab/blog/item/13d84ef4896207cdf3d385fc.html
        http://pengzong155.blog.163.com/blog/static/104028160200912073636329/