天天看點

資料庫中的悲觀鎖和樂觀鎖悲觀鎖樂觀鎖總結

現在我們簡單聊一下資料庫中的悲觀鎖和樂觀鎖。

悲觀鎖

悲觀鎖正如其名稱,比較悲觀。總會認為:每當修改資料時,會有其他線程也會同時修改該資料。是以針對這種情況悲觀鎖的做法是:讀取資料之後就加鎖

(eg: select...for update)

,這樣别的線程讀取該資料的時候就需要等待目前線程釋放鎖,獲得到鎖的線程才能獲得該資料的讀寫權限。進而保證了并發修改資料錯誤的問題。但是由于阻塞原因,是以導緻吞吐量不高。悲觀鎖更适用于多寫少讀的情況。
資料庫中的悲觀鎖和樂觀鎖悲觀鎖樂觀鎖總結

場景: 同學A和同學B都要給你轉500塊錢(開心壞了吧,這樣最終你能得到1000塊錢)。

使用悲觀鎖的流程:

  1. 同學A擷取到你的賬戶餘額

    balance = 0

    并對該條記錄加鎖。
  2. 同學B擷取你的賬戶餘額。由于同學A已經對這條記錄加鎖了,是以同學B需要等同學A轉帳完成(釋放鎖)才能獲得餘額。
  3. 同學A轉賬完成并釋放鎖,此時你的賬戶餘額

    balance=balance + 500 = 500

  4. 同學B擷取到你的賬戶餘額

    balance = 500

    ,并對該條記錄加鎖(如果你人緣好,此時同學C給你轉賬也是需要等待同學B轉賬完成才可以轉賬哦)
  5. 同學B轉賬完成并釋放鎖(如果有同學C想給你轉賬,此時同學C就可以獲得鎖并轉賬了)。此時你的賬戶餘額為

    balance = balance + 500 = 1000

  6. 最終你開開心心的得到了1000塊錢。

假設轉賬過程沒有鎖,我們看看會發生什麼:

  1. 同學A擷取到你的賬戶餘額

    balance_a = 0

    (沒有加鎖,此時同學B也可以擷取到賬戶餘額)
  2. 同學B擷取到你的賬戶餘額

    balance_b = 0

  3. 同學A轉賬完成,此時你的賬戶餘額為

    balance = balance_a + 500 = 500

  4. 同學B轉賬完成,此時你的賬戶餘額為

    balance = balance_b + 500 = 500

  5. 最終同學A和同學B都轉了500,但是你最終隻獲得了500。這一定是不能接受的吧。
資料庫中的悲觀鎖和樂觀鎖悲觀鎖樂觀鎖總結

丢失的500塊去哪裡了呢?從第2步可以看到同學B擷取到的賬戶餘額是0,而不是同學A轉帳之後的餘額500。是以問題出在這裡,這是高并發場景的常見問題。是以加鎖是非常必須的。但是加了悲觀鎖,同學都要排隊給我轉賬,對于沒有耐心的同學就直接不轉帳了,我豈不是錯失了發财的好機會。那有什麼好辦法呢?答案就是下面的樂觀鎖

樂觀鎖

樂觀鎖顧名思義比較樂觀,他隻有在更新資料的時候才會檢查這條資料是否被其他線程更新了(這點與悲觀鎖一樣,悲觀鎖是在讀取資料的時候就加鎖了)。如果更新資料時,發現這條資料被其他線程更新了,則此次更新失敗。如果資料未被其他線程更新,則更新成功。由于樂觀鎖沒有了鎖等待,提高了吞吐量,是以樂觀鎖适合多讀少寫的場景。
常見的樂觀鎖實作方式是:版本号version和CAS(compare and swap)。此處隻介紹版本号方式。

要采用版本号,首先需要在資料庫表中新增一個字段version,表示此條記錄的更新版本,記錄每變動一次,版本号加1。依舊使用上面轉賬的例子說明:

  1. 同學A擷取到你的賬戶餘額

    balance = 0

    和版本号

    version_a = 0

  2. 同學B擷取到你的賬戶餘額

    balance = 0

    和版本号

    version_b = 0

  3. 同學A轉賬完成

    update table set balance = ${balance}, version = version + 1 and version = 0

    。(此時版本号為0,是以更新成功)
  4. 同學B轉賬完成

    update table set balance = ${balance}, version = version + 1 and version = 0

    。(此時版本号為1,是以更新失敗,更新失敗之後同學B再轉一次即可)
  5. 同學B重新轉帳之後,你還是美滋滋的獲得了1000。

總結

悲觀鎖:讀取時加鎖,更新完釋放鎖,再此過程中會造成其他線程阻塞,導緻吞吐量低,适用于多寫場景。

樂觀鎖:不加鎖,隻有在更新時驗證資料是否被其他線程更新,吞吐量較高,适用于多讀場景。