1. 程式人生 > >資料庫的隔離級別以及悲觀鎖和樂觀鎖詳解

資料庫的隔離級別以及悲觀鎖和樂觀鎖詳解

一、事務四大屬性

分別是原子性、一致性、隔離性、永續性。

1、原子性(Atomicity)

原子性是指事務包含的所有操作要麼全部成功,要麼全部失敗回滾,因此事務的操作如果成功就必須要完全應用到資料庫,如果操作失敗則不能對資料庫有任何影響。

2、一致性(Consistency)

一致性是指事務必須使資料庫從一個一致性狀態變換到另一個一致性狀態,也就是說一個事務執行之前和執行之後都必須處於一致性狀態。舉例來說,假設使用者A和使用者B兩者的錢加起來一共是1000,那麼不管A和B之間如何轉賬、轉幾次賬,事務結束後兩個使用者的錢相加起來應該還得是1000,這就是事務的一致性。

3、隔離性(Isolation)

隔離性是當多個使用者併發訪問資料庫時,比如同時操作同一張表時,資料庫為每一個使用者開啟的事務,不能被其他事務的操作所幹擾,多個併發事務之間要相互隔離。關於事務的隔離性資料庫提供了多種隔離級別,稍後會介紹到。

4、永續性(Durability)

永續性是指一個事務一旦被提交了,那麼對資料庫中的資料的改變就是永久性的,即便是在資料庫系統遇到故障的情況下也不會丟失提交事務的操作。例如我們在使用JDBC操作資料庫時,在提交事務方法後,提示使用者事務操作完成,當我們程式執行完成直到看到提示後,就可以認定事務已經正確提交,即使這時候資料庫出現了問題,也必須要將我們的事務完全執行完成。否則的話就會造成我們雖然看到提示事務處理完畢,但是資料庫因為故障而沒有執行事務的重大錯誤。這是不允許的。

二、事務的隔離級別

1、為什麼要設定隔離級別

在資料庫操作中,在併發的情況下可能出現如下問題:

  • 更新丟失(Lost update)
    如果多個執行緒操作,基於同一個查詢結構對錶中的記錄進行修改,那麼後修改的記錄將會覆蓋前面修改的記錄,前面的修改就丟失掉了,這就叫做更新丟失。這是因為系統沒有執行任何的鎖操作,因此併發事務並沒有被隔離開來。
    第1類丟失更新:事務A撤銷時,把已經提交的事務B的更新資料覆蓋了。
    這裡寫圖片描述
    第2類丟失更新:事務A覆蓋事務B已經提交的資料,造成事務B所做的操作丟失。
    這裡寫圖片描述
    解決方法:對行加鎖,只允許併發一個更新事務。

  • 髒讀(Dirty Reads)
    髒讀(Dirty Read):A事務讀取B事務尚未提交的資料並在此基礎上操作,而B事務執行回滾,那麼A讀取到的資料就是髒資料。
    這裡寫圖片描述


    解決辦法:如果在第一個事務提交前,任何其他事務不可讀取其修改過的值,則可以避免該問題。

  • 不可重複讀(Non-repeatable Reads)
    一個事務對同一行資料重複讀取兩次,但是卻得到了不同的結果。事務T1讀取某一資料後,事務T2對其做了修改,當事務T1再次讀該資料時得到與前一次不同的值。
    這裡寫圖片描述
    解決辦法:如果只有在修改事務完全提交之後才可以讀取資料,則可以避免該問題。

  • 幻象讀
    指兩次執行同一條 select 語句會出現不同的結果,第二次讀會增加一資料行,並沒有說這兩次執行是在同一個事務中。一般情況下,幻象讀應該正是我們所需要的。但有時候卻不是,如果開啟的遊標,在對遊標進行操作時,並不希望新增的記錄加到遊標命中的資料集中來。隔離級別為 遊標穩定性 的,可以阻止幻象讀。例如:目前工資為1000的員工有10人。那麼事務1中讀取所有工資為1000的員工,得到了10條記錄;這時事務2向員工表插入了一條員工記錄,工資也為1000;那麼事務1再次讀取所有工資為1000的員工共讀取到了11條記錄。
    這裡寫圖片描述
    解決辦法:如果在操作事務完成資料處理之前,任何其他事務都不可以新增新資料,則可避免該問題。

正是為了解決以上情況,資料庫提供了幾種隔離級別。

2、事務的隔離級別

資料庫事務的隔離級別有4個,由低到高依次為Read uncommitted(未授權讀取、讀未提交)、Read committed(授權讀取、讀提交)、Repeatable read(可重複讀取)、Serializable(序列化),這四個級別可以逐個解決髒讀、不可重複讀、幻象讀這幾類問題。

  • Read uncommitted(未授權讀取、讀未提交):
    如果一個事務已經開始寫資料,則另外一個事務則不允許同時進行寫操作,但允許其他事務讀此行資料。該隔離級別可以通過“排他寫鎖”實現。這樣就避免了更新丟失,卻可能出現髒讀。也就是說事務B讀取到了事務A未提交的資料。
  • Read committed(授權讀取、讀提交):
    讀取資料的事務允許其他事務繼續訪問該行資料,但是未提交的寫事務將會禁止其他事務訪問該行。該隔離級別避免了髒讀,但是卻可能出現不可重複讀。事務A事先讀取了資料,事務B緊接了更新了資料,並提交了事務,而事務A再次讀取該資料時,資料已經發生了改變。
  • Repeatable read(可重複讀取):
    可重複讀是指在一個事務內,多次讀同一資料。在這個事務還沒有結束時,另外一個事務也訪問該同一資料。那麼,在第一個事務中的兩次讀資料之間,即使第二個事務對資料進行修改,第一個事務兩次讀到的的資料是一樣的。這樣就發生了在一個事務內兩次讀到的資料是一樣的,因此稱為是可重複讀。讀取資料的事務將會禁止寫事務(但允許讀事務),寫事務則禁止任何其他事務。這樣避免了不可重複讀取和髒讀,但是有時可能出現幻象讀。(讀取資料的事務)這可以通過“共享讀鎖”和“排他寫鎖”實現。
  • Serializable(序列化):
    提供嚴格的事務隔離。它要求事務序列化執行,事務只能一個接著一個地執行,但不能併發執行。如果僅僅通過“行級鎖”是無法實現事務序列化的,必須通過其他機制保證新插入的資料不會被剛執行查詢操作的事務訪問到。序列化是最高的事務隔離級別,同時代價也花費最高,效能很低,一般很少使用,在該級別下,事務順序執行,不僅可以避免髒讀、不可重複讀,還避免了幻像讀。
    這裡寫圖片描述
    隔離級別越高,越能保證資料的完整性和一致性,但是對併發效能的影響也越大。對於多數應用程式,可以優先考慮把資料庫系統的隔離級別設為Read Committed。它能夠避免髒讀取,而且具有較好的併發效能。儘管它會導致不可重複讀、幻讀和第二類丟失更新這些併發問題,在可能出現這類問題的個別場合,可以由應用程式採用悲觀鎖或樂觀鎖來控制。大多數資料庫的預設級別就是Read committed,比如Sql Server , Oracle。MySQL的預設隔離級別就是Repeatable read。

三、悲觀鎖和樂觀鎖

雖然資料庫的隔離級別可以解決大多數問題,但是靈活度較差,為此又提出了悲觀鎖和樂觀鎖的概念。

1、悲觀鎖

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

  • 使用場景舉例:以MySQL InnoDB為例

商品t_items表中有一個欄位status,status為1代表商品未被下單,status為2代表商品已經被下單(此時該商品無法再次下單),那麼我們對某個商品下單時必須確保該商品status為1。假設商品的id為1。
如果不採用鎖,那麼操作方法如下:

//1.查詢出商品資訊
select status from  t_items where id=1;
//2.根據商品資訊生成訂單,並插入訂單表 t_orders 
insert into t_orders (id,goods_id) values (null,1);
//3.修改商品status為2
update t_items set status=2;

但是上面這種場景在高併發訪問的情況下很可能會出現問題。例如當第一步操作中,查詢出來的商品status為1。但是當我們執行第三步Update操作的時候,有可能出現其他人先一步對商品下單把t_items中的status修改為2了,但是我們並不知道資料已經被修改了,這樣就可能造成同一個商品被下單2次,使得資料不一致。所以說這種方式是不安全的。

  • 使用悲觀鎖來解決問題

在上面的場景中,商品資訊從查詢出來到修改,中間有一個處理訂單的過程,使用悲觀鎖的原理就是,當我們在查詢出t_items資訊後就把當前的資料鎖定,直到我們修改完畢後再解鎖。那麼在這個過程中,因為t_items被鎖定了,就不會出現有第三者來對其進行修改了。需要注意的是,要使用悲觀鎖,我們必須關閉mysql資料庫的自動提交屬性,因為MySQL預設使用autocommit模式,也就是說,當你執行一個更新操作後,MySQL會立刻將結果進行提交。我們可以使用命令設定MySQL為非autocommit模式:set autocommit=0;
設定完autocommit後,我們就可以執行我們的正常業務了。具體如下:

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

上面的begin/commit為事務的開始和結束,因為在前一步我們關閉了mysql的autocommit,所以需要手動控制事務的提交。
上面的第一步我們執行了一次查詢操作:select status from t_items where id=1 for update;與普通查詢不一樣的是,我們使用了select…for update的方式,這樣就通過資料庫實現了悲觀鎖。此時在t_items表中,id為1的那條資料就被我們鎖定了,其它的事務必須等本次事務提交之後才能執行。這樣我們可以保證當前的資料不會被其它事務修改。需要注意的是,在事務中,只有SELECT ... FOR UPDATELOCK IN SHARE MODE 操作同一個資料時才會等待其它事務結束後才執行,一般SELECT ... 則不受此影響。拿上面的例項來說,當我執行select status from t_items where id=1 for update;後。我在另外的事務中如果再次執行select status from t_items where id=1 for update;則第二個事務會一直等待第一個事務的提交,此時第二個查詢處於阻塞的狀態,但是如果我是在第二個事務中執行select status from t_items where id=1;則能正常查詢出資料,不會受第一個事務的影響。

  • Row Lock與Table Lock

使用select…for update會把資料給鎖住,不過我們需要注意一些鎖的級別,MySQL InnoDB預設Row-Level Lock,所以只有「明確」地指定主鍵或者索引,MySQL 才會執行Row lock (只鎖住被選取的資料) ,否則MySQL 將會執行Table Lock (將整個資料表單給鎖住)。舉例如下:
1、select * from t_items where id=1 for update;
這條語句明確指定主鍵(id=1),並且有此資料(id=1的資料存在),則採用row lock。只鎖定當前這條資料。
2、select * from t_items where id=3 for update;
這條語句明確指定主鍵,但是卻查無此資料,此時不會產生lock(沒有元資料,又去lock誰呢?)。
3、select * from t_items where name='手機' for update;
這條語句沒有指定資料的主鍵,那麼此時產生table lock,即在當前事務提交前整張資料表的所有欄位將無法被查詢。
4、select * from t_items where id>0 for update; 或者select * from t_items where id<>1 for update;(注:<>在SQL中表示不等於)
上述兩條語句的主鍵都不明確,也會產生table lock。
5、select * from t_items where status=1 for update;(假設為status欄位添加了索引)
這條語句明確指定了索引,並且有此資料,則產生row lock。
6、select * from t_items where status=3 for update;(假設為status欄位添加了索引)
這條語句明確指定索引,但是根據索引查無此資料,也就不會產生lock。

  • 悲觀鎖小結
    悲觀鎖並不是適用於任何場景,它也有它存在的一些不足,因為悲觀鎖大多數情況下依靠資料庫的鎖機制實現,以保證操作最大程度的獨佔性。如果加鎖的時間過長,其他使用者長時間無法訪問,影響了程式的併發訪問性,同時這樣對資料庫效能開銷影響也很大,特別是對長事務而言,這樣的開銷往往無法承受。所以與悲觀鎖相對的,我們有了樂觀鎖。

2、樂觀鎖

樂觀鎖( Optimistic Locking ) 相對悲觀鎖而言,樂觀鎖假設認為資料一般情況下不會造成衝突,所以只會在資料進行提交更新的時候,才會正式對資料的衝突與否進行檢測,如果發現衝突了,則返回使用者錯誤的資訊,讓使用者決定如何去做。實現樂觀鎖一般來說有以下2種方式:

  • 使用版本號
    使用資料版本(Version)記錄機制實現,這是樂觀鎖最常用的一種實現方式。何謂資料版本?即為資料增加一個版本標識,一般是通過為資料庫表增加一個數字型別的 “version” 欄位來實現。當讀取資料時,將version欄位的值一同讀出,資料每更新一次,對此version值加一。當我們提交更新的時候,判斷資料庫表對應記錄的當前版本資訊與第一次取出來的version值進行比對,如果資料庫表當前版本號與第一次取出來的version值相等,則予以更新,否則認為是過期資料。
  • 使用時間戳
    樂觀鎖定的第二種實現方式和第一種差不多,同樣是在需要樂觀鎖控制的table中增加一個欄位,名稱無所謂,欄位型別使用時間戳(timestamp), 和上面的version類似,也是在更新提交的時候檢查當前資料庫中資料的時間戳和自己更新前取到的時間戳進行對比,如果一致則OK,否則就是版本衝突。