小議“悲觀鎖和樂觀鎖”的原理、場景、示例

AlbenXie發表於2018-08-20

[1] 博由

前幾天與一些朋友談到這個問題,之前有一些概念的上的涉及,但是並沒有相對深入的瞭解,因此找一些資料來幫助自己理解悲觀鎖和樂觀鎖的概念理解、場景、然後通過示例來闡述樂觀鎖和悲觀鎖的實現方式。

[2] 摘要

    本文將從三個方面來闡述悲觀鎖和樂觀鎖,以理論到實踐的思維方式呈現出個人對悲觀鎖和樂觀鎖的理解。
    [1] 悲觀鎖和樂觀鎖的理論知識
    [2] 悲觀鎖和樂觀鎖的一般使用場景&優缺點
    [3] 簡單實現樂觀鎖和悲觀鎖的

[3] 理論知識

[QA]什麼是悲觀鎖和樂觀鎖?

術語 描述 常見案例
樂觀鎖 每次去拿資料的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個資料 版本號控制,適用於多讀少寫的場景
悲觀鎖 每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會上鎖,這樣別人想拿這個資料就會block直到它拿到鎖 DB的行鎖、表鎖等,適用於資料一致性比較高的場景

[?] 個人理解
假設每個操作的物件是一個或者多個資源,悲觀鎖:可以理解為很悲觀的看待資源許可權的訪問,因此每次去操作資源時,總是會try操作,問一問是否可以去訪問(這個問一問,就是去嘗試獲取鎖),只有獲取了鎖之後,才會開始放心操作資源了;然後樂觀鎖卻是相反的,很樂觀的看待資源,不關心這個資源是否有鎖,而是直接去訪問資源,至多檢查一下當前資源是不是最新的。
用虛擬碼解釋一下:
[1] 悲觀鎖: 
while (!lock.tryGet) { // 一直等待獲取鎖許可權
    // do something,會經歷等待鎖、獲取鎖、釋放鎖的過程,是比較佔用資源的,但是確保了資源的併發訪問可能出現的問題。
    lock.release
    break
}
[2] 樂觀鎖:
if (checkResourceVersion) { // 檢查資源版本是否一致
    // do something
}else {
    // 過期, 更新資料無效
}

[4] 案例

在大概瞭解了,悲觀鎖和樂觀鎖的概念之後,我們看看在實際生產中,具體有那些案例和場景使用到了悲觀鎖和樂觀鎖,以及其對應的一般問題解決方案。

[4.1] 樂觀鎖案例

一般實現樂觀鎖的方式:
[1] 版本號控制
[2] 時間戳控制

[4.1.1] 樂觀鎖 - 版本號控制案例

一般會在資料庫表增加一個version欄位,這個欄位標識當前資料的版本,每次更新操作都會version+=1;流程下圖:

版本控制流程情況 
[圖片引用] 
http://www.javaweb1024.com/java/JavaWebzhongji/2015/09/06/847.html

[1] 過程描述
1,start transaction  
2,first_version = get_cur_version() // 獲取當前資料版本
3,update_data(version+=1)           // 更新操作版本號+1
4,cur_version = get_cur_version()   // 提交更新時,獲取版本號
5,if first_version == cur_version // 比較提交時的版本號與第一次獲取的版本號,如果一致,那麼認為資源是最新的,可以更新
  then commit 
  else rollback or raise exception // 否則回滾或者丟擲異常
[2] 原理描述
最關鍵的點,在於確保每次提交的資訊是最新的,認為是沒有競爭的或者說很少競爭的,通過version來標識每一次的資料更新操作,當存在併發時,同一個資料,會又多個使用者進行更新操作,如果通過樂觀鎖來實現,在多寫的情況下,會頻繁出現異常或者回滾,因此一般使用在多讀少寫的情況,以提高系統吞吐量。

[4.1.2] 樂觀鎖 - 時間戳控制案例

時間戳的方式與版本號實際上原理差不多,每次更新資料時,會更新該時間戳欄位,以標識資料的更新情況。
[過程描述]
1, start transaction
2, first_timestamp = get_cur_timestamp()
3, update_data(timestamp=get_sys_cur_timestamp)
4, if first_timestamp = get_cur_timestamp()
   then commit 
   else rollback or raise Exception
[原理]
每次更新資料時,時間戳會記錄更新時間,如果出現併發更新,會導致A,B事務更新提交時讀取的不是事務起初讀取的時間戳,因而導致失敗,同樣適合於多讀少寫的場景。

[4.2] 悲觀鎖案例

    悲觀鎖的實現一般都是通過鎖機制來實現的,鎖可以簡單理解為資源的訪問的入口。如果要對一個具有鎖屬性的資源執行訪問時,在更新操作時,需要持鎖權才能進行操作,但是往往這種操作可以保證資料的一致性和完整性。

在資料庫中,表鎖、行鎖都是通過悲觀鎖形式來實現的,通過模擬一下mysql的行鎖形式,來闡述悲觀鎖的執行機制: 
行鎖

事務A,事務B,當事務A對id=1的記錄加了for update行鎖之後,事務B如果想訪問id=1的記錄,會出現block,因為鎖已經被事務A佔有了,要麼事務A操作完成,然後執行事務B block的操作,要不等待超時。

[5] 場景

我們知道了樂觀鎖和悲觀鎖的概念,已經一般的使用方式,那麼我們還需要了解到的是:什麼時候使用悲觀鎖,什麼時候使用樂觀鎖?

[5.1] 什麼時候使用悲觀鎖?

    一旦通過悲觀鎖鎖定一個資源,那麼其他需要操作該資源的使用方,只能等待直到鎖被釋放,好處在於可以減少併發,但是當併發量非常大的時候,由於鎖消耗資源,並且可能鎖定時間過長,容易導致系統效能下降,資源消耗嚴重。因此一般我們可以在併發量不是很大,並且出現併發情況導致的異常使用者和系統都很難以接受的情況下,會選擇悲觀鎖進行。

[5.2] 什麼時候使用樂觀鎖?

    樂觀鎖實際上並沒用實際的鎖資源操作,就如上面概述的版本號和時間戳方式一樣,使用方都可以操作相應的資源,而當第一個使用方提交之後,其他使用方提交時,會出現異常(例如:程式碼版本控制器SVN,GIT),其可以增加系統的併發處理能力,但是如果併發導致了資源提交衝突,其他使用方需要重新讀取資源,會增加讀的次數,但是可以面對高併發場景,前提是如果出現提交失敗,使用者是可以接受的。因此一般樂觀鎖只用在高併發、多讀少寫的場景。
    其中:GIT,SVN,CVS等程式碼版本控制管理器,就是一個樂觀鎖使用很好的場景,例如:A、B程式設計師,同時從SVN伺服器上下載了code.html檔案,當A完成提交後,此時B再提交,那麼會報版本衝突,此時需要B進行版本處理合並後,再提交到伺服器。這其實就是樂觀鎖的實現全過程。如果此時使用的是悲觀鎖,那麼意味者所有程式設計師都必須一個一個等待操作提交完,才能訪問檔案,這是難以接受的。


參考

[1]http://www.javaweb1024.com/java/JavaWebzhongji/2015/09/06/847.html 
[2]http://blog.csdn.net/sd4015700/article/details/50162965

相關文章