並發初體驗,解決小規模並發下單問題


最近解決了一個小規模並發下單問題,來跟大家分享一下。

場景描述

現在有這么一個業務場景,線上通過手機app下單買祈福燈,支付成功后,線下寺廟點亮。存在多個 用戶同時選擇同一個燈的情況出現,如下圖。此時,正常情況應為一個用戶下單成功,其余顯示燈已被選。由於,支付和下單是單獨分開的,只要focus on下單就ok了。

簡而言之,就是一個並發現單的問題。

分析過程

我們可以想到的正常下單的流程,應該是這樣的:

1. 選擇祈福燈時,先查詢燈是否可用。
2. 選擇祈福燈,例如圖中的“D0000065”。
3. 下單業務邏輯,再次查詢燈是否可用。
if(燈可用){
	該祈福燈狀態設為已購買
	生成訂單記錄
	相關日志記錄...
}

在沒有並發問題發生時,上面的流程近乎完美(really?),可是,多人下單時,同時去數據庫中查詢燈的狀態時,結果都是可用的,接下來,emmmm,你懂的。

那么,判斷燈是否可用再下單,這樣的邏輯是存在問題的。解決並發下單的常規思路不外乎兩種,一是加鎖,二是利用隊列。這里,我主要是通過對數據庫加鎖的方式來解決這個問題的。

樂觀鎖與悲觀鎖

在此之前,需要了解一些關於的概念。在本科的數據庫原理課上。我們接觸到兩個概念——共享鎖和排它鎖,現在又需要兩個新的概念——樂觀鎖和悲觀鎖

  • 樂觀鎖(Optimistic Lock),想法樂觀,認為自己在操作數據庫時不會發生沖突,取數據時不加鎖,更新數據是加鎖,進行判斷。
  • 悲觀鎖(Pessimistic Lock),想法悲觀,每次去拿數據的時候都認為別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會block直到它拿到鎖。傳統的關系型數據庫里邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。

顯然樂觀鎖的相應速度快,悲觀鎖的時間消耗等都比較大。對於我們這個案例,使用這兩種都是可以解決的。

鎖的sql實現方式

這里主要以mysql innodb為例。innodb本身是支持行鎖的。

悲觀鎖

要使用悲觀鎖,我們必須關閉mysql數據庫的自動提交屬性,因為MySQL默認使用autocommit模式,也就是說,當你執行一個更新操作后,MySQL會立刻將結果進行提交。

set autocommit=0;
  • 共享鎖(s鎖)
SELECT … LOCK IN SHARE MODE

SELECT … LOCK IN SHARE MODE 在讀取的行上設置一個共享鎖,其他的session可以讀這些行,但在你的事務提交之前不可以修改它們。如果這些行里有被其他的還沒有提交的事務修改,你的查詢會等到那個事務結束之后使用最新的值。

  • 排它鎖(x鎖)
SELECT … FOR UPDATE

索引搜索遇到的記錄,SELECT … FOR UPDATE 會鎖住行及任何關聯的索引條目,和你對那些行執行 update 語句相同。其他的事務會被阻塞在對這些行執行 update 操作,獲取共享鎖,或從某些事務隔離級別讀取數據等操作。一致性讀(Consistent Nonlocking Reads)會忽略在讀取視圖上的記錄的任何鎖。(舊版本的記錄不能被鎖定;它們通過應用撤銷日志在記錄的內存副本上時被重建。)

注:普通 select 語句默認不加鎖,而CUD操作默認加排他鎖。

樂觀鎖

樂觀鎖也就是在更新時進行查詢,通常用一個version字段來實現。

UPDATE ... WHERE...
# 基於version的實現
SELECT ..., verison FROM [table] WHERE id = #{id}
UPDATE [table] SET..., version = version + 1 where id = #{id} AND version = #{version}

當然,在ORM中也有相應的實現方式。具體可以參考細談Hibernate之悲觀鎖和樂觀鎖解決hibernate並發

問題解決

在項目中,由於時間關系沒有使用基於version方式的樂觀鎖,而是直接采用了update ... where的方式。直接對當前的燈號進行查詢,如果可用就立刻更新燈的狀態為不可用,相當於加共享鎖。如果發生並發的情況,同時用update語句,數據庫也會自動加上X鎖,因此最終只有一個用戶可以下單成功。
下單流程:

public int saveOrder(){
	// 執行update ... where
    boolean isAvaliable;
    isAvaliable = denginfoService.updateDengAnyway();
    if (isAvaliable) {
    	//下單的業務邏輯
    }
}

public boolean updateDengAnyway(String ccode, List<String> dengid) {
	//判斷燈是否可用
    String hql = "update DenginfoEntity set ordertype =2 where ccode=? and dengid=? and (ordertype=0 or (ordertype=1 and ordertime<?))";
    Query query;int flag;
    try {
        for (String deng : dengid) {
            query = getSession().createQuery(hql);
            //參數化賦值
            flag = query.executeUpdate();
            logger.info("update---  " + flag);
            if (flag == 0)
                return false;
        }
    } catch (HibernateException | NullPointerException e) {
        e.printStackTrace();
        return false;
    }
    return true;
}

參考文獻


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM