最近解決了一個小規模併發下單問題,來跟你們分享一下。mysql
如今有這麼一個業務場景,線上經過手機app下單買祈福燈,支付成功後,線下寺廟點亮。存在多個 用戶同時選擇同一個燈的狀況出現,以下圖。此時,正常狀況應爲一個用戶下單成功,其他顯示燈已被選。因爲,支付和下單是單獨分開的,只要focus on下單就ok了。sql
簡而言之,就是一個並發現單的問題。typescript
咱們能夠想到的正常下單的流程,應該是這樣的:數據庫
1. 選擇祈福燈時,先查詢燈是否可用。 2. 選擇祈福燈,例如圖中的「D0000065」。 3. 下單業務邏輯,再次查詢燈是否可用。 if(燈可用){ 該祈福燈狀態設爲已購買 生成訂單記錄 相關日誌記錄... }
在沒有併發問題發生時,上面的流程近乎完美(really?),但是,多人下單時,同時去數據庫中查詢燈的狀態時,結果都是可用的,接下來,emmmm,你懂的。markdown
那麼,判斷燈是否可用再下單,這樣的邏輯是存在問題的。解決併發下單的常規思路不外乎兩種,一是加鎖,二是利用隊列。這裏,我主要是經過對數據庫加鎖的方式來解決這個問題的。session
在此以前,須要瞭解一些關於鎖的概念。在本科的數據庫原理課上。咱們接觸到兩個概念——共享鎖和排它鎖,如今又須要兩個新的概念——樂觀鎖和悲觀鎖。併發
樂觀鎖(Optimistic Lock),想法樂觀,認爲本身在操做數據庫時不會發生衝突,取數據時不加鎖,更新數據是加鎖,進行判斷。app
悲觀鎖(Pessimistic Lock),想法悲觀,每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會block直到它拿到鎖。傳統的關係型數據庫裏邊就用到了不少這種鎖機制,好比行鎖,表鎖等,讀鎖,寫鎖等,都是在作操做以前先上鎖。ui
顯然樂觀鎖的相應速度快,悲觀鎖的時間消耗等都比較大。對於咱們這個案例,使用這兩種都是能夠解決的。spa
這裏主要以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; }