訂單系統中併發問題和鎖機制的探討

飄揚的紅領巾發表於2014-05-06

問題由來

    假設在一個訂單系統中(以火車票訂單系統為例),使用者A,使用者B都要預定從成都到北京的火車票,A、B在不同的售票視窗均同時查詢到了某車廂臥鋪中、下鋪位有空位。使用者A正在猶豫訂中鋪還是下鋪,這時使用者B果斷訂購了下鋪。當使用者A決定訂下鋪時,系統提示下鋪已經被預訂,請重新選擇鋪位。在這個系統場景中,我們來探討一下,火車票系統是怎樣處理併發事件以及怎麼利用鎖機制來避免重複訂票的。

設想的方案

方案1:

    為了避免重複訂票,大部分人會想到在做訂票操作前,去資料庫查詢該鋪位是否已經被預訂,假設“鋪位”資料庫表增加標記欄位FLAG(空閒:0;已預訂:1),如果查詢到鋪位的FLAG欄位值為1,那麼預訂就不成功,如果為0就成功預訂,並把FLAG置為1。這種方案如果在業務量很少的系統中,或許可行。但業務量較大時,特別是火車票這樣的業務量,就會出現問題。問題在,當使用者A、使用者B同時對同一鋪位預訂時,雖說是“同時”,但對於資料庫操作來說一定是有先後順序的,假設A在查詢該鋪位的FLAG時,值為0,準備預訂並將值設為1,而與此同時B已經預訂成功,並已將FLAG設為1。而A因為沒有即時查詢到FLAG=1,因此也預訂成功,又將FLAG設為1。

A      FLAG=0      時刻=T1   (查詢)

B      FLAG=0      時刻=T2   (查詢)

B      FLAG=1      時刻=T3   (更新)

A      FLAG=1      時刻=T4   (更新)

這樣就造成了重複訂票,在購票高峰期,使用這樣的方案,重複訂票不可避免。

方案2:

    我們想到了利用資料庫的悲觀鎖來解決這個問題,設想假如使用者A查詢到想預訂的票,使用者B根本都查詢不到,只有A一個人能看到,那是不是沒有重複訂票的可能了,因為壓根沒人跟他搶。可以這樣實現這個方案:

select * from table where …… for update skip locked,該語句是查詢使用者指定條件的票資訊,並加鎖(for update),如果有記錄已經被鎖則自動跳到下一條記錄(skip locked),這樣誰先查詢誰就可以慢慢的考慮要上鋪還是下鋪。但火車票系統是這樣做的嗎?顯然不是,因為這樣使用者體驗太不好,票實際還很多,但確看不到買不到,這顯然不合理。

方案3:

    我們又想到了從程式層面來解決併發問題,最簡便的方式是利用synchronized來處理,但我們要知道一個大型系統必然是叢集方式部署的,synchronized只能解決單節點環境的併發問題,要解決此問題還是必須依賴全域性性的鎖機制。

方案4:

    既然又回到了在資料庫上加鎖,我們又想一下如果我們在查詢時,使用樂觀鎖,但在預訂之前使用悲觀鎖會怎樣呢?例如我們查詢時:

select * from table where ……

使用者A、使用者B都查詢到了相同的票資訊(中鋪和下鋪),使用者A或使用者B在預訂時做一次悲觀鎖:

select * from table where …… for update(只對預訂的票做悲觀鎖)

此時後者在預訂時,無法獲取該記錄的鎖,自然就無法預訂,避免了重複預訂的問題。

 

 

還有方案嗎……

相關文章