關於處理高並發,防止庫存超賣的問題


一.問題描述:

一般電子商務網站都會遇到如團購、秒殺、特價之類的活動,而這樣的活動有一個共同的特點就是訪問量激增、上千甚至上萬人搶購一個商品。

然而,作為活動商品,庫存肯定是很有限的,如何控制庫存不讓出現超買,以防止造成不必要的損失是眾多電子商務網站程序員頭疼的問題,這同時也是最基本的問題。

從技術方面剖析,很多人肯定會想到事務,但是事務是控制庫存超賣的必要條件,但不是充分必要條件。

 

1、在秒殺的情況下,肯定不能如此高頻率的去讀寫數據庫,會嚴重造成性能問題的。

必須使用緩存,將需要秒殺的商品放入緩存中,並使用鎖來處理其並發情況。當接到用戶秒殺提交訂單的情況下,

先將商品數量遞減(加鎖/解鎖)后再進行其他方面的處理,處理失敗在將數據遞增1(加鎖/解鎖),否則表示交易成功。

當商品數量遞減到0時,表示商品秒殺完畢,拒絕其他用戶的請求。

 

2、這個肯定不能直接操作數據庫的,會掛的。直接讀庫寫庫對數據庫壓力太大,要用緩存。

把你要賣出的商品比如10個商品放到緩存中;然后在memcache里設置一個計數器來記錄請求數,這個請求數你可以以你要秒殺賣出的商品數為基數,

比如你想賣出10個商品,只允許100個請求進來。那當計數器達到100的時候,后面進來的就顯示秒殺結束,這樣可以減輕你的服務器的壓力。

然后根據這100個請求,先付款的先得后付款的提示商品以秒殺完。

 

3、首先,多用戶並發修改同一條記錄時,肯定是后提交的用戶將覆蓋掉前者提交的結果了。

這個直接可以使用加鎖機制去解決,樂觀鎖或者悲觀鎖。

(1)樂觀鎖:就是在數據庫設計一個版本號的字段,每次修改都使其+1,這樣在提交時比對提交前的版本號就知道是不是並發提交了,

              但是有個缺點就是只能是應用中控制,如果有跨應用修改同一條數據樂觀鎖就沒辦法了,這個時候可以考慮悲觀鎖。

(2)悲觀鎖:就是直接在數據庫層面將數據鎖死,類似於oralce中使用select xxxxx from xxxx where xx=xx for update,這樣其他線程將無法提交數據。

除了加鎖的方式也可以使用接收鎖定的方式,思路是在數據庫中設計一個狀態標識位,用戶在對數據進行修改前,將狀態標識位標識為正在編輯的狀態,

這樣其他用戶要編輯此條記錄時系統將發現有其他用戶正在編輯,則拒絕其編輯的請求,類似於你在操作系統中某文件正在執行,然后你要修改該文件時,系統會提醒你該文件不可編輯或刪除。

 

4、不建議在數據庫層面加鎖,建議通過服務端的內存鎖(鎖主鍵)。當某個用戶要修改某個id的數據時,把要修改的id存入memcache,

若其他用戶觸發修改此id的數據時,讀到memcache有這個id的值時,就阻止那個用戶修改。

 

5、實際應用中,並不是讓mysql去直面大並發讀寫,會借助“外力”,比如緩存、利用主從庫實現讀寫分離、分表、使用隊列寫入等方法來降低並發讀寫。

 

二.秒殺帶來了什么

秒殺或搶購活動一般會經過【預約】【搶訂單】【支付】這3個大環節,而其中【搶訂單】這個環節是最考驗業務提供方的抗壓能力的。

搶訂單環節一般會帶來2個問題:

1、高並發

比較火熱的秒殺在線人數都是10w起的,如此之高的在線人數對於網站架構從前到后都是一種考驗。

2、超賣

任何商品都會有數量上限,如何避免成功下訂單買到商品的人數不超過商品數量的上限,這是每個搶購活動都要面臨的難題。

 

三.如何解決

首先討論技術解決方案

1、前端

面對高並發的搶購活動,前端常用的三板斧是【擴容】【靜態化】【限流】

(1)擴容:加機器,這是最簡單的方法,通過增加前端池的整體承載量來抗峰值。

(2)靜態化:將活動頁面上的所有可以靜態的元素全部靜態化,並盡量減少動態元素。通過CDN來抗峰值。

(3)限流:一般都會采用IP級別的限流,即針對某一個IP,限制單位時間內發起請求數量。或者活動入口的時候增加游戲或者問題環節進行消峰操作。

(4)有損服務:最后一招,在接近前端池承載能力的水位上限的時候,隨機拒絕部分請求來保護活動整體的可用性。

 

2、后端

那么后端的數據庫在高並發和超賣下會遇到什么問題呢?主要會有如下3個問題:(主要討論寫的問題,讀的問題通過增加cache可以很容易的解決)

(1)首先MySQL自身對於高並發的處理性能就會出現問題,一般來說,MySQL的處理性能會隨着並發thread上升而上升,

         但是到了一定的並發度之后會出現明顯的拐點,之后一路下降,最終甚至會比單thread的性能還要差。

(2)其次,超賣的根結在於減庫存操作是一個事務操作,需要先select,然后insert,最后update -1。最后這個-1操作是不能出現負數的,但是當多用戶在有庫存的情況下並發操作,出現負數這是無法避免的。

(3)最后,當減庫存和高並發碰到一起的時候,由於操作的庫存數目在同一行,就會出現爭搶InnoDB行鎖的問題,導致出現互相等待甚至死鎖,

         從而大大降低MySQL的處理性能,最終導致前端頁面出現超時異常。

 

針對上述問題,如何解決呢? 先看眼淘寶的高大上解決方案:

(1)關閉死鎖檢測,提高並發處理性能。

(2)修改源代碼,將排隊提到進入引擎層前,降低引擎層面的並發度。

(3)組提交,降低server和引擎的交互次數,降低IO消耗。

淘寶在所有優化都使用后,TPS在高並發下,從原始的150飆升到8.5w,提升近566倍,非常嚇人!

不過結合我們的實際,改源碼這種高大上的解決方案顯然有那么一點不切實際。於是小伙伴們需要討論出一種適合我們實際情況的解決方案。以下就是我們討論的解決方案:

首先設定一個前提,為了防止超賣現象,所有減庫存操作都需要進行一次減后檢查,保證減完不能等於負數。(由於MySQL事務的特性,這種方法只能降低超賣的數量,但是不可能完全避免超賣)

update number set x=x-1 where (x -1 ) >= 0;

解決方案1:

將存庫從MySQL前移到Redis中,所有的寫操作放到內存中,由於Redis中不存在鎖故不會出現互相等待,並且由於Redis的寫性能和讀性能都遠高於MySQL,

這就解決了高並發下的性能問題。然后通過隊列等異步手段,將變化的數據異步寫入到DB中。

優點:解決性能問題

缺點:沒有解決超賣問題,同時由於異步寫入DB,存在某一時刻DB和Redis中數據不一致的風險。

 

解決方案2:

引入隊列,然后將所有寫DB操作在單隊列中排隊,完全串行處理。當達到庫存閥值的時候就不在消費隊列,並關閉購買功能。這就解決了超賣問題。

優點:解決超賣問題,略微提升性能。

缺點:性能受限於隊列處理機處理性能和DB的寫入性能中最短的那個,另外多商品同時搶購的時候需要准備多條隊列。

 

解決方案3:

將寫操作前移到Memcached中,同時利用Memcached的輕量級的鎖機制CAS來實現減庫存操作。

優點:讀寫在內存中,操作性能快,引入輕量級鎖之后可以保證同一時刻只有一個寫入成功,解決減庫存問題。

缺點:沒有實測,基於CAS的特性不知道高並發下是否會出現大量更新失敗?不過加鎖之后肯定對並發性能會有影響。

 

解決方案4:

將提交操作變成兩段式,先申請后確認。然后利用Redis的原子自增操作(相比較MySQL的自增來說沒有空洞),同時利用Redis的事務特性來發號,

保證拿到小於等於庫存閥值的號的人都可以成功提交訂單。然后數據異步更新到DB中。

優點:解決超賣問題,庫存讀寫都在內存中,故同時解決性能問題。

缺點:由於異步寫入DB,可能存在數據不一致。另可能存在少買,也就是如果拿到號的人不真正下訂單,可能庫存減為0,但是訂單數並沒有達到庫存閥值。

 


免責聲明!

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



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