最近解決了一個小規模並發下單問題,來跟大家分享一下。
場景描述
現在有這么一個業務場景,線上通過手機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;
}
