1. 程式人生 > >大促下熱點資料寫(庫存扣減解決方案

大促下熱點資料寫(庫存扣減解決方案

0?wx_fmt=gif&wxfrom=5&wx_lazy=1

       針對交易系統大促場景下熱點資料寫優化的相關案例。當然,不同的企業有不同的解決方案和實現,但是萬變不離其宗,還是那句話,對於大型網站而言,其架構一定是簡單和清晰的,而不是炫技般的複雜化,畢竟解決問題採用最直接的方式直擊要害才是最見效的,否則事情只會變得越來越糟

   在大部分情況下,商品庫存都是直接在關係型資料庫中進行扣減,那麼在限時搶購活動正式開始後,那些單價比平時更給力、更具吸引力的熱賣商品大家肯定都會積極踴躍的參與搶購,這必然會產生大量針對資料庫同一行記錄的併發更新操作。因此資料庫為了保證原子性,InnoDB引擎預設會對同一行資料記錄進行加鎖,把前端的併發請求變成序列操作來確保資料更新時的一致性。

一、在RDBMS中扣減商品庫存

先來看看如果是直接在資料庫中扣減庫存,應該如何避免商品超賣呢?在生產環境中我們可以通過樂觀鎖機制來避免這個問題,所謂樂觀鎖,簡單來說,就是在item表中建立一個version欄位。假設某一個熱賣商品的實際庫存為n,處於效能考慮對於查詢庫存操作是不建議加for update的,那麼在併發場景下,必然會導致多個使用者拿到的stock和version都一樣。因此當第1個使用者成功扣減商品庫存後,則需要將item表中的version加1,這樣一來,當第2個使用者扣減庫存時,由於version不匹配,那麼為了提升庫存扣減的成功率,可以適當進行重試,如果庫存不足,則說明商品已經售罄,反之扣減庫存後version繼續加1。關於在資料庫中使用樂觀鎖釦減庫存的虛擬碼,如下所示:

  1. public void testStock(int num) {  

  2. if (version不一致時的重試次數閾值) {  

  3.     SELECT stock,version FROM item WHERE item_id=1;  

  4. if (如果查詢的指定商品存在) {  

  5. if (判斷stock是否夠扣減) {  

  6.              UPDATE item SET version=version+1,stock=stock-1 WHERE   

  7.                           item_id=1 AND version="+ version +"

    ;  

  8. if (扣減庫存失敗) {  

  9. /* version不一致時開始嘗試重試 */  

  10. testStock(--num);  

  11. else {  

  12. logger.info("扣減庫存成功");  

  13. }  

  14. else {  

  15. logger.warn("指定商品已售罄");  

  16. }  

  17. }  

  18. }  

如果系統前端不配合做限流消峰等處理,隨意放任大量的併發更新請求直接在資料庫中扣減同一熱賣商品的庫存資料,這將會導致執行緒之間相互競爭InnoDB的行鎖,由於資料庫中針對同一行資料的更新操作是序列執行的,那麼某一個執行緒在未釋放鎖之前,其餘的執行緒將會全部阻塞在佇列中等待拿鎖,併發越高時,等待的執行緒也就會越多,這會嚴重影響資料庫的TPS,從而導致RT線性上升,最終可能引發系統出現雪崩。

二、在Redis中扣減庫存
InnoDB的行鎖特性其實是一把利與弊都同樣明顯的雙刃劍,在保證一致性的同時卻降低了可用性,那麼究竟應該如何保證大併發更新熱點資料不會導致資料庫淪為瓶頸這其實是秒殺、搶購場景下最核心的技術難題之一。可以嘗試將熱賣商品的庫存扣減操作轉移至資料庫外,由於Redis的讀/寫能力要遠勝過任何型別的關係型資料庫,因此在Redis中實現庫存扣減將會是一個不錯的替代方案,這樣一來,資料庫中儲存的商品庫存可以理解為實際庫存,而Redis中儲存的商品庫存則為實時庫存。

在Redis中扣減熱賣商品的庫存,或許有同學會有疑問,Redis如何保證一致性呢?如何才能做到不超賣和少買呢?答案就是Redis提供的Watch命令來實現樂觀鎖,和基於MySQL的樂觀鎖機制一樣,併發環境下,通過Watch命令對目標Key進行標記後,當事務提交時,如果監控到目標Key對應的值已經發生了改變,那麼也就則意味著版本號發生了改變,因此這一次的事務提交操作就失敗,如圖1所示:

0?wx_fmt=png

圖1 利用Redis樂觀鎖釦減商品庫存 

在Redis中扣減熱賣商品的庫存主要是出於以下2個目的:

1、首先是為了避免在RDBMS中,多執行緒之間相互競爭InnoDB引擎的行鎖導致RT上升,TPS下降,最終引發雪崩的問題;

2、其次是能夠利用Redis與生俱來的高效讀/寫能力來提升系統的整體吞吐量。

三、利用“分裂”技巧巧妙地提升庫存扣減成功率

   這裡跟大家分享一個筆者公司的業務場景,由於特務特點,我們整點的限時搶購往往是爆款+大庫存(幾萬至十幾萬不等的庫存數),我們都知道限時搶購的峰值其實就是秒殺,並且還伴隨的大庫存。相對於普通的秒殺場景而言,由於庫存並不多,如果上游系統配合交易系統做好擴容、限流保護、隔離(業務隔離、資料隔離,以及系統隔離)、動靜分離、localCache等措施,秒殺場景下就能夠將絕大多數流量擋在系統上游,讓使用者流量像漏斗模型一樣逐層減少,讓流量始終保持在系統可處理的容量範圍之內。

   由於“變態”的業務特點,業務系統除了要承受億級流量的衝擊,交易系統還要想辦法提升下單時的庫存扣減成功率,這對於我們來說確實是一次挑戰,因為在生產環境中,一次的不小心,將會帶來災難性的後果。我們都知道架構的意義是有序的對系統進行重構,不斷減少系統的“熵”,讓其不斷進步,但架構調整的失誤,將會是不可逆的,尤其是那些成熟且使用者規模較大的網站。

    我們都知道,秒殺活動開始後,能夠搶購到心儀的產品,是非常不容的一件事情,因為在同一個單位時間內,除了你之外,還有別的使用者也在下單,那麼針對同一個爆款的WATCH碰撞概率將會被無情放大,成功率自然降低。如果是小庫存,直接返回商品已經售罄即可,但是多大十幾萬的庫存,讓使用者看得到,買不到,心裡癢癢的似乎不太友好,並且運營策略上也希望能夠快速消完這些庫存好製造噱頭。

   你不用指望能夠利用某一種資料庫就能夠即提升吞吐量又提升成功率,首先你需要搞明白的是,這是一個實打實的單點問題,要保證一致性,就必然會犧牲成功率,這個矛盾點,該怎麼解決呢?我們目前採用的做法是在Redis中,將某一個SKU的Key,拆分成N個對應的subKeys,庫存服務在扣減庫存的時候,通過輪詢路由策略路由到不同的subKey上來降低WATCH碰撞概率,達到大幅度提升下單成功率的目的,如圖2所示:

圖2 將parentKey分裂為n個subKeys

   分裂的概念相信大家都已經清楚了,接下來筆者再跟大家分享關於分裂操作的具體細節和一些注意事項。支撐分裂操作的主要由2部分構成,首先是嵌入在庫存服務中的路由元件,其次是分裂管理服務,路由元件的任務很簡單,訂閱配置中心的分裂規則,然後輪詢路由到不同的subKeys上做扣減即可。而分裂管理服務則相對複雜,parentKey的分裂操作就由它負責,並且它還需要處理一些相關的庫存聚合(subKeys庫存聚合)和下拉(重新劃分庫存給subKeys)任務。

   分裂了,必然需要對分裂資訊進行管理,比如:運營後臺對某一個parentKey進行大庫存扣減、調整某一個parentKey的分裂數量,以及刪除某一個parentKey的分裂規則。這些操作全都包含著以下2個動作:
1、庫存聚合(subKeys庫存聚合),並將subKey庫存設定為0;
2、然後將聚合後的庫存歸還給目標parentKey;

     由於聚合和歸還並不在同一個事物中,如果因為某些原因導致執行異常,那就悲劇了。比如聚合庫存的時候成功了,這時subKeys的庫存已經被設定為0,使用者是無法正常下單的,但還庫存給parentKey這個動作失敗了,將會導致商品少賣,所以需要依靠以下2點來儘量保證商品不少賣:
1、業務上增大Redis的重試次數;
2、如果Redis故障,告警後人工介入歸還庫存;

     為什麼要區分普通使用者扣減庫存和運營後臺扣減庫存?因為這是2個截然不同的概念,因為使用者扣減庫存,往往會受限於業務(比如限制1個使用者1次能夠購買的商品數量),但運營後臺則不同,有時候可能因為人為原因導致庫存設超,因此需要扣減大量的庫存,但是如果扣減的庫存數量大於每一個subKey持有的有效庫存數,則無法完成扣減操作,所以針對運營後臺的扣減我們提供有單獨的扣減方法,首先會聚合subKeys的庫存並將subKey持有的庫存數設定為0,將扣減後的庫存還給parentKey,再等待重新下拉分配庫存給subKeys。在此大家需要注意,如果一個商品特別爆,使用者併發越大,聚合再分配的時間視窗期就會越長。

    有時候,subKeys之間的庫存數可能存在不均勻的情況,那麼當某一個subKey持有的庫存被扣減完,且無迴流庫存以便下拉重新分配時,只要路由到這個subKey的庫存扣減動作都會是失敗的,使用者就會存在看得到,買不到的不友好體驗,因此可以在路由元件上做動作,當某一個subKey的庫存已經消完後,本地需要做剔除動作,下次不路由到這個subKey上。

   最後給大家一點建議,如果parentKey的分裂數量越多,庫存扣減的成功率就會越大,當然分裂數量也不是越多越好,一般來說一個parentKey分裂為10-20個subKey就夠了,相對以前已經擁有了10-20倍的下單扣減成功率提升。

640?wx_fmt=png

看完本文有收穫?請轉發分享給更多人

歡迎關注“暢聊架構”,我們分享最有價值的網際網路技術乾貨文章,助力您成為有思想的全棧架構師,我們只聊網際網路、只聊架構!打造最有價值的架構師圈子和社群。

長按下方的二維碼可以快速關注我們

640?wx_fmt=jpeg