關於樂觀鎖與悲觀鎖的實際應用

開門見山,先聊一聊我實際遇到的業務問題:java

在項目中有一個競猜下注的功能,它的賠率是根據A隊和B隊兩邊的下注總金額來計算的。因而當有用戶下注某一邊時,兩邊的賠率都會進行相應的變化。mysql

反應到數據庫裏就是(簡化版本),一我的下注,會更改數據庫盤口表的幾個字段:A隊賠率,A隊下注金額、B隊賠率,B隊下注金額 等等。算法

​ 若是使用默認事務方式,就加個@Transactional 註解,會致使更新丟失的問題。(何爲丟失更新:就是一個事務的更新覆蓋了其它事務的更新結果。舉個例子,A讀到的數據爲下注金額1000,對他進行計算,這時B讀到的數據也是1000。A再把計算完的1200寫進數據庫。最後B把計算完的1100寫進數據庫。最終表裏的下注金額就只有1100,發生了丟失更新)。若是真有高併發的狀況,每秒鐘幾十上百我的下注的話,就必須解決此問題。sql

​ 默認事務沒法解決,固然就得尋求解決方案。這裏能夠採用樂觀鎖或悲觀鎖的方式。數據庫

1、悲觀鎖解決方案

重點:每次讀數據都加行鎖(也稱寫鎖、X鎖),修改完後事務結束才釋放。小程序

注意:mysql使用InonDB引擎時,默認增刪改時都會加行鎖。讀不加行鎖。微信小程序

  • 實現以下(select 語句最後面加 for update 便可):
# 第一步 查的時候加行鎖 (注意:InnoDB只有經過索引條件檢索數據才使用行級鎖,不然,InnoDB將使用表鎖, 也就是說,InnoDB的行鎖是基於索引的!)
SELECT * FROM table_name WHERE xxx FOR UPDATE;
# 第二步 邏輯處理完後更新數據
UPDATE xxx...
複製代碼
@Query(value = "SELECT * FROM guessing_handicap WHERE handicap_id = ?1 FOR UPDATE", nativeQuery = true)
GuessingHandicap getBet(Integer id);
複製代碼

實現起來十分簡單,概念的理解放在後面寫。性能優化

2、樂觀鎖解決方案

樂觀鎖的實現通常會使用版本號機制或CAS算法bash

  • 版本號機制:在數據表中加上一個數據版本號version字段,表示數據被修改的次數,當數據被修改時,version值會加1。在讀取數據的同時也會讀取version值,在提交更新時,若剛纔讀取到的version值爲當前數據庫中的version值相等時才更新,不然重試更新操做,直到更新成功。
int count = 0; // 計數重複次數,暫定10次
while (count < 10) {
    count++;

    // 先讀取數據,保存版本號
    GuessingHandicap handicap = guessingHandicapDao.getBet(id);
    Integer version = handicap.getVersion();
    // 進行數據的處理
    // ...

    // 將處理完的結果寫回數據庫
    Integer rows = guessingHandicapDao.updateHandicap(...);
    if (rows == 0) {
        continue;
    }
    // ...
}
throw new ValidationException("下注失敗");
複製代碼
  • CAS算法:即compare and swap(比較與交換),是一種有名的無鎖算法。

CAS概念略複雜,舉個簡單的實現方式:仍是盤口表,我讀數據的時候讀到了該條記錄的下注金額,賠率,將其數據暫時保存。處理完邏輯寫回去時,可用 update xxx set odds = 新賠率 where odds = 原來賠率微信

- CAS算法也有缺點,最明顯且容易理解的就是,會致使 ABA 問題。

- 若是一個變量V初次讀取的時候是A值,而且在準備賦值的時候檢查到它仍然是A值,
那咱們就能說明它的值沒有被其餘線程修改過了嗎?很明顯是不能的,由於在這段時間它的值可能被改成其餘值,
而後又改回A,那CAS操做就會誤認爲它歷來沒有被修改過。這個問題被稱爲CAS操做的 "ABA"問題。
複製代碼

因此樂觀鎖建議使用版本號機制。就加個字段,簡單輕鬆。

兩種鎖的使用場景

​ 從上面對兩種鎖的介紹,咱們知道兩種鎖各有優缺點,不可認爲一種好於另外一種,像樂觀鎖適用於寫比較少的狀況下(多讀場景),即衝突真的不多發生的時候,這樣能夠省去了鎖的開銷,加大了系統的整個吞吐量。但若是是多寫的狀況,通常會常常產生衝突,這就會致使上層應用會不斷的進行retry,這樣反卻是下降了性能,因此通常多寫的場景下用悲觀鎖就比較合適。

​ 記住結論,即:樂觀鎖適用於寫比較少的狀況(多讀場景);悲觀鎖適用於多寫場景。

3、何謂悲觀鎖與樂觀鎖(概念的理解)

​ 樂觀鎖對應於生活中樂觀的人老是想着事情往好的方向發展,悲觀鎖對應於生活中悲觀的人老是想着事情往壞的方向發展。這兩種人各有優缺點,不能不以場景而定說一種人好於另一種人。

悲觀鎖

​ 老是假設最壞的狀況,每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖(共享資源每次只給一個線程使用,其它線程阻塞,用完後再把資源轉讓給其它線程)。傳統的關係型數據庫裏邊就用到了不少這種鎖機制,好比行鎖,表鎖等,讀鎖,寫鎖等,都是在作操做以前先上鎖。Java中synchronized和ReentrantLock等獨佔鎖就是悲觀鎖思想的實現。

樂觀鎖

​ 老是假設最好的狀況,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可使用版本號機制和CAS算法實現。樂觀鎖適用於多讀的應用類型,這樣能夠提升吞吐量,像數據庫提供的相似於write_condition機制,其實都是提供的樂觀鎖。在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS實現的。

另外補充一些易混淆概念

  • 從鎖的粒度,咱們能夠將數據庫的鎖分紅兩大類: 表鎖行鎖

  • 表鎖又分爲表讀鎖和表寫鎖,

  • 行鎖又分爲共享鎖排他鎖,而共享鎖、排他所又有其餘別名,其實只是叫法的不一樣而已

    • 共享鎖--讀鎖--S鎖
    • 排它鎖--寫鎖--X鎖
  • 爲了容許行鎖和表鎖共存,實現多粒度鎖機制,InnoDB還有兩種內部使用的意向鎖(Intention Locks),這兩種意向鎖都是表鎖

    • 意向共享鎖(IS):事務打算給數據行加行共享鎖,事務在給一個數據行加共享鎖前必須先取得該表的IS鎖。
    • 意向排他鎖(IX):事務打算給數據行加行排他鎖,事務在給一個數據行加排他鎖前必須先取得該表的IX鎖。
    • 認真梳理一遍,概念仍是挺清晰的,並且意向鎖也是數據庫隱式幫咱們作了,不須要咱們關心!

4、 對於只讀事務的理解

網上的各類資料裏衆說紛紜:
​ 「只讀事務」並非一個強制選項,它只是一個「暗示」,提示數據庫驅動程序和數據庫系統,這個事務並不包含更改數據的操做,那麼JDBC驅動程序和數據庫就有可能根據這種狀況對該事務進行一些特定的優化,比方說不安排相應的數據庫鎖,以減輕事務對數據庫的壓力,畢竟事務也是要消耗數據庫的資源的。 所以,「只讀事務」僅僅是一個性能優化的推薦配置而已,並不是強制你要這樣作不可。

@Transactional(readOnly = true)
複製代碼

只讀事務的注意點:

  • 只讀事務內,不能增長、修改、刪除內容,不然報Cannot execute statement in a READ ONLY transaction。
  • 只讀事務內,只能讀取到執行時間點前的內容,期間修改的內容不能讀取到。
  • 只讀事務做爲ORM框架優化執行的一個暗號,好比放棄加鎖,或者flush never。
  • 只讀事務也有缺點,使用了事務,會動態生成代理類,增長開銷。

應用場景總結:

  1. 若是一次執行單條查詢語句,則沒有必要啓用事務支持,數據庫默認支持SQL執行期間的讀一致性;
  2. 若是一次執行多條查詢語句,例如統計查詢,報表查詢,在這種場景下,多條查詢SQL必須保證總體的讀一致性,不然,在前條SQL查詢以後,後條SQL查詢以前,數據被其餘用戶改變,則該次總體的統計查詢將會出現讀數據不一致的狀態,此時,應該啓用事務支持。
  3. 若要將代碼寫的精緻,可按照前兩點來添加只讀事務,若嫌麻煩,所有加上註解也行 (捂臉.jpg)

隆鵬
蘆葦科技Java開發工程師

蘆葦科技-廣州專業軟件外包服務公司

提供微信小程序、APP應用研發、UI設計等專業服務,專一於互聯網產品諮詢、品牌設計、技術研發等領域、

訪問 www.talkmoney.cn 瞭解更多

萬能說明書 | 早起日記Lite | 凹凸壁紙 | 言財

相關文章
相關標籤/搜索