Spring MVC + MyBatis 搭建的項目,mysql資料庫。現在出現的問題是:A、B兩個客戶端登錄同一個賬號,同時發送同一個請求(請求使用紅包投資),出現同一個紅包使用了兩次,導致金額錯誤。伺服器現有邏輯是controller接到請求後轉給service,service裡邊會先判斷紅包能不能使用,能使用才會繼續下一步具體使用操作,但是如果A在判斷完紅包能不能使用後cpu執行時間片用完,這個時候剛好B也去判斷這個紅包能不能使用 ,A、B得到的結果都是能使用,然後都去使用紅包了


至少判斷紅包有沒有使用這個地方需要加鎖,當然涉及金額的流程一定要仔細了,我感覺不止這一個流程有問題


我知道你們都牛逼,但這次你們先閃開,讓我來裝一下。

這種問題有多種解決方式(就像茴香的茴有多種寫法),最普遍的當屬利用資料庫本身的特性來解決了。一般做活動都考慮性能,此處注意不能鎖全表,利用for update鎖全表或者利用語言特性加鎖的答案不要寫了(性能和集羣問題),這種答案一看就是小菜鳥寫的。

首先確認你所使用的的資料庫事務隔離級別,一般為讀提交或者可重複讀,這兩個應該都可以,第一個和第四個(讀未提交和序列化)不建議使用。剩餘具體步驟:

創建紅包表,每一行都代表一個紅包,紅包有一個狀態標記,我們假設這個標記欄位有三種情況:未使用、鎖定、已使用。

假設A請求來了,開啟事務-&>直接更新紅包狀態為鎖定,注意要加where條件,例如:update tb_cupon set status=locked where id=xxx and status = init ; 這裡如果返回0行,則直接回滾事務(回滾後的後續動作看業務上如何規定,是否再選一個紅包),如果返回1行-&>更新這個紅包為A使用,再次更新紅包狀態為已使用,如果更新0行同樣回滾-&>提交事務。

如果A已經用上了紅包,如果B隨後請求到同一個紅包上,update tb_cupon set status=locked where id=xxx and status = init 這條語句會返回0行(因為A請求已經將status更新成了locked),導致事務回滾,B用不上當前紅包。如果A和B完全並發,那在最後提交的時候也是隻有一個能返回1行,另一個返回更新0行導致回滾。

什麼?id事先是不知道的,臨時分配的?我知道樓主遇到什麼問題了,尼瑪,果真同行。解決方法:如果是mysql,每個請求都要給一個序列號,這個序列號要與紅包ID對應或者是同一個;如果是oracle,請自行創建sequence,或者使用緩存的自增序列,讓序列號對應紅包ID,具體用哪個看你業務情況。

本質上,這裡涉及的是一個原子性的問題,還可以有其他方式,例如使用redis的setnxex或者incr的原子性,直接先置位再用紅包,置位失敗則不能使用當前紅包。當然緩存有一定的不可靠性,這裡要根據自身業務的重要程度去定奪,好處就是效率要高一些。

話說知乎什麼時候開始有這種問題了?

--------------------------------以上是原答案,此處是分界限-------------------------

我終於理解為何大家都不在原答案上修改了。不過我只是刪除了幾句(大概在倒數第二段),其他都是原話。


請求量不是特別龐大的話,利用分散式的原子操作可以滿足中大量級的需求,請求量特別龐大的情況下,可以控制路由層,讓同一個用戶的請求分發到同一個伺服器,甚至同一個線程(路由+隊列),僅同一伺服器,可以用cas,同一線程,那同一用戶的請求就直接串列了。資料庫加鎖,這個方案適合體量小的服務。

瀉藥。

不是高並發。因為此處的邏輯線程不安全。

加鎖以確保多線程安全/用隊列轉換為單線程操作


redis之類不都支持原子操作麼 票券不做校驗? 感覺架構要重寫

redis.setnx就可以了啊


很多回答已經很好了,隊列,12306是怎麼做的?哈哈
1.資料庫加鎖2.代碼加鎖即可

把判斷紅包能不能使用的地方加鎖。其他容易並發操作造成數據不正常的也需要加鎖


推薦閱讀:
相關文章