先來就庫存超賣的問題做描述:通常電子商務網站都會遇到如團購、秒殺、特價之類的活動,而這樣的活動有一個共同的特色就是訪問量激增、上千甚至上萬人搶購一個商品。然而,做爲活動商品,庫存確定是頗有限的,如何控制庫存不讓出現超買,以防止形成沒必要要的損失是衆多電子商務網站程序員頭疼的問題,這同時也是最基本的問題。前端
從技術方面剖析,不少人確定會想到事務,可是事務是控制庫存超賣的必要條件,但不是充分必要條件。mysql
舉例:程序員
總庫存:4個商品sql
請求人:a、1個商品 b、2個商品 c、3個商品數據庫
程序以下:後端
beginTranse(開啓事務)緩存
try{服務器
$result = $dbca->query('select amount from s_store where postID = 12345');架構
if(result->amount > 0){併發
//quantity爲請求減掉的庫存數量
$dbca->query('update s_store set amount = amount - quantity where postID = 12345');
}
}catch($e Exception){
rollBack(回滾)
}
commit(提交事務)
以上代碼就是咱們平時控制庫存寫的代碼了,大多數人都會這麼寫,看似問題不大,其實隱藏着巨大的漏洞。數據庫的訪問其實就是對磁盤文件的訪問,數據庫中的表其實就是保存在磁盤上的一個個文件,甚至一個文件包含了多張表。例如因爲高併發,當前有三個用戶a、b、c三個用戶進入到了這個事務中,這個時候會產生一個共享鎖,因此在select的時候,這三個用戶查到的庫存數量都是4個,同時還要注意,mysql innodb查到的結果是有版本控制的,再其餘用戶更新沒有commit以前(也就是沒有產生新版本以前),當前用戶查到的結果依然是就版本;
而後是update,假如這三個用戶同時到達update這裏,這個時候update更新語句會把併發串行化,也就是給同時到達這裏的是三個用戶排個序,一個一個執行,並生成排他鎖,在當前這個update語句commit以前,其餘用戶等待執行,commit後,生成新的版本;這樣執行完後,庫存確定爲負數了。可是根據以上描述,咱們修改一下代碼就不會出現超買現象了,代碼以下:
beginTranse(開啓事務)
try{
//quantity爲請求減掉的庫存數量
$dbca->query('update s_store set amount = amount - quantity where postID = 12345');
$result = $dbca->query('select amount from s_store where postID = 12345');
if(result->amount < 0){
throw new Exception('庫存不足');
}
}catch($e Exception){
rollBack(回滾)
}
commit(提交事務)
另外,更簡潔的方法:
beginTranse(開啓事務)
try{
//quantity爲請求減掉的庫存數量
$dbca->query('update s_store set amount = amount - quantity where amount>=quantity and postID = 12345');
}catch($e Exception){
rollBack(回滾)
}
commit(提交事務)
=====================================================================================
一、在秒殺的狀況下,確定不能如此高頻率的去讀寫數據庫,會嚴重形成性能問題的
必須使用緩存,將須要秒殺的商品放入緩存中,並使用鎖來處理其併發狀況。當接到用戶秒殺提交訂單的狀況下,先將商品數量遞減(加鎖/解鎖)後再進行其餘方面的處理,處理失敗在將數據遞增1(加鎖/解鎖),不然表示交易成功。
當商品數量遞減到0時,表示商品秒殺完畢,拒絕其餘用戶的請求。
二、這個確定不能直接操做數據庫的,會掛的。直接讀庫寫庫對數據庫壓力太大,要用緩存。
把你要賣出的商品好比10個商品放到緩存中;而後在memcache裏設置一個計數器來記錄請求數,這個請求數你能夠以你要秒殺賣出的商品數爲基數,好比你想賣出10個商品,只容許100個請求進來。那當計數器達到100的時候,後面進來的就顯示秒殺結束,這樣能夠減輕你的服務器的壓力。而後根據這100個請求,先付款的先得後付款的提示商品以秒殺完。
三、首先,多用戶併發修改同一條記錄時,確定是後提交的用戶將覆蓋掉前者提交的結果了。
這個直接可使用加鎖機制去解決,樂觀鎖或者悲觀鎖。
樂觀鎖,就是在數據庫設計一個版本號的字段,每次修改都使其+1,這樣在提交時比對提交前的版本號就知道是否是併發提交了,可是有個缺點就是隻能是應用中控制,若是有跨應用修改同一條數據樂觀鎖就沒辦法了,這個時候能夠考慮悲觀鎖。
悲觀鎖,就是直接在數據庫層面將數據鎖死,相似於oralce中使用select xxxxx from xxxx where xx=xx for update,這樣其餘線程將沒法提交數據。
除了加鎖的方式也可使用接收鎖定的方式,思路是在數據庫中設計一個狀態標識位,用戶在對數據進行修改前,將狀態標識位標識爲正在編輯的狀態,這樣其餘用戶要編輯此條記錄時系統將發現有其餘用戶正在編輯,則拒絕其編輯的請求,相似於你在操做系統中某文件正在執行,而後你要修改該文件時,系統會提醒你該文件不可編輯或刪除。
四、不建議在數據庫層面加鎖,建議經過服務端的內存鎖(鎖主鍵)。當某個用戶要修改某個id的數據時,把要修改的id存入memcache,若其餘用戶觸發修改此id的數據時,讀到memcache有這個id的值時,就阻止那個用戶修改。
五、實際應用中,並非讓mysql去直面大併發讀寫,會藉助「外力」,好比緩存、利用主從庫實現讀寫分離、分表、使用隊列寫入等方法來下降併發讀寫。
1、秒殺帶來了什麼?
秒殺或搶購活動通常會通過【預定】【搶訂單】【支付】這3個大環節,而其中【搶訂單】這個環節是最考驗業務提供方的抗壓能力的。
搶訂單環節通常會帶來2個問題:
一、高併發
比較火熱的秒殺在線人數都是10w起的,如此之高的在線人數對於網站架構從前到後都是一種考驗。
二、超賣
任何商品都會有數量上限,如何避免成功下訂單買到商品的人數不超過商品數量的上限,這是每一個搶購活動都要面臨的難題。
2、如何解決?
首先,產品解決方案咱們就不予討論了。咱們只討論技術解決方案
一、前端
面對高併發的搶購活動,前端經常使用的三板斧是【擴容】【靜態化】【限流】
A:擴容
加機器,這是最簡單的方法,經過增長前端池的總體承載量來抗峯值。
B:靜態化
將活動頁面上的全部能夠靜態的元素所有靜態化,並儘可能減小動態元素。經過CDN來抗峯值。
C:限流
通常都會採用IP級別的限流,即針對某一個IP,限制單位時間內發起請求數量。
或者活動入口的時候增長遊戲或者問題環節進行消峯操做。
D:有損服務
最後一招,在接近前端池承載能力的水位上限的時候,隨機拒絕部分請求來保護活動總體的可用性。
二、後端
那麼後端的數據庫在高併發和超賣下會遇到什麼問題呢?主要會有以下3個問題:(主要討論寫的問題,讀的問題經過增長cache能夠很容易的解決)
I:首先MySQL自身對於高併發的處理性能就會出現問題,通常來講,MySQL的處理性能會隨着併發thread上升而上升,可是到了必定的併發度以後會出現明顯的拐點,以後一路降低,最終甚至會比單thread的性能還要差。
II:其次,超賣的根結在於減庫存操做是一個事務操做,須要先select,而後insert,最後update -1。最後這個-1操做是不能出現負數的,可是當多用戶在有庫存的狀況下併發操做,出現負數這是沒法避免的。
III:最後,當減庫存和高併發碰到一塊兒的時候,因爲操做的庫存數目在同一行,就會出現爭搶InnoDB行鎖的問題,致使出現互相等待甚至死鎖,從而大大下降MySQL的處理性能,最終致使前端頁面出現超時異常。
針對上述問題,如何解決呢? 咱們先看眼淘寶的高大上解決方案:
I: 關閉死鎖檢測,提升併發處理性能。
II:修改源代碼,將排隊提到進入引擎層前,下降引擎層面的併發度。
III:組提交,下降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,可是訂單數並無達到庫存閥值。
3、總結
一、前端三板斧【擴容】【限流】【靜態化】
二、後端兩條路【內存】+【排隊】
4、非技術感想
一、團隊的力量是無窮的,各類各樣的解決方案(先不談可行性)都是討論出來的。咱們須要讓全部人都發出本身的聲音,不要着急去否認。
二、優化須要從總體層面去思考,不要只糾結於本身負責的部分,若是隻盯着一個點思考,最後極可能就走進死衚衕中了。
三、有不少東西覺得讀過了就懂了,其實否則。依然仍是須要實踐,不然別人的知識永遠不可能變成本身的。
四、多思考爲何,會發生什麼,不要想固然。只有這樣才能深刻進去,而不是留在表面。