開門見山,先聊一聊我實際遇到的業務問題:java
在項目中有一個競猜下注的功能,它的賠率是根據A隊和B隊兩邊的下注總金額來計算的。因而當有用戶下注某一邊時,兩邊的賠率都會進行相應的變化。mysql
反應到數據庫裏就是(簡化版本),一我的下注,會更改數據庫盤口表的幾個字段:A隊賠率,A隊下注金額、B隊賠率,B隊下注金額 等等。算法
若是使用默認事務方式,就加個@Transactional 註解,會致使更新丟失的問題。(何爲丟失更新:就是一個事務的更新覆蓋了其它事務的更新結果。舉個例子,A讀到的數據爲下注金額1000,對他進行計算,這時B讀到的數據也是1000。A再把計算完的1200寫進數據庫。最後B把計算完的1100寫進數據庫。最終表裏的下注金額就只有1100,發生了丟失更新)。若是真有高併發的狀況,每秒鐘幾十上百我的下注的話,就必須解決此問題。sql
默認事務沒法解決,固然就得尋求解決方案。這裏能夠採用樂觀鎖或悲觀鎖的方式。數據庫
重點:每次讀數據都加行鎖(也稱寫鎖、X鎖),修改完後事務結束才釋放。小程序
注意:mysql使用InonDB引擎時,默認增刪改時都會加行鎖。讀不加行鎖。微信小程序
# 第一步 查的時候加行鎖 (注意: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);
複製代碼
實現起來十分簡單,概念的理解放在後面寫。性能優化
樂觀鎖的實現通常會使用版本號機制或CAS算法bash
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概念略複雜,舉個簡單的實現方式:仍是盤口表,我讀數據的時候讀到了該條記錄的下注金額,賠率,將其數據暫時保存。處理完邏輯寫回去時,可用 update xxx set odds = 新賠率 where odds = 原來賠率微信
- CAS算法也有缺點,最明顯且容易理解的就是,會致使 ABA 問題。
- 若是一個變量V初次讀取的時候是A值,而且在準備賦值的時候檢查到它仍然是A值,
那咱們就能說明它的值沒有被其餘線程修改過了嗎?很明顯是不能的,由於在這段時間它的值可能被改成其餘值,
而後又改回A,那CAS操做就會誤認爲它歷來沒有被修改過。這個問題被稱爲CAS操做的 "ABA"問題。
複製代碼
因此樂觀鎖建議使用版本號機制。就加個字段,簡單輕鬆。
從上面對兩種鎖的介紹,咱們知道兩種鎖各有優缺點,不可認爲一種好於另外一種,像樂觀鎖適用於寫比較少的狀況下(多讀場景),即衝突真的不多發生的時候,這樣能夠省去了鎖的開銷,加大了系統的整個吞吐量。但若是是多寫的狀況,通常會常常產生衝突,這就會致使上層應用會不斷的進行retry,這樣反卻是下降了性能,因此通常多寫的場景下用悲觀鎖就比較合適。
記住結論,即:樂觀鎖適用於寫比較少的狀況(多讀場景);悲觀鎖適用於多寫場景。
樂觀鎖對應於生活中樂觀的人老是想着事情往好的方向發展,悲觀鎖對應於生活中悲觀的人老是想着事情往壞的方向發展。這兩種人各有優缺點,不能不以場景而定說一種人好於另一種人。
老是假設最壞的狀況,每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖(共享資源每次只給一個線程使用,其它線程阻塞,用完後再把資源轉讓給其它線程)。傳統的關係型數據庫裏邊就用到了不少這種鎖機制,好比行鎖,表鎖等,讀鎖,寫鎖等,都是在作操做以前先上鎖。Java中synchronized和ReentrantLock等獨佔鎖就是悲觀鎖思想的實現。
老是假設最好的狀況,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可使用版本號機制和CAS算法實現。樂觀鎖適用於多讀的應用類型,這樣能夠提升吞吐量,像數據庫提供的相似於write_condition機制,其實都是提供的樂觀鎖。在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS實現的。
從鎖的粒度,咱們能夠將數據庫的鎖分紅兩大類: 表鎖和行鎖,
表鎖又分爲表讀鎖和表寫鎖,
行鎖又分爲共享鎖和排他鎖,而共享鎖、排他所又有其餘別名,其實只是叫法的不一樣而已
爲了容許行鎖和表鎖共存,實現多粒度鎖機制,InnoDB還有兩種內部使用的意向鎖(Intention Locks),這兩種意向鎖都是表鎖:
網上的各類資料裏衆說紛紜:
「只讀事務」並非一個強制選項,它只是一個「暗示」,提示數據庫驅動程序和數據庫系統,這個事務並不包含更改數據的操做,那麼JDBC驅動程序和數據庫就有可能根據這種狀況對該事務進行一些特定的優化,比方說不安排相應的數據庫鎖,以減輕事務對數據庫的壓力,畢竟事務也是要消耗數據庫的資源的。 所以,「只讀事務」僅僅是一個性能優化的推薦配置而已,並不是強制你要這樣作不可。
@Transactional(readOnly = true)
複製代碼
隆鵬
蘆葦科技Java開發工程師
蘆葦科技-廣州專業軟件外包服務公司
提供微信小程序、APP應用研發、UI設計等專業服務,專一於互聯網產品諮詢、品牌設計、技術研發等領域、
訪問 www.talkmoney.cn 瞭解更多
萬能說明書 | 早起日記Lite | 凹凸壁紙 | 言財