[原創]商城系統下單庫存管控系列雜記(二)(併發安全和性能部分延伸)

 
商城系統下單庫存管控系列雜記(二)(併發安全和性能部分延伸)
 
 
前言
 
參與過幾箇中小型商城系統的開發,隨着時間的增加,以及對系統的深刻研究和測試,發現確實有不少值得推敲和商榷的地方(總有不少重要細節存在缺陷)。基於商城系統,不管規模大小,或者自己是否分佈架構,我的以爲最核心的一環就是下單模塊,而這裏面更相關和棘手的一些設計和問題,大多時候都涉及庫存系統。想一想以前跟某人的交流,他精闢點評「庫存管控作得好,系統設計就成功了一半」,本身很有認同。圍繞這個點,結合目前經驗和朋友間的交流(包括近來參閱其餘文章提到的點),閒來作些整理記錄,也許不太完整,但總歸但願能有更多啓發,本身日後也會從新揣摩。固然,文中如有不妥,歡迎指正。
 
 
正文
 
談及」下單「,就馬上想起前年參與的一個基於微信的小型商城系統,裏面下單這塊自己談不上覆雜,大概能夠這樣描述提交過程:用戶提交商品訂單,系統覈對用戶提交的訂單,校驗商品(商品價格、優惠折扣、積分等),檢測附屬信息(地址運費等),一切Pass,操做庫存(記錄/預扣),生成訂單及相關聯的明細數據。此時下單Ok,那麼後續則是等待用戶的及時付款了。
 
然而,看似如此簡單的一個流程,放在併發環境下,就暴露了足夠多的問題。深刻進去,首當其衝的就是庫存管控。包括但不限於庫存的扣減方式,如何安全操做,以及減小性能損耗等等。
 
【爲了方便獨立成文,原諒在內容排版上的一點點我的強迫症】
【本文內容由上一篇擴展論述(詳見:商城系統下單庫存管控系列雜記(一) http://www.cnblogs.com/bsfz/p/7801980.html)】
 
 
4、闡述關於併發環境中庫存管控的一些案例問題,以及涉及到的相關技術實現細節
 
庫存扣減,簡單來講,就是在對應的存儲器中(數據庫或者持久緩存)將對應商品的數量減小。
數據庫設計時,通常包含但不限於 商品主表,商品規格表,商品庫存表,商品庫存流水日誌表等等。但這裏爲了方便後續闡述,將其簡化爲一張表——商品表(PT),該表僅包含兩個字段——商品主鍵(id)和商品庫存(qty )。
 
依然以商品P舉例,其主鍵爲pid,那麼就是在下單時,將歷史庫存S修改成 S -N。具體到SQL裏,原始操做大概是這樣(以SQL SERVER 舉例):
update PT set qty = (S - N) where id = pid ;
 
這是之前的最原始的操做方式,單粒度的看,也沒什麼大礙。然而,放在一個併發環境中,則立馬暴露出諸多問題。
 
假定在同一時刻,有兩個用戶提交了訂單,同樣的操做,同樣的商品,同樣的數量。那麼最終商品P的庫存數量應該爲 S - N - N。而執行上面的SQL,由於併發,致使兩次查詢到歷史庫存均是S(應該至少有一次qty爲S - N),則更新完畢後,商品數量最終是 S - N。這種致命性的Bug,也屬於超賣(雖然不會扣爲負數),若是放在線上,簡直是一個定時炸彈,不,還不只僅只是這一個定時炸彈。
 
圍繞解決這樣的問題,考慮到併發安全以及併發性能,產生了各類解決方案。大致基於兩種機制:悲觀鎖和樂觀鎖。在諸多場景裏,基於每種鎖,都有配套的輔助手段,以及各自不一樣的側重取捨和相關實現。
 
 
4.1 使用悲觀鎖的理念,實際就是在併發的關鍵地方,強制將「相似並行」改成串行,相關的一些處理方式:
 
4.1.1  數據庫鎖,利用數據庫的自身的事務隔離機制(Isolation),進行排他操做。
 
       4.1.1.1
  極端的在查詢時,直接開啓事務設置行鎖(rowlock)。串行目的是達到了,但即時在單機系統中,也沒法承受巨大的性能損耗。而且最終的超賣問題也沒有解決,很是不推薦。
 
4.1.1.2
  僅利用數據庫在update時形成的排他鎖,使真實更新時串行,並增長庫存判斷,若庫存發生變更,則更新無效,超賣問題也不會發生。譬如(以SQL SERVER 舉例):
  update PT set qty = qty - N  where id = pid and qty >= N;
 
  嚴格來說,這依然是一個較粗的粒度,但不得不說,在單機環境下有必定的可行性。同時,須要考慮高併發狀況下(例如商戶舉辦活動,同時參與用戶過多)存在必定性能瓶頸,數據庫IO負載過大。此時須要結合其餘方案,包括增長上層緩存層等。甚至部分場景須要單獨設計一套流程(例如秒殺搶購場景,首先就是應用到隊列,不然網站可能沒崩潰在併發請求數上,而是直接掛在了DB上,後面會有相關闡述)
 
4.1.2  使用程序鎖(單機線程鎖和分佈式調度鎖),使部分關鍵代碼串行。
 
4.1.2.1
  極端的直接使用程序自帶的全局線程鎖,以.NET Framewok 舉例,裏面有各級粒度的鎖,經常使用的輕量鎖有lock(Mointor語法糖)、SpinLock(自旋鎖)。使用它們,最先大概是應用在「單例模式」的構建,原理自己不復雜,使用也方便,而且也達到了串行的目的。
  然而,放在下單庫存管控這裏,串行的倒是全部用戶進行任意商品下單操做,打擊面太大(甚至直接上升到全面打擊),對性能形成極大影響,不可行,不過多延伸,也不推薦。(曾經優化一箇舊項目裏的模塊,初步Review代碼時就發現了幾處不經意的地方竟直接使用了這種寫法,而開發人員仍是兩名老員工)。
 
4.1.2.2
  構建一個本地的線程鎖管理器(這裏稱爲LockerManage),統一分配鎖對象(等待對象)。其本質是針對上面4.1.2.1方式的包裝處理,實現相似「工廠模式」的機制。主要是經過它來生產具備惟一特徵的Object對象,這個對象將會做爲鎖對象資源返回給Monitor等調用,並具備必定的使用時效,每次生成後保存在內部的線程安全的集合裏,同時具備自動銷燬機制(運行一個獨立線程,定時檢查清理)。其中有個小細節,爲了優化管理器內部的併發問題,開始使用的是.NET Framewok 裏自帶的線程安全的字典集合(ConcurrentDictionary),後來經測試,發現併發處理並不理想,後面便換了其餘方案(讀寫分離)。迴歸到下單這裏,這裏依然以商品P爲例,首先調用LockerManage,獲取一個以當前商品主鍵爲標識的Object對象,而後在庫存的預扣覈對時,使用Mointor加鎖處理。(固然,這裏是本機鎖,後續有說明)。這種方式對比數據庫鎖,則是下降數據庫的操做,而將壓力大部分轉移到了程序上,但相對能夠更靈活的去操控。
 
4.1.2.3
  使用分佈式鎖。上面的普通程序鎖做爲單機的存在,決定了其在分佈式架構上的不可控性,而這時就有了分佈式調度鎖。它主要是爲了方便解決分佈式狀況下,在多個Web程序內實現併發線程的一個管控。值得一提的是,這個「輪子」並不須要手動從新創造,目前市面上已經有相對成熟的解決方案,如利用Zookeeper和Redis。在AutumnBing項目中,當時選擇的是Redis,使用的驅動庫是StackExchange.Redis。(後續聽到朋友提到Zookeeper更適合充當這樣的角色,但因爲目前本身尚未太多涉獵研究,暫時持保留態度)。固然,純粹採用分佈式鎖,天然調用性能會有更多損耗。而相對更合理的作法,是結合單機鎖搭配應用(試討論,分佈式鎖放置外層,單機鎖放置內部,每一個站點各自維護)。
 
 
4.2  遵循樂觀鎖的理念,則是默許不會有太大的併發問題(聚焦在小粒度的商品P上,則是認爲大多數狀況下P不會被同時消費),「聽任」線程的執行,不作管控。可是會在關鍵地方進行版本覈對,假如失敗,則內部重試或拋出失敗信號。
 
 
4.2.1  數據庫層面上,增長顯式的版本號字段(ver)。
 
  購買商品P,下單這裏須要獲取到當前時刻對應的庫存qty01,當前記錄是版本ver01,而後在真實更新時,再次查詢商品P的庫存,以及對應的當前的版本ver02,若是 ver01 == ver02,那麼能夠更新。不然,當前數據已因併發被修改,沒法更新。這更像是數據庫的「不可重複讀」,而出現這種狀況後(高併發狀況下,出現機率直線上升),必須附有關聯的內部嘗試機制(注意保證冪等性)。 這是一種實現併發管控的方案,但只適合存在併發,但併發量不太大的狀況,不然,一是違背樂觀鎖的理念初衷,二是總體性能以及體驗會大打折扣。
 
 
4.2.2  程序控制上,採起隊列(queue)方式,進行相對集中化預受理,而後分發逐個處理。
 
  須要聲明,這裏自己執行原理,其實質依然離不開相似悲觀鎖的管控性質,一是入隊時須要有個小粒度的鎖機制保證串行(固然也能夠是其餘方式,這是隊列內部的管控機制之一),二是出隊,例如分發到不一樣服務上去處理,最終也是一個一個在操做更新(依然是某種程度上的串行)。可是,做爲用戶下單的提交,自己是保證了樂觀的態度,一股腦「同時」或者「快速」接收,而後再考慮如何告知處理。
 
   因爲單機隊列的應用,會出現更多相似上面單機鎖的一些額外問題,這裏不推薦(固然你能夠結合),也不作擴展說明。下面僅就分佈式隊列在大方向上舉例闡述。
 
  如何採用分佈式隊列來實現下單以及庫存管控呢?依然以商品P爲例,用戶同時購買商品P,自己是一個併發操做,可是咱們能夠將一系列的請求商品扣減數據Push到一個隊列中(生產者開始生產),而後由專門的線程進行訂閱消費(消費者開始消費)。暫且假定爲一個線程在消費,那麼該線程具體消費時,逐個將商品數據出隊,進行庫存扣減,這裏必然不會出現併發。消費完畢,不管扣除庫存邏輯上是成功仍是失敗,均給出一個應答(ACK)。注意這裏並無過多的拆分邏輯,而是將下單的一些操做扔進一個隊列中,使用專門的程序去逐個或者逐幾個(分批)處理。實際使用每每是根據業務,作更小粒度的拆分和調整。另外,關於技術框架選型,目前各種開源成熟的MQ項目比比皆是,我的圈子裏瞭解到最多的仍是 RabbitMQ,對於多個生產者以及與之配合的多個消費者,還有應答處理機制,包括自己的性能和高可用性,均極其出色。額外的,關於web前端,不少時候則是須要配合一些輪詢機制來檢查訂單狀態(固然,輪詢這裏也有一些具體細節,好比異步體驗、輪詢時長和狀態重置等考慮)
 
 
 
5、涉及到分佈式SOA架構體系(包括現在基於SOA開始流行的微服務架構)狀況下的一些額外考慮。
 
首先聲明,我的認爲SOA只是一種架構上的抽離設計,自己與論述的庫存管控沒有直接關係。但這裏以庫存管控爲例,也有須要額外考慮的地方。
 
咱們假定在一個下單API中,包含了3個獨立的API接口:A-積分扣減API,B-優惠券扣減API,C-庫存扣減API。考慮一種狀況:假定庫存自己能夠被合法扣除,而且執行C成功了,可是發生了其餘問題,A或者B執行失敗了,那庫存該如何回滾。
 
必須糾正的是,在這樣一個耦合性系統場景裏(而上例僅是其中一種案例),須要解決的問題本質和庫存如何扣減沒有絲毫直接關係,其暴露的實質問題是如何實現一個分佈式事務機制。這是一個比較大的專題,實現相對複雜,開發成本也足夠高。基於單一RPC接口,到現在流行的更小粒度的微服務,都足夠寫一本書了。截止目前我的的瞭解,如早期的2PC (兩階段)、3PC(三階段)、TCC(補償事務),以及後來的純消息列表式方案等等,均是一些沒法達到完美的理論(性能、時效、複雜度等)。至於實踐上,天然就沒有絕對OK的方案,只能根據項目規模和實際業務作些取捨,最終獲得一個儘可能知足的「高可用」方案。之後待到經驗足夠,有機會嘗試一下單獨開篇討論。(對於分佈式事務,寫過一些demo,卻應用不深,之後會考慮抽個專門的時間在續篇中嘗試撰寫探討)。 
 
 
 
6、結合高併發場景(如:秒殺活動),簡單聊聊如何關聯各種技術手段,進行下單及庫存管控的應用。
 
在電商系統裏,併發簡直無處不在,目前較爲突出的一個場景,則是秒殺活動。所謂秒殺,最簡單直觀的場景以下:在某個時刻,商品P開放購買(P的實際庫存僅爲1個或者幾個),大批量的用戶同時進行下單搶購。
 
秒殺時併發量之大遠遠超過通常狀況下的併發(你要考慮到不止一個商品),甚至還會影響到商城裏現有其餘業務(這裏討論非獨立部署)。須要考慮諸多細節,以及大量技術手段來進行有效管控。如下簡單聊聊後臺下單相關問題,不討論其餘前端處理技術,包括定時查詢,頁面靜態化,網絡帶寬優化等。
 
6.1  明確業務本質需求,脫離業務,固然談不了任何技術架構和實現方案。
 
  秒殺的業務場景,宏觀上來講,就是一個典型的排序模型。誰先來,誰先獲得。這裏咱們儘可能簡化舉例:假定商品P庫存爲10,同時參與下單的用戶數爲100000。那麼,最終只有開始的(理論上的)10個用戶購買成功,其他99990個用戶購買失敗。商品庫存被成功消費爲0。
 
6.2  防做弊等安全監測,從RPC的第一個接口開始,就進行過濾。
 
  例如,在雜記上一篇中提到的(見第一篇主題三),作好基礎的安全監測機制。如相同IP的殭屍帳號,作限制IP的訪問,並增長驗證碼等。同時,包括但不限於一些額外的業務輔助手段,如限制僅知足必定註冊時間的用戶可下單等。
 
6.3  限流機制,在外層計數,達到一個下單閾值,直接拋棄。
 
  從6.1中就能夠發現,秒殺業務自己就註定了大部分人是搶不到的,那麼針對大部分人的下單請求,徹底就能夠不作處理(直接拋棄)。在進行真正的下單操做以前,可在具體操做接口上,增長一個攔截計數器來統計,好比當計數超過3000時,後續下單直接返回搶購失敗的信息。這樣就將數據處理由大化小了,實現了限流(僅針對下單)。固然,具體實現時,這個3000名額推薦是篩選後的。好比,先過濾8000,從中隨機抽取3000(這裏不擴展)。
 
6.4  從數據庫角度,首先就是要增長單獨的臨時緩存層。 
 
  即便是3000的量,在這個環節也確定是不能直接操做數據庫的(你要明白,實際秒殺的商品,不僅一個),直接讀庫寫庫對數據庫壓力太大,甚至直接負載過大致使數據庫掛掉。那麼,針對這種狀況,推薦的一種方案就是結合緩存來操做。譬如:把商品P * 10 這條數據提早Push到專門的緩存中,而後每次讀取和更新,均是走的該緩存。這裏額外提到一點,若是用戶下單成功,預扣庫存 -1,但又未進行安全時間內的支付,那麼系統將自動回滾商品P的庫存,進行 +1(固然,回滾一樣須要協調處理併發)。
 
6.5  從程序角度,修改庫存依然須要保證必定串行。
 
  首先,若是保證DAL的串行,能夠是數據庫上鎖,也能夠是程序上鎖(或者隊列)。但若是直接數據庫上鎖,諸多併發請求(依然考慮到,單時間內的多個商品被多用戶搶購),即便前面削減了部分下單處理,數據庫的I/O負載依然會很嚴重。那麼,首先就是推薦樂觀進隊列,而後悲觀進分佈式程序鎖,混合處理(便是對主題四的結合應用)。
 
 
 
結語
 
電商項目裏,幾乎到處是併發,不管是單機仍是分佈式架構。結合下單庫存管控相關,咱們能夠深入理解解決這些併發性能問題和併發安全顧慮,即便是同一類型的業務,也有諸多方案,每種方案都有一些細粒度的問題須要嘗試克服,更需結合實際項目(具體業務性質和規模),作一些實現上的各類優化與權衡等。
 
 
[不知不覺又是凌晨兩點多了,本文做爲系列第二篇雜記(部分延伸篇),暫告一段落吧。第三篇,待續。該睡了,晚安。]
 
 
 
End.
相關文章
相關標籤/搜索