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

来源:http://www.cnblogs.com/bsfz/archive/2017/11/13/7824428.html
-Advertisement-
Play Games

【為了方便獨立成文,原諒在內容排版上的一點點個人強迫症】 【本文內容由上一篇擴展論述(詳見:商城系統下單庫存管控系列雜記(一) http://www.cnblogs.com/bsfz/p/7801980.html)】 四、闡述關於併發環境中庫存管控的一些案例問題,以及涉及到的相關技術實現細節 ... ...


  商城系統下單庫存管控系列雜記(二)(併發安全和性能部分延伸)     前言   參與過幾個中小型商城系統的開發,隨著時間的增長,以及對系統的深入研究和測試,發現確實有很多值得推敲和商榷的地方(總有很多重要細節存在缺陷)。基於商城系統,無論規模大小,或者本身是否分佈架構,個人覺得最核心的一環就是下單模塊,而這裡面更相關和棘手的一些設計和問題,大多時候都涉及庫存系統。想想之前跟某人的交流,他一句“庫存管控做得好,系統設計就成功了一半”,自己頗有認同。圍繞這個點,結合目前經驗和朋友間的交流(包括近來參閱其他文章提到的點),閑來做些整理記錄,也許不太完整,但總歸希望能有更多啟發,自己往後也會重新揣摩。當然,文中若有不妥,歡迎指正。     正文   談及”下單“,就立刻想起前年參與的一個基於微信的小型商城系統,裡面下單這塊本身談不上複雜,大概可以這樣描述提交過程:用戶提交商品訂單,系統核對用戶提交的訂單,校驗商品(商品價格、優惠折扣、積分等),檢測附屬信息(地址運費等),一切Pass,操作庫存(記錄/預扣),生成訂單及相關聯的明細數據。此時下單Ok,那麼後續則是等待用戶的及時付款了。   然而,看似如此簡單的一個流程,放在併發環境下,就暴露了足夠多的問題。深入進去,首當其衝的就是庫存管控。包括但不限於庫存的扣減方式,如何安全操作,以及減少性能損耗等等。     【為了方便獨立成文,原諒在內容排版上的一點點個人強迫症】 【本文內容由上一篇擴展論述(詳見:商城系統下單庫存管控系列雜記(一) http://www.cnblogs.com/bsfz/p/7801980.html)】     四、闡述關於併發環境中庫存管控的一些案例問題,以及涉及到的相關技術實現細節   庫存扣減,簡單來說,就是在對應的存儲器中(資料庫或者持久緩存)將對應商品的數量減少。 資料庫設計時,一般包含但不限於 商品主表,商品規格表,商品庫存表,商品庫存流水日誌表等等。但這裡為了方便後續闡述,將其簡化為一張表——商品表(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前端,很多時候則是需要配合一些輪詢機制來檢查訂單狀態(當然,輪詢這裡也有一些具體細節,比如非同步體驗、輪詢時長和狀態重置等考慮)       五、涉及到分散式SOA架構體系(包括如今基於SOA開始流行的微服務架構)情況下的一些額外考慮。   首先聲明,個人認為SOA只是一種架構上的抽離設計,本身與論述的庫存管控沒有直接關係。但這裡以庫存管控為例,也有需要額外考慮的地方。   我們假定在一個下單API中,包含了3個獨立的API介面:A-積分扣減API,B-優惠券扣減API,C-庫存扣減API。考慮一種情況:假定庫存本身可以被合法扣除,並且執行C成功了,但是發生了其他問題,A或者B執行失敗了,那庫存該如何回滾。   必須糾正的是,在這樣一個耦合性系統場景里(而上例僅是其中一種案例),需要解決的問題本質和庫存如何扣減沒有絲毫直接關係,其暴露的實質問題是如何實現一個分散式事務機制。這是一個比較大的專題,實現相對複雜,開發成本也足夠高。基於單一RPC介面,到如今流行的更小粒度的微服務,都足夠寫一本書了。截止目前個人的瞭解,如早期的2PC (兩階段)、3PC(三階段)、TCC(補償事務),以及後來的純消息列表式方案等等,均是一些無法達到完美的理論(性能、時效、複雜度等)。至於實踐上,自然就沒有絕對OK的方案,只能根據項目規模和實際業務做些取捨,最終得到一個儘量滿足的“高可用”方案。以後待到經驗足夠,有機會嘗試一下單獨開篇討論。(對於分散式事務,寫過一些demo,卻應用不深,以後會考慮抽個專門的時間在續篇中嘗試撰寫探討)。        六、結合高併發場景(如:秒殺活動),簡單聊聊如何關聯各類技術手段,進行下單及庫存管控的應用。   在電商系統里,併發簡直無處不在,目前較為突出的一個場景,則是秒殺活動。所謂秒殺,最簡單直觀的場景如下:在某個時刻,商品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.        
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 題目內容: 寫一個將華氏溫度轉換成攝氏溫度的程式,轉換的公式是: °F = (9/5)*°C + 32 其中C表示攝氏溫度,F表示華氏溫度。 程式的輸入是一個整數,表示華氏溫度。輸出對應的攝氏溫度,也是一個整數。 提示,為了把計算結果的浮點數轉換成整數,需要使用下麵的表達式: (int)x; 其中x ...
  • druapl 的核心可能會有漏洞,這時就需要我們去打補丁。很多補丁都已經有人寫好了,我這裡講的就是如何去打這些已經寫好的補丁。 對於這個問題:drupal8 核心有bug導致了兩個相同的錯誤提示的出現 1.打開項目最外層中的composer.json文件 2.確保 "enable-patching" ...
  • 1.查找文件find / -name filename.txt 根據名稱查找/目錄下的filename.txt文件。find . -name “*.xml” 遞歸查找所有的xml文件2.查看一個程式是否運行ps –ef|grep tomcat 查看所有有關tomcat的進程3.終止線程kill -9 ...
  • 在項目開發裡面,我遇到了這麼一個需求,就是對於node的title欄位,編輯內容的角色不允許對title進行編輯。title欄位是創建內容類型時自動生成的欄位,不能在drupal8後臺直接配置許可權,所以我需要用代碼自定義一個許可權。 1.在/modules/custom下自定義一個模塊,我的模塊名為o ...
  • Hadoop生態大數據系統分為Yam、 HDFS、MapReduce計算框架。TensorFlow分散式相當於MapReduce計算框架,Kubernetes相當於Yam調度系統。TensorFlowOnSpark,利用遠程直接記憶體訪問(Remote Direct Memory Access,RDM ...
  • PHP5以上提供了一個simpleXML對象來操作XML,把XML的節點轉換成對象和數組去操作。 ...
  • 在學習bootstrap的路上,需要使用roots主題,而roots是屬於wordpress的一個主題,那也開始了wordpress的探索~ 首先,使用wordpress我們需要一個必要的環境:PHP+Apache+Mysql。這裡我用的是集成環境,沒必要非獨立安裝。根據自己的操作系統自行下載即可~ ...
  • 1、初步認識 觀察者模式的定義: 在對象之間定義了一對多的依賴,這樣一來,當一個對象改變狀態,依賴它的對象會收到通知並自動更新。 大白話: 其實就是發佈訂閱模式,發佈者發佈信息,訂閱者獲取信息,訂閱了就能收到信息,沒訂閱就收不到信息。 2、這個模式的結構圖 3、可以看到,該模式包含四個角色 抽象被觀 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...