天天看點

悲觀鎖、樂觀鎖

悲觀鎖(Pessimistic Lock)

顧名思義,就是很悲觀,每次去拿資料的時候都認為别人會修改,是以每次在拿資料的時候都會上鎖,這樣别人想拿這個資料就會block直到它拿到鎖。傳統的關系型資料庫裡邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。

我們認為系統中的并發更新會非常頻繁,并且事務失敗了以後重來的開銷很大,這樣以來,我們就需要采用真正意義上的鎖來進行實作。悲觀鎖的基本思想就是每次一個事務讀取某一條記錄後,就會把這條記錄鎖住,這樣其它的事務要想更新,必須等以前的事務送出或者復原解除鎖。

實作方式:

大多在資料庫層面實作加鎖操作,JDBC方式:在JDBC中使用悲觀鎖,需要使用select for update語句,e.g.

<code class="language-sql hljs ">Select * from Account 
where ...(where condition).. for update</code>      

樂觀鎖(Optimistic Lock)

顧名思義,就是很樂觀,每次去拿資料的時候都認為别人不會修改,是以不會上鎖,但是在更新的時候會判斷一下在此期間别人有沒有去更新這個資料,可以使用版本号等機制。樂觀鎖适用于多讀的應用類型,這樣可以提高吞吐量,像資料庫如果提供類似于write_condition機制的其實都是提供的樂觀鎖。

我們認為系統中的事務并發更新不會很頻繁,即使沖突了也沒事,大不了重新再來一次。它的基本思想就是每次送出一個事務更新時,我們想看看要修改的東西從上次讀取以後有沒有被其它事務修改過,如果修改過,那麼更新就會失敗。

實作方式:

大多是基于資料版本(Version)記錄機制實作,何謂資料版本?即為資料增加一個版本辨別,在基于資料庫表的版本解決方案中,一般是通過為資料庫表增加一個 “version” 字段來實作。

讀取出資料時,将此版本号一同讀出,之後更新時,對此版本号加一。此時,将提 交資料的版本資料與資料庫表對應記錄的目前版本資訊進行比對,如果送出的資料 版本号大于資料庫表目前版本号,則予以更新,否則認為是過期資料。 假如系統中有一個Account的實體類,我們在Account中多加一個version字段,那麼我們JDBC Sql語句将如下寫:

<code class="language-sql hljs "><code class="language-sql hljs ">Select a.version....from Account as a 
where (where condition..)
 
Update Account set version = version+1.....(another field) 
where version =?...(another contidition)</code></code>      

這樣以來我們就可以通過更新結果的行數來進行判斷,如果更新結果的行數為0,那麼說明實體從加載以來已經被其它事務更改了,是以就抛出自定義的樂觀鎖定異常。具體執行個體如下:

<code class="language-sql hljs "><code class="language-sql hljs "><code class="language-java hljs ">int rowsUpdated = statement.executeUpdate(sql);
if (rowsUpdated ==0 ) {
    throws new OptimisticLockingFailureException();
}</code></code></code>      

典型的樂觀鎖:基于沖突檢測的樂觀鎖(CAS自旋)

Synchronized互斥鎖屬于悲觀鎖,它有一個明顯的缺點,它不管資料存不存在競争都加鎖,随着并發量增加,且如果鎖的時間比較長,其性能開銷将會變得很大。有沒有辦法解決這個問題?答案就是基于沖突檢測的樂觀鎖。這種模式下,已經沒有所謂的鎖概念了,每條線程都直接先去執行操作,計算完成後檢測是否與其他線程存在共享資料競争,如果沒有則讓此操作成功,如果存在共享資料競争則可能不斷地重新執行操作和檢測,直到成功為止,這種叫做CAS自旋。

CAS(Compare And Swap)比較并轉換 

該算法涉及三個數:記憶體位址V,舊的預期值A,新的預期值B。當且僅當舊的預期值A和記憶體值V相同時,将記憶體值改為B,否則什麼也不做。 上述的處理過程是一個原子操作(靠硬體來保證)。

如何來了解上面這一段話呢?我們先了解一下樂觀鎖和悲觀鎖各自的做事方式,首先,悲觀鎖的态度是一件事情我必須要能百分之百掌控才能去做,否則就認為這件事情一定會出問題,而樂觀鎖的态度就是不管什麼事情,我都會先嘗試去做,大不了最後不成功就是了。 

基于CAS的自旋就是典型的樂觀鎖,程式執行時,線程1從共享記憶體中取值V并建一個副本A,對A進行計算後将新的值儲存為B,然後對A值和記憶體中的V值進行比較,如果A等于V,則認為記憶體中的V值沒有被其他線程修改過,可以将新值B賦給記憶體,否則,認為記憶體中已被其他的線程修改,則重新執行計算操作和檢測,知道舊的期望值A等于記憶體值V為止。 

Java并發包java.util.concurrent.*的核心就是CAS自旋原理。如AtomicInteger、AtomicLong等都是基于CAS實作的。

兩種鎖的比較

兩種鎖各有優缺點,不可認為一種好于另一種,像樂觀鎖适用于寫比較少的情況下,即沖突真的很少發生的時候,這樣可以省去了鎖的開銷,加大了系統的整個吞吐量。但如果經常産生沖突,上層應用會不斷的進行retry,這樣反倒是降低了性能,是以這種情況下用悲觀鎖就比較合适。

樂觀鎖是假定讀取的資料,在寫之前不會被更新。适用于資料更新不頻繁的場景。

相反,當資料更新頻繁的時候,樂觀鎖的效率很低,因為基本上每次寫的時候都要重複讀寫兩次以上。

  1. 對于資料更新頻繁的場合,悲觀鎖效率更高 ;
  2. 對于資料更新不頻繁的場合,樂觀鎖效率更高;

一般來說如果并發量很高的話,建議使用悲觀鎖,否則的話就使用樂觀鎖。

如果并發量很高時使用樂觀鎖的話,會導緻很多的并發事務復原、操作失敗。