$goods->query('update order set = store- num where store>=num and goodID = 12345');
$goods->query('update order set = store- num where store>=num and goodID = 12345');
一、扣減庫存的三種方案
(1)下單減庫存(秒殺商品這種方式最好)
用戶下單(確認訂單)時減庫存
優點:實時減庫存,避免付款時因庫存不足減庫存的問題
缺點:惡意買家大量下單,將庫存用完,但是不付款,真正想買的人買不到
(2)付款后減庫存(支付成功回調時減庫存)
下單頁面顯示最新的庫存,下單時不會立即減庫存,而是等到支付時才會減庫存。
優點:防止惡意買家大量下單用光庫存,避免下單減庫存的缺點
缺點:下單頁面顯示的庫存數可能不是最新的庫存數,而庫存數用完后,下單頁面的庫存數沒有刷新,出現下單數超過庫存數,若支付的訂單數超過庫存數,則會出現支付失敗。(其他用戶可能提示庫存不足,可能出現超賣問題) 因第三方支付返回結果存在時差,同一時間多個用戶同時付款成功,會導致下單數目超過庫存,商家庫存不足容易引發斷貨和投訴,成本增加
(3)預扣庫存(確認訂單之后與支付之間保留庫存,調起支付界面前鎖定庫存, 業務系統中最常見的就是預扣庫存方案)
下單頁面顯示最新的庫存,下單后保留這個庫存一段時間(比如10分鍾),超過保留時間后,庫存釋放。若保留時間過后再支付,如果沒有庫存,則支付失敗。例如:要求30分鍾內支付訂單。(①.支付前預占庫存,②.限制支付時間:若訂單創建成功N分鍾不付款,則訂單取消,庫存回滾,③.檢測惡意下單用戶加入到店鋪黑名單,④.加入限購):設置了“付款減庫存”后,買家拍下商品待付款,這是系統會預扣庫存出來,就叫“預扣庫存”。若買家30分鍾后依然未付款,預扣庫存釋放,回增到總庫存里。若買家30分鍾內付款了,庫存就真正被扣除 ;存在預扣庫存時,無法編輯商品庫存,也無法更改“庫存計數”的設置
預扣庫存須知:

1、問:為什么我在商品發布頁面看到了預扣庫存? 答:預扣庫存是指付款減庫存商品被拍下30分鍾未付款的數量。 2、問:為什么我有的商品發布頁面有預扣庫存數量設置,有的又沒有? 答:目前該功能在測試期間,只針對部分商品開通此功能。 3、問:我修改商品數量的時候,為什么報錯“不能低於預扣庫存”? 答:為了保障已經拍下寶貝的買家在30分鍾內付款能夠買到商品,不允許賣家修改商品數量小於預扣庫存數量。 4、問:為什么我發布的商品數和寶貝頁面看到的可售數量不一樣? 答:寶貝頁面展示的可售數量=賣家維護的商品數量-預扣庫存數量。 5、問:為什么我要刪除部分sku庫存無法刪除? 答:該寶貝有未付款訂單,請聯系買家進行支付或者取消訂單后才能刪除sku庫存。
高並發下減庫存操作避免超賣:
1:事務+行鎖(悲觀鎖)
for update:這是數據庫行鎖,也是我們常用的悲觀鎖,可用於針對某商品的秒殺操作,但是當出現主鍵索引和非主鍵索引同時等待對方時,會造成數據庫死鎖
select * from goods where ID=1 for update
for update 僅適用於InnoDB,並且必須開啟事務,在begin與commit之間才生效
2:設置無符號:性能是1種的三倍 try sql語句 當庫存不夠時catch捕捉錯誤 返回數量不足
3:樂觀鎖:先比較在更新
方式1:case:該方式在庫存小於購買商品數量時會冗余的更新一遍庫存且不能正確獲取是否扣減成功,所以還得查詢一次數據庫:
UPDATE goods SET store = CASE WHEN store>= num THEN store-num ELSE store END
方式2:where語句添加條件判斷:性能又略微優於設置無符號
UPDATE goods SET store = store-num where id=1 and store>num
數據表上設計一個數據更新字段作遞增的版本號, 每次修改時修改數據版本號或者時間戳;修改數據的時候首先把這條數據的版本號查出來,update時判斷這個版本號是否和數據庫里的一致,如果一致則表明這條數據沒有被其他用戶修改,若不一致則表明這條數據在操作期間被其他客戶修改過,此時需要在代碼中拋異常或者回滾等
update tb set name='yyy' and version=version+1 where id=1 and version=version;
1. SELECT name AS old_name, version AS old_version FROM tb where ...; 2. 根據獲取的數據進行業務操作,得到new_name和new_version 3. UPDATE SET name = new_name, version = new_version WHERE version = old_version if (updated row > 0) { // 樂觀鎖獲取成功,操作完成 } else { // 樂觀鎖獲取失敗,回滾並重試 }
4:阻塞隊列
請求過來的時候放到固定大小的阻塞隊列,請求結束后或者秒殺時間結束后,遍歷隊列,做減操作,這需要顯示購買數量,因為100個請求不等於100個庫存
建議采用無符號加樂觀鎖
優點:結合下單減庫存的優點,實時減庫存,且緩解惡意買家大量下單的問題,保留時間內未支付,則釋放庫存。
缺點:保留時間內,惡意買家大量下單將庫存用完。並發量很高的時候,依然會出現下單數超過庫存數。
二、如何解決惡意買家下單的問題
這里的惡意買家指短時間內大量下單,將庫存用完的買家。
(1)限制用戶下單數量
優點:限制惡意買家下單
缺點:用戶想要多買幾件,被限制了,會降低銷售量
(2)標識惡意買家
優點:賣家設定一個備用庫存,當支付時,庫存已用完,扣減備用庫存數,這就是常見的補貨場景
缺點:因高並發場景下,數據可能存在不一致性的問題
三、如何解決下單成功而支付失敗(庫存不足)的問題
(1)備用庫存
商品庫存用完后,如果還有用戶支付,直接扣減備用庫存。
優點:緩解部分用戶支付失敗的問題
缺點:備用庫存只能緩解問題,不能從根本上解決問題。另外備用庫存針對普通商品可以,針對特殊商品這種庫存少的,備用庫存量也不會很大,還是會出現大量用戶下單成功卻因庫存不足而支付失敗的問題。
四、如何解決高並發下庫存超賣的場景
庫存超賣最簡單的解釋就是多成交了訂單而發不了貨。
場景:
用戶A和B成功下單,在支付時扣減庫存,當前庫存數為10。因A和B查詢庫存時,都還有庫存數,所以A和B都可以付款。
A和B同時支付,A和B支付完成后,可以看做兩個請求回調后台系統扣減庫存,有兩個線程處理請求,兩個線程查詢出來的庫存數 inventory=10,
然后A線程更新最終庫存數 lastInventory=inventory - 1 = 9,
B線程更新庫存數 lastInventory=inventory - 1 = 9。
而實際最終的庫存應是8才對,這樣就出現庫存超賣的情況,而發不出貨。
那如何解決庫存超賣的情況呢?
1.SQL語句更新庫存時,如果扣減庫存后,庫存數為負數,直接拋異常,利用事務的原子性進行自動回滾。
2.利用SQL語句更新庫存,防止庫存為負數
UPDATE [庫存表] SET 庫存數 - 1 WHERE 庫存數 - 1 > 0
五、秒殺場景下如何扣減庫存
(1)下單減庫存
因秒殺場景下,大部分用戶都是想直接購買商品的,可以直接用下單減庫存。
大量用戶和惡意用戶都是同時進行的,區別是正常用戶會直接購買商品,惡意用戶雖然在競爭搶購的名額,但是獲取到的資格和普通用戶一樣,所以下單減庫存在秒殺場景下,惡意用戶下單並不能造成之前說的缺點。
而且下單直接扣減庫存,這個方案更簡單,在第一步就扣減庫存了。
(2)將庫存放到redis緩存中
查詢緩存要比查詢數據庫快,所以將庫存數放在緩存中,直接在緩存中扣減庫存。然后在通過MQ異步完成數據庫處理。
(3)使用量自增方式
可以先增加已使用量,然后與設定的庫存進行比較,如果超出,則將使用量減回去。
項目中用到了很多機制,但是沒有總結出來,學習架構需要不斷地總結。
六 第三方支付
1 支付成功多次回調:把減庫存放在微信支付的成功回調URL的方法里面。但是微信支付成功之后微信支付平台會發送8次請求到回調地址。這樣的做法就會導致庫存減少,一定要驗證返回的編號是否已經完成扣減。
避免超賣:
主要就是保證大並發請求時庫存數據不能為負數,也就是要保證數據庫中的庫存字段值不能為負數,一般我們有多種解決方案:一種是在應用程序中通過事務來判斷,即保證減后庫存不能為負數,否則就回滾;另一種辦法是直接設置數據庫的字段數據為無符號整數,這樣減后庫存字段值小於零時會直接執行SQL語句來報錯;再有一種就是使用CASE WHEN判斷語句,例如這樣的SQL語句:
UPDATE item SET store = CASE WHEN store>= num THEN store-num ELSE store END
秒殺庫存處理:
秒殺前將庫存放緩存中(redis,mamecache) (沒有復雜的SKU庫存和總庫存這種聯動關系)SKU庫存:比如淘寶商品的屬性,顏色,尺寸等沒個屬性對應的商品數量都不同
$store=10;//庫存10個
for($i=0;$i<$store;$i++){
$redis->lpush('goods_store',1);
}
秒殺活動開始后:
$res=$redis->lpop('goods_store');//移除並返回列表
if($res){
$where['g_id'] =1;
$res=$pdo->table('good')->where($where)->setDec('goods_store',1);
}else{
echo '秒殺失敗';
}
秒殺復雜的SKU庫存方案:redis hash能解決嗎 或者事務處理
其他問題:

由於MySQL存儲數據的特點,同一數據在數據庫里肯定是一行存儲(MySQL),因此會有大量線程來競爭InnoDB行鎖,而並發度越高時等待線程會越多,TPS(Transaction Per Second,即每秒處理的消息數)會下降,響應時間(RT)會上升,數據庫的吞吐量就會嚴重受影響。 這就可能引發一個問題,就是單個熱點商品會影響整個數據庫的性能, 導致0.01%的商品影響99.99%的商品的售賣,這是我們不願意看到的情況。一個解決思路是遵循前面介紹的原則進行隔離,把熱點商品放到單獨的熱點庫中。但是這無疑會帶來維護上的麻煩,比如要做熱點數據的動態遷移以及單獨的數據庫等。 而分離熱點商品到單獨的數據庫還是沒有解決並發鎖的問題,我們應該怎么辦呢?要解決並發鎖的問題,有兩種辦法: 應用層做排隊。按照商品維度設置隊列順序執行,這樣能減少同一台機器對數據庫同一行記錄進行操作的並發度,同時也能控制單個商品占用數據庫連接的數量,防止熱點商品占用太多的數據庫連接。 數據庫層做排隊。應用層只能做到單機的排隊,但是應用機器數本身很多,這種排隊方式控制並發的能力仍然有限,所以如果能在數據庫層做全局排隊是最理想的。阿里的數據庫團隊開發了針對這種MySQL的InnoDB層上的補丁程序(patch),可以在數據庫層上對單行記錄做到並發排隊。 你可能有疑問了,排隊和鎖競爭不都是要等待嗎,有啥區別? 如果熟悉MySQL的話,你會知道InnoDB內部的死鎖檢測,以及MySQL Server和InnoDB的切換會比較消耗性能,淘寶的MySQL核心團隊還做了很多其他方面的優化,如COMMIT_ON_SUCCESS和ROLLBACK_ON_FAIL的補丁程序,配合在SQL里面加提示(hint),在事務里不需要等待應用層提交(COMMIT),而在數據執行完最后一條SQL后,直接根據TARGET_AFFECT_ROW的結果進行提交或回滾,可以減少網絡等待時間(平均約0.7ms)。據我所知,目前阿里MySQL團隊已經將包含這些補丁程序的MySQL開源。 另外,數據更新問題除了前面介紹的熱點隔離和排隊處理之外,還有些場景(如對商品的lastmodifytime字段的)更新會非常頻繁,在某些場景下這些多條SQL是可以合並的,一定時間內只要執行最后一條SQL就行了,以便減少對數據庫的更新操作。
超級詳細的博客: https://blog.csdn.net/qq_33862644/article/details/79434146
問題1、按你的架構,其實壓力最大的反而是服務端,假設真實有效的請求數有1000萬,不太可能限制請求連接數吧,那么這部分的壓力怎么處理?
答:每秒鍾的並發可能沒有1kw,假設有1kw,解決方案2個:
(1)服務層(web服務器)是可以通過加機器擴容的,最不濟1k台機器來唄。
(2)如果機器不夠,拋棄請求,拋棄50%(50%直接返回稍后再試),原則是要保護系統,不能讓所有用戶都失敗。
問題2、“控制了10w個肉雞,手里有10w個uid,同時發請求” 這個問題怎么解決哈?
答:上面說了,服務層(web服務器)寫請求隊列控制
問題3:限制訪問頻次的緩存,是否也可以用於搜索?例如A用戶搜索了“手機”,B用戶搜索“手機”,優先使用A搜索后生成的緩存頁面?
答:這個是可以的,這個方法也經常用在“動態”運營活動頁,例如短時間推送4kw用戶app-push運營活動,做頁面緩存。
問題4:如果隊列處理失敗,如何處理?肉雞把隊列被撐爆了怎么辦?
答:處理失敗返回下單失敗,讓用戶再試。隊列成本很低,爆了很難吧。最壞的情況下,緩存了若干請求之后,后續請求都直接返回“無票”(隊列里已經有100w請求了,都等着,再接受請求也沒有意義了)
問題5:服務端過濾的話,是把uid請求數單獨保存到各個站點的內存中么?如果是這樣的話,怎么處理多台服務器集群經過負載均衡器將相同用戶的響應分布到不同服務器的情況呢?還是說將服務端的過濾放到負載均衡前?
答:可以放在內存,這樣的話看似一台服務器限制了5s一個請求,全局來說(假設有10台機器),其實是限制了5s 10個請求,解決辦法:
1)加大限制(這是建議的方案,最簡單)
2)在nginx層做7層均衡,讓一個uid的請求盡量落到同一個機器上
問題6:服務層(web服務器)過濾的話,隊列是服務層(web服務器)統一的一個隊列?還是每個提供服務的服務器各一個隊列?如果是統一的一個隊列的話,需不需要在各個服務器提交的請求入隊列前進行鎖控制?
答:可以不用統一一個隊列,這樣的話每個服務透過更少量的請求(總票數/服務個數),這樣簡單。統一一個隊列又復雜了。
問題7:秒殺之后的支付完成,以及未支付取消占位,如何對剩余庫存做及時的控制更新?
答:數據庫里一個狀態,未支付。如果超過時間,例如45分鍾,庫存會重新會恢復(大家熟知的“回倉”),給我們搶票的啟示是,開動秒殺后,45分鍾之后再試試看,說不定又有票喲~
問題8:不同的用戶瀏覽同一個商品 落在不同的緩存實例顯示的庫存完全不一樣 請問老師怎么做緩存數據一致或者是允許臟讀?
答:目前的架構設計,請求落到不同的站點上,數據可能不一致(頁面緩存不一樣),這個業務場景能接受。但數據庫層面真實數據是沒問題的。
問題9:就算處於業務把優化考慮“3k張火車票,只透3k個下單請求去db”那這3K個訂單就不會發生擁堵了嗎?
答:(1)數據庫抗3k個寫請求還是ok的;(2)可以數據拆分;(3)如果3k扛不住,服務層(web服務器)可以控制透過去的並發數量,根據壓測情況來吧,3k只是舉例;
問題10;如果在服務端或者服務層(web服務器)處理后台失敗的話,需不需要考慮對這批處理失敗的請求做重放?還是就直接丟棄?
答:別重放了,返回用戶查詢失敗或者下單失敗吧,架構設計原則之一是“fail fast”。
問題11.對於大型系統的秒殺,比如12306,同時進行的秒殺活動很多,如何分流?
答:垂直拆分
問題12、額外又想到一個問題。這套流程做成同步還是異步的?如果是同步的話,應該還存在會有響應反饋慢的情況。但如果是異步的話,如何控制能夠將響應結果返回正確的請求方?
答:用戶層面肯定是同步的(用戶的http請求是夯住的),服務層(web服務器)面可以同步可以異步。
問題13、秒殺群提問:減庫存是在那個階段減呢?如果是下單鎖庫存的話,大量惡意用戶下單鎖庫存而不支付如何處理呢?
答:數據庫層面寫請求量很低,還好,下單不支付,等時間過完再“回倉”,之前提過了。