悲觀鎖和樂觀鎖的比較和使用

悲觀鎖(Pessimistic Lock)

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

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

實現方式:

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

Select * from Account 
where ...(where condition).. for update

樂觀鎖(Optimistic Lock)

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

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

實現方式:

大多是基於數據版本(Version)記錄機制實現,何謂數據版本?即爲數據增加一個版本標識,在基於數據庫表的版本解決方案中,一般是通過爲數據庫表增加一個 “version” 字段來實現。

讀取出數據時,將此版本號一同讀出,之後更新時,對此版本號加一。此時,將提 交數據的版本數據與數據庫表對應記錄的當前版本信息進行比對,如果提交的數據 版本號大於數據庫表當前版本號,則予以更新,否則認爲是過期數據。
假如系統中有一個Account的實體類,我們在Account中多加一個version字段,那麼我們JDBC Sql語句將如下寫:
e.g.

Select a.version....from Account as a 
where (where condition..)

Update Account set version = version+1.....(another field) 
where version =?...(another contidition)

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

int rowsUpdated = statement.executeUpdate(sql);
if (rowsUpdated ==0 ) {
    throws new OptimisticLockingFailureException();
}

悲觀鎖的實現

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

Java裏的CompareAndSet(CAS)

以AtomicInteger的incrementAndGet的實現爲例:

incrementAndGet的實現
    public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return next;
        }
    }

首先可以看到他是通過一個無限循環(spin)直到increment成功爲止。

循環的內容是:

  1. 取得當前值
  2. 計算+1後的值
  3. 如果當前值還有效(沒有被)的話設置那個+1後的值
  4. 如果設置沒成功(當前值已經無效了即被別的線程改過了), 再從1開始。
compareAndSet的實現
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

直接調用的是UnSafe這個類的compareAndSwapInt方法,全稱是sun.misc.Unsafe。這個類是Oracle(Sun)提供的實現,可能在別的公司的JDK裏就不是這個類了。

compareAndSwapInt的實現
    /**
     * Atomically update Java variable to <tt>x</tt> if it is currently
     * holding <tt>expected</tt>.
     * @return <tt>true</tt> if successful
     */
    public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);

此方法不是Java實現的,而是通過JNI調用操作系統的原生程序,涉及到CPU原子操作,現在幾乎所有的CPU指令都支持CAS的原子操作,X86下對應的是CMPXCHG彙編指令。

出於好奇,查看了下CAS原子操作的代碼描述:

int compare_and_swap(int* reg, int oldval, int newval) {
    ATOMIC();
    int old_reg_val = *reg;
    if (old_reg_val == oldval)
    *reg = newval;
    END_ATOMIC();
    return old_reg_val;
}

也就是檢查內存*reg裏的值是不是oldval,如果是的話,則對其賦值newval。上面的代碼總是返回old_reg_value,調用者如果需要知道是否更新成功還需要做進一步判斷,爲了方便,它可以變種爲直接返回是否更新成功,如下:

bool compare_and_swap (int *accum, int *dest, int newval)
{
    if ( *accum == *dest ) {
        *dest = newval;
        return true;
    }
    return false;
}

兩種鎖的比較

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章