天天看點

SQL-樂觀鎖,悲觀鎖之于并發

資料庫并發處理機制:樂觀鎖,悲觀鎖。C#并發處理,SQL鎖機制,并發,如何處理高并發?

   每次寫部落格,第一句話都是這樣的:程式員很苦逼,除了會寫程式,還得會寫部落格!當然,希望将來的一天,某位老闆看到此部落格,給你的程式員職工加點薪資吧!因為程式員的世界除了苦逼就是沉默。我眼中的程式員大多都不愛說話,默默承受着程式設計的巨大壓力,除了技術上的交流外,他們不願意也不擅長和别人交流,更不樂意任何人走進他們的内心!

   最近悟出來一個道理,在這兒分享給大家:學曆代表你的過去,能力代表你的現在,學習代表你的将來。我們都知道計算機技術發展日新月異,速度驚人的快,你我稍不留神,就會被慢慢淘汰!是以:每日不間斷的學習是避免被淘汰的不二法寶。

   當然,題外話說多了,咱進入正題!

引言

為什麼需要鎖(并發控制)?

  在多使用者環境中,在同一時間可能會有多個使用者更新相同的記錄,這會産生沖突。這就是著名的并發性問題。

典型的沖突有:

丢失更新:一個事務的更新覆寫了其它事務的更新結果,就是所謂的更新丢失。例如:使用者A把值從6改為2,使用者B把值從2改為6,則使用者A丢失了他的更新。

髒讀:當一個事務讀取其它完成一半事務的記錄時,就會發生髒讀取。例如:使用者A,B看到的值都是6,使用者B把值改為2,使用者A讀到的值仍為6。

為了解決這些并發帶來的問題。 我們需要引入并發控制機制。

并發控制機制

  悲觀鎖:假定會發生并發沖突,屏蔽一切可能違反資料完整性的操作。[1]

  樂觀鎖:假設不會發生并發沖突,隻在送出操作時檢查是否違反資料完整性。[1] 樂觀鎖不能解決髒讀的問題。

      最常用的處理多使用者并發通路的方法是加鎖。當一個使用者鎖住資料庫中的某個對象時,其他使用者就不能再通路該對象。加鎖對并發通路的影響展現在鎖的粒度上。比如,放在一個表上的鎖限制對整個表的并發通路;放在資料頁上的鎖限制了對整個資料頁的通路;放在行上的鎖隻限制對該行的并發通路。可見行鎖粒度最小,并發通路最好,頁鎖粒度最大,并發通路性能就會越低。

悲觀鎖:假定會發生并發沖突,屏蔽一切可能違反資料完整性的操作。[1] 悲觀鎖假定其他使用者企圖通路或者改變你正在通路、更改的對象的機率是很高的,是以在悲觀鎖的環境中,在你開始改變此對象之前就将該對象鎖住,并且直到你送出了所作的更改之後才釋放鎖。悲觀的缺陷是不論是頁鎖還是行鎖,加鎖的時間可能會很長,這樣可能會長時間的鎖定一個對象,限制其他使用者的通路,也就是說悲觀鎖的并發通路性不好。

樂觀鎖:假設不會發生并發沖突,隻在送出操作時檢查是否違反資料完整性。[1] 樂觀鎖不能解決髒讀的問題。 樂觀鎖則認為其他使用者企圖改變你正在更改的對象的機率是很小的,是以樂觀鎖直到你準備送出所作的更改時才将對象鎖住,當你讀取以及改變該對象時并不加鎖。可見樂觀鎖加鎖的時間要比悲觀鎖短,樂觀鎖可以用較大的鎖粒度獲得較好的并發通路性能。但是如果第二個使用者恰好在第一個使用者送出更改之前讀取了該對象,那麼當他完成了自己的更改進行送出時,資料庫就會發現該對象已經變化了,這樣,第二個使用者不得不重新讀取該對象并作出更改。這說明在樂觀鎖環境中,會增加并發使用者讀取對象的次數。

      從資料庫廠商的角度看,使用樂觀的頁鎖是比較好的,尤其在影響很多行的批量操作中可以放比較少的鎖,進而降低對資源的需求提高資料庫的性能。再考慮聚集索引。在資料庫中記錄是按照聚集索引的實體順序存放的。如果使用頁鎖,當兩個使用者同時通路更改位于同一資料頁上的相鄰兩行時,其中一個使用者必須等待另一個使用者釋放鎖,這會明顯地降低系統的性能。interbase和大多數關系資料庫一樣,采用的是樂觀鎖,而且讀鎖是共享的,寫鎖是排他的。可以在一個讀鎖上再放置讀鎖,但不能再放置寫鎖;你不能在寫鎖上再放置任何鎖。鎖是目前解決多使用者并發通路的有效手段。

    綜上所述:在實際生産環境裡邊,如果并發量不大且不允許髒讀,可以使用悲觀鎖解決并發問題;但如果系統的并發非常大的話,悲觀鎖定會帶來非常大的性能問題,是以我們就要選擇樂觀鎖定的方法.

需要使用資料庫的鎖機制,比如SQL SERVER 的TABLOCKX(排它表鎖) 此選項被選中時,SQL  Server  将在整個表上置排它鎖直至該指令或事務結束。這将防止其他程序讀取或修改表中的資料。

SqlServer中使用

Begin Tran

select top 1 @TrainNo=T_NO

         from Train_ticket   with (UPDLOCK)   where S_Flag=0

      update Train_ticket

         set T_Name=user,

             T_Time=getdate(),

             S_Flag=1

         where T_NO=@TrainNo

commit

我們在查詢的時候使用了with (UPDLOCK)選項,在查詢記錄的時候我們就對記錄加上了更新鎖,表示我們即将對此記錄進行更新. 注意更新鎖和共享鎖是不沖突的,也就是其他使用者還可以查詢此表的内容,但是和更新鎖和排它鎖是沖突的.是以其他的更新使用者就會阻塞.

在此:舉個簡單的例子來說明悲觀鎖的應用,我們以SQLServer為例進行說明:

假如兩個線程同時修改資料庫同一條記錄,就會導緻後一條記錄覆寫前一條,進而引發一些問題。

例如:

  一個售票系統有一個餘票數,用戶端每調用一次出票方法,餘票數就減一。

情景: 

  總共300張票,假設兩個售票點,恰好在同一時間出票,它們做的操作都是先查詢餘票數,然後減一。

一般的sql語句:

SQL-樂觀鎖,悲觀鎖之于并發

    問題就在于,同一時間擷取的餘票都為300,每個售票點都做了一次更新為299的操作,導緻餘票少了1,而實際出了兩張票。

  打開兩個查詢視窗,分别快速運作以上代碼即可看到效果。

      是以:在此我們可以采用悲觀鎖進行解決

SQL-樂觀鎖,悲觀鎖之于并發

  在查詢的時候加了一個更新鎖,保證自查詢起直到事務結束不會被其他事務讀取修改,避免産生髒資料。

  進而可以解決上述問題。

如果這個售票系統并發太高(例如:春節售票,十月一國慶售票等買票人員并發操作很高),我們采用上述的悲觀鎖方案就會是系統性能大大降低。譬如:售票員A鎖定了這個操作,售票員A在處理這個業務的過程中,又去喝了杯咖啡,在此期間,其他業務員是無法進行訂票的,是以...

樂觀鎖解決方案:

SQL-樂觀鎖,悲觀鎖之于并發

 這便是樂觀鎖的解決方案,可以解決并發帶來的資料錯誤問題,但不保證每一次調用更新都成功,可能會傳回'更新失敗'

悲觀鎖和樂觀鎖

  悲觀鎖一定成功,但在并發量特别大的時候會造成很長堵塞甚至逾時,僅适合小并發的情況。

  樂觀鎖不一定每次都修改成功,但能充分利用系統的并發處理機制,在大并發量的時候效率要高很多。

下面以SQLSERVER為例,詳情說明悲觀鎖和樂觀鎖(請務必看懂關于樂觀鎖的實作,因為:在實際的系統中,樂觀鎖應用較為廣泛)

鎖( locking ) 

業務邏輯的實作過程中,往往需要保證資料通路的排他性。如在金融系統的日終結算進行中,我們希望針對某個 cut-off 時間點的資料進行處理,而不希望在結算進行過程中(可能是幾秒種,也可能是幾個小時),資料再發生變化。此時,我們就需要通過一些機制來保證這些資料在某個操作過程中不會被外界修改,這樣的機制,在這裡,也就是所謂的 “ 鎖 ” ,即給我們標明的目标資料上鎖,使其無法被其他程式修改。 

Hibernate 支援兩種鎖機制:即通常所說的 “ 悲觀鎖( Pessimistic Locking ) ”和 “ 樂觀鎖( Optimistic Locking ) ” 。

悲觀鎖( Pessimistic Locking ) 

悲觀鎖,正如其名,它指的是對資料被外界(包括本系統目前的其他事務,以及來自外部系統的事務處理)修改持保守态度,是以,在整個資料處理過程中,将資料處于鎖定狀态。悲觀鎖的實作,往往依靠資料庫提供的鎖機制(也隻有資料庫層提供的鎖機制才能真正保證資料通路的排他性,否則,即使在本系統中實作了加鎖機制,也無法保證外部系統不會修改資料)。 

一個典型的倚賴資料庫的悲觀鎖調用: 

select * from account where name=”Erica” for update

這條 sql 語句鎖定了 account 表中所有符合檢索條件( name=”Erica” )的記錄。 

本次事務送出之前(事務送出時會釋放事務過程中的鎖),外界無法修改這些記錄。 

Hibernate 的悲觀鎖,也是基于資料庫的鎖機制實作。 

Hibernate 的加鎖模式有: 

Ø LockMode.NONE : 無鎖機制。 

Ø LockMode.WRITE : Hibernate 在 Insert 和 Update 記錄的時候會自動擷取。

Ø LockMode.READ : Hibernate 在讀取記錄的時候會自動擷取。 

樂觀鎖( Optimistic Locking ) 

相對悲觀鎖而言,樂觀鎖機制采取了更加寬松的加鎖機制。悲觀鎖大多數情況下依 靠資料庫的鎖機制實作,以保證操作最大程度的獨占性。但随之而來的就是資料庫性能的大量開銷,特别是對長事務而言,這樣的開銷往往無法承受。 

如一個金融系統,當某個操作員讀取使用者的資料,并在讀出的使用者資料的基礎上進 行修改時(如更改使用者帳戶餘額),如果采用悲觀鎖機制,也就意味着整個操作過 程中(從操作員讀出資料、開始修改直至送出修改結果的全過程,甚至還包括操作員中途去煮咖啡的時間),資料庫記錄始終處于加鎖狀态,可以想見,如果面對幾百上千個并發,這樣的情況将導緻怎樣的後果。 

樂觀鎖機制在一定程度上解決了這個問題。樂觀鎖,大多是基于資料版本 Version )記錄機制實作。何謂資料版本?即為資料增加一個版本辨別,在基于(資料庫表的版本解決方案中,一般是通過為資料庫表增加一個 “version” 字段來實作。讀取出資料時,将此版本号一同讀出,之後更新時,對此版本号加一。此時,将送出資料的版本資料與資料庫表對應記錄的目前版本資訊進行比對,如果送出的資料版本号大于資料庫表目前版本号,則予以更新,否則認為是過期資料。

對于上面修改使用者帳戶資訊的例子而言,假設資料庫中帳戶資訊表中有一個version 字段,目前值為 1 ;而目前帳戶餘額字段( balance )為 $100 。 

1 操作員 A 此時将其讀出( version=1 ),并從其帳戶餘額中扣除 $50( $100-$50 )。

2 在操作員 A 操作的過程中,操作員 B 也讀入此使用者資訊( version=1 ),并從其帳戶餘額中扣除 $20 ( $100-$20 )。 

3 操作員 A 完成了修改工作,将資料版本号加一( version=2 ),連同帳戶扣除後餘額( balance=$50 ),送出至資料庫更新,此時由于送出資料版本大于資料庫記錄目前版本,資料被更新,資料庫記錄 version 更新為 2 。 

4 操作員 B 完成了操作,也将版本号加一( version=2 )試圖向資料庫送出資料( balance=$80 ),但此時比對資料庫記錄版本時發現,操作員 B 送出的資料版本号為 2 ,資料庫記錄目前版本也為 2 ,不滿足 “ 送出版本必須大于記錄目前版本才能執行更新 “ 的樂觀鎖政策,是以,操作員 B 的送出被駁回。 

這樣,就避免了操作員 B 用基于 version=1 的舊資料修改的結果覆寫操作員 A 的操作結果的可能。 

從上面的例子可以看出,樂觀鎖機制避免了長事務中的資料庫加鎖開銷(操作員 A和操作員 B 操作過程中,都沒有對資料庫資料加鎖),大大提升了大并發量下的系統整體性能表現.

需要注意的是,樂觀鎖機制往往基于系統中的資料存儲邏輯,是以也具備一定的局限性,如在上例中,由于樂觀鎖機制是在我們的系統中實作,來自外部系統的使用者餘額更新操作不受我們系統的控制,是以可能會造成髒資料被更新到資料庫中。在系統設計階段,我們應該充分考慮到這些情況出現的可能性,并進行相應調整(如将樂觀鎖政策在資料庫存儲過程中實作,對外隻開放基于此存儲過程的資料更新途徑,而不是将資料庫表直接對外公開)。

當然:除了SQL上的鎖可以解決資料并發外,我們也可以結合程式設計語言來實作并發的控制。

在此:以C#為例,我們可以通過代碼臨界區的定義來實作并發的控制。在C#語言中,定義一個代碼臨界區使用的關鍵字是LOCK,在此,小弟不作詳細介紹,僅僅作為提示大家,有興趣的小夥伴可以自行查閱關于C#臨界區代碼關鍵字LOCK的使用,本人系列部落格:模拟并發/C# 并發處理 鎖OR線程

中對LOCK關鍵字作了部分講解,有興趣的小虎斑可以參考下,謝謝、

@陳卧龍的部落格 

付婷,你還那麼胖嗎?如果胖,就減肥肥吧。

繼續閱讀