秒殺庫存解決方案

来源:https://www.cnblogs.com/jzhlin/archive/2023/08/14/17628179.html
-Advertisement-
Play Games

電商系統中秒殺是一種常見的業務場景需求,其中核心設計之一就是如何扣減庫存。本篇主要分享一些常見庫存扣減技術方案,庫存扣減設計選擇並非一味追求性能更佳,更多的應該考慮根據實際情況來進行架構取捨... ...


電商系統中秒殺是一種常見的業務場景需求,其中核心設計之一就是如何扣減庫存。本篇主要分享一些常見庫存扣減技術方案,庫存扣減設計選擇並非一味追求性能更佳,更多的應該考慮根據實際情況來進行架構取捨。在商品購買的過程中,庫存的抵扣過程通常包括以下步驟:

  1. 開啟事務:在開始進行庫存抵扣操作前,開啟一個事務。
  2. 查詢庫存:根據商品ID,使用SELECT語句從庫存表中查詢該商品的當前庫存數量。
  3. 檢查庫存是否足夠:將查詢到的庫存數量與用戶購買數量進行比較。如果庫存數量大於或等於用戶購買數量,則庫存足夠,可以繼續下單。如果庫存不足,需要採取相應的處理措施,例如提示用戶庫存不足或進行庫存預訂等。
  4. 扣減庫存:如果庫存足夠,根據用戶購買數量,使用UPDATE語句將庫存表中對應商品的庫存數量減去購買數量,得到最新的庫存剩餘值。
  5. 記錄交易明細:在購買過程中,通常需要記錄交易明細,例如生成訂單記錄或交易日誌,以便後續查詢和跟蹤。
  6. 提交事務:以上操作通常會在一個事務中進行,確保操作的原子性。如果所有步驟都成功執行,則提交事務,庫存扣減過程完成。如果在任何步驟中出現錯誤或異常,事務會回滾,恢復到操作前的狀態,確保數據的完整性和一致性。

由於涉及到 SELECT後進行UPDATE,以上步驟中存在多事務併發時寫覆蓋的問題。

悲觀鎖更新庫存

在資料庫併發控制中,防止寫覆蓋是一個重要的問題,特別是在多個會話(事務)同時嘗試修改同一行數據時。如果不進行適當的併發控制,可能會導致數據的不一致性和丟失更新。

為瞭解決這個問題,可以使用 SELECT FOR UPDATE語句。在使用SELECT FOR UPDATE時,資料庫會將目標行的數據加上寫鎖,阻止其他事務在當前事務完成之前修改被鎖定的數據。這樣,其他會話無法同時修改同一行數據,從而避免了併發寫入衝突。

需要註意的是,使用SELECT FOR UPDATE可能會引起一些併發性能問題,因為其他會話需要等待鎖釋放才能繼續執行。因此,在設計併發控制策略時,需要綜合考慮併發性能和數據一致性之間的平衡。

在上述流程中,步驟3 改為:

3. 查詢庫存並鎖定:使用SELECT ... FOR UPDATE語句查詢指定商品的庫存,並將其鎖定。這將確保其他併發事務在當前事務提交或回滾之前無法修改該商品的庫存。

    # 開始事務
    connection.begin()

    # 加鎖(FOR UPDATE)並讀取當前庫存記錄
    cursor.execute("SELECT quantity FROM inventory WHERE id = ? FOR UPDATE", item_id)
    current_quantity = cursor.fetchone()['quantity']

    # 檢查庫存是否足夠
    if current_quantity >= requested_quantity:
        # 計算更新後的庫存數量
        new_quantity = current_quantity - requested_quantity

        # 更新庫存
        cursor.execute("UPDATE inventory SET quantity = ? WHERE id = ?", (new_quantity, item_id))

        #{... 記錄明細等操作}

        # 提交事務
        connection.commit()
        return True
    else:
        # 庫存不足,回滾事務
        connection.rollback()
        return False 

樂觀鎖更新庫存

除開悲觀鎖,自然也可以想到使用樂觀鎖的方式來進行更新;最常見的設計就是CAS + 版本號的更新來實現庫存更新,在庫存表中新加一個 version的欄位。

# 開始事務
connection.begin()

# 讀取當前庫存記錄和版本號
cursor.execute("SELECT quantity, version FROM inventory WHERE id = ?", item_id)
result = cursor.fetchone()
current_quantity = result['quantity']
current_version = result['version']

# 檢查版本號是否匹配
if current_quantity >= requested_quantity:
    # 計算更新後的庫存數量和版本號
    new_quantity = current_quantity - requested_quantity
    new_version = current_version + 1

    # 更新庫存和版本號
    cursor.execute("UPDATE inventory SET quantity=%s,version=%s WHERE id=%s AND version=%s"
    	,(new_quantity, new_version, item_id, current_version))

    #{... 記錄明細等操作}

    # 檢查是否有更新行數
    if cursor.rowcount == 1:
        # 提交事務
        connection.commit()
        return True
    else:
        # 更新失敗,可能是由於版本號不匹配導致的併發操作問題
        connection.rollback()
        return False
else:
    # 庫存不足,回滾事務
    connection.rollback()
    return False

可以進一步思考,是否需要版本的概念;扣減庫存流程中,如果將 SELECT 查詢 作為庫存超賣前置檢查的(保障扣減成功率,減少不必要的寫操作)是視角看待,其實需要保障的是扣減後的庫存是否大於等於零。

如何理解前置檢查視角?
用個賣西瓜的例子來說明,假如你今天微信問到樓下水果店老闆有特價5毛一斤西瓜還有10個,這時你立刻下樓去購買。那麼可能兩種結果,結果一 你買到了特價西瓜;結果二 買的人太多,你到店的時候已經賣光了。從結果看,微信詢問的消息只是決定你下不下樓購買,而並非決定真正買到(不影響庫存);這種詢問作用在於減少直接下樓購買花費體力。

			
	# 讀取當前庫存記錄
    cursor.execute("SELECT quantity FROM inventory WHERE id = ? ", item_id)
    current_quantity = cursor.fetchone()['quantity']
			
    # 檢查庫存是否足夠
    if current_quantity >= requested_quantity:
    	# 開始事務
		connection.begin()
        
        # 更新庫存
        cursor.execute("UPDATE inventory SET quantity = quantity-? WHERE id = ? and quantity - ?>= 0"
        				, (requested_quantity, item_id,requested_quantity))

        #{... 記錄明細等操作}

         # 檢查是否有更新行數
    if cursor.rowcount == 1:
        # 提交事務
        connection.commit()
        return True
    else:
        # 更新失敗
        connection.rollback()
        return False

    else:
        return False 

庫存讀寫分離

再考慮一個極端的例子:假設有一個最新款的 iPhone 秒殺活動,庫存只有 100 件,活動期間預估峰值每秒查詢請求量(QPS)為 10 萬次。在活動結束後,流水錶最終只會插入 100 條記錄,但是查詢的 QPS 卻接近 10 萬次,導致讀取的壓力非常大。

在這種情況下,查詢壓力主要是由於活動期間大量的用戶查詢商品的秒殺狀態和庫存數量所導致的。雖然流水錶最終只插入了 100 條記錄,但是查詢請求卻非常頻繁,可能會導致資料庫性能問題。

優化首先可以想到是採用讀寫分離架構,通過新增一套從庫來實現。藉助MySQL自帶的數據同步能力,可以將主庫的數據同步到從庫,從而在讀取庫存時可以直接查詢從資料庫。這樣可以將讀取請求分散到從庫,減輕主庫的查詢壓力。

雖然讀寫分離可以提高查詢性能,但需要註意從庫的數據同步可能會有一定的時間延遲,導致從庫的數據新鮮度(實時性)有一定的滯後性。(前置檢查視角)在進行庫存校驗時,從庫的數據並不一定完全準確,但可以攔截大部分無效流量,起到了一定的作用。

最終的購買決策仍然由主庫的UPATE SQL語句來控制,以確保最終扣減的準確性。雖然從庫的數據可能有一定的滯後,但並不會影響最終扣減的結果,因為購買操作仍然在主庫上執行,確保了數據的一致性和準確性。

優點:1. 藉助資料庫的 ACID 特性,確保事務的原子性、一致性、隔離性和持久性,避免了超賣和少買等業務問題。
2. 實現簡單,適用於項目工期緊張或開發資源有限的情況。

不足: 如果參與秒殺的 SKU(庫存量單位)非常多,最終的寫操作都是基於庫存主庫,可能會導致主庫的性能壓力較大。

這裡 犧牲數據實時性(新鮮度) 來提升性能 是一種典型的 技術架構選型的 取捨方向。

庫存分庫分表

為瞭解決上述存在的容量和性能上限問題,庫存分庫分表會是一種優化選擇。

  • 將庫存扣減表和扣減明細表根據商品ID進行水平拆分,將不同商品的記錄存儲在不同的分片中。這樣可以將高併發的請求路由到不同的資料庫實例上,分攤資料庫負載。

  • 在水平拆分的基礎上,進一步考慮將不同商品的記錄分佈在不同的資料庫實例中,每個實例稱為一個庫。對於每個庫,可以再將表進行分表,將不同商品的記錄分開存儲。例如,可以按照商品ID的哈希值進行分表,或者按照一定的範圍將商品ID進行分段,確保每個表的數據量均衡。

在實際應用中,系統需要根據商品ID來決定將請求路由到哪個資料庫實例上。可以使用一致性哈希演算法、分段路由規則等方式來實現請求的正確路由。這樣可以確保同一商品的庫存扣減和明細記錄在同一個資料庫實例上進行,保證事務的原子性和數據的一致性。

總體來說,通過水平拆分和分庫分表的設計,可以有效地提高系統的吞吐量和性能,並減輕單一實例的容量限制。但是在實際應用中,需要仔細考慮資料庫的選擇、數據路由策略、數據一致性等問題,以確保系統的可用性和性能。同時,還需要合理評估業務需求和數據增長趨勢,以選擇合適的分片和資料庫配置,避免出現過度分片或數據傾斜等問題。

緩存扣減庫存

讀寫分離、分庫分表確實能分攤主庫很大一部分壓力,但是如果面對是 單品萬級QPS 的秒殺流量,MySQL 的千級 TPS 同樣也支撐不了,需要進一步升級性能。

(讀 改為 Redis)此時引入緩存中間件,將 MySQL 的數據定時同步到緩存中(可能存在延遲,庫存顯示不准確)。庫存超賣前置檢查,從 Redis 中查詢剩餘的庫存數據,寫入操作在資料庫校驗不准也不會超賣。 由於緩存基於記憶體操作,性能比資料庫高出幾個數量級,單台 Redis 實例可以達到 10W QPS 的讀性能。

(讀/寫庫存都為 Redis)對於扣減庫存的操作,如果直接執行多個 Redis 命令,無法保證原子性。為了確保原子性,可以採用 Lua 腳本的形式,將多個 Redis 命令打包到一個腳本中,作為一個命令發送給 Redis 執行,從而保證了操作的原子性。

具體步驟如下:

  • 使用 Lua 腳本:將扣減庫存的多個 Redis 命令封裝在一個 Lua 腳本中。這樣可以確保這些命令在 Redis 中以原子方式執行,避免併發問題。
  • 執行 Lua 腳本:將封裝了扣減庫存邏輯的 Lua 腳本作為一個整體命令發送給 Redis 執行。這樣在 Redis 中執行腳本時,將按照腳本中的邏輯一次性執行多個命令。
  • 非同步保存到資料庫:Redis 扣減庫存成功後,將此次扣減操作非同步化保存到資料庫中進行持久化存儲。

單品分桶扣減

在更大規模,針對單一商品的超高併發扣減的庫存集群中,可能基於資料庫內核的改造優化還無法滿足業務需求。單一商品的超高併發扣減可能會影響到同一資料庫實例上的其他商品扣減,同一個資料庫實例上也可能存在多個熱點商品造成互相影響,這時就考慮引入基於緩存的分桶扣減方案。

將商品ID按照一定的規則分成多個桶(Bucket),每個桶對應一個緩存項。例如,可以根據商品ID的哈希值或者取模運算的結果來分桶。分桶的目的是將不同商品的庫存信息均勻地存儲在不同的緩存項中,避免單個緩存項過大導致性能問題。

在進行庫存扣減時,首先根據商品ID找到對應的緩存項。然後,在緩存中讀取當前庫存數量,併進行判斷是否足夠進行扣減操作。如果足夠,更新緩存中的庫存數量,並將扣減後的值存回緩存。如果不足,直接返回扣減失敗。

其他解決方案

  1. 針對單品較多場景,也可以考慮批量扣減庫存,批量處理庫存的更新操作,這樣可以大量的減少資料庫事務。

  2. 基於消息的庫存,下單完成後發生訂單相關消息,庫存通過消息消費的方式進行更新;優勢在於庫存的更新速率可控。

  3. 令牌庫存,可控的時間內進行秒殺庫存,提升用戶秒殺感知。

以上綜述

可以看到庫存扣減方案場景多樣,更多的 應該根據業務要求 以及 具體的流量進行選擇,僅追求性能非好的選擇;性能高的同時 往往意味 著其他方面的取捨,比如:代碼複雜性、庫存精準性、部署複雜性等等。

歡迎關註 公眾號
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ## 1、背景 公司內部看到一則問題 > 1、kill -9 mysqld_safe 進程 > 2、systemd 檢測到 mysqld_safe 進程不存在後,重新拉起 mysqld_safe 進程 > 3、mysqld_safe 進程啟動後,發現 mysqld 進程也被重啟 期望:啟、停 mys ...
  • 一、背景 在《SRE: Google運維解密》一書中作者指出,監控系統需要能夠有效的支持白盒監控和黑盒監控。黑盒監控只在某個問題目前正在發生,並且造成了某個現象時才會發出緊急警報。“白盒監控則大量依賴對系統內部信息的檢測,如系統日誌、抓取提供指標信息的 HTTP 節點等。白盒監控系統因此可以檢測到即 ...
  • 本機環境:win10專業版,64位,16G記憶體。 原先用的AS2.2,是很早之前在看《第一行代碼Android(第2版)》的時候,按書里的鏈接下載安裝的,也不用怎麼配置。(PS:第一行代碼這本書對新手確實很適合,第1版是eclise,第2版是Android studio) 最近想給AS升級一下,果不 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 form 表單恢復初始數據 在現代的 Web 開發中,表單是不可或缺的組件之一。用戶可以通過表單輸入和提交數據,而開發者則需要對這些數據進行處理和存儲。然而,在某些情況下,我們可能需要重置表單並恢復到最初的狀態。 本文介紹瞭如何使用 fo ...
  • 在我的項目中,使用i18n切換語言後,會進行`router.push`來刷新頁面。 但我發現寫在store中的選項(我把它們用作下拉框組件的`options`,例如`options="store.statusOption"`),卻並沒有切換語言。它們需要我手動刷新頁面後才能夠切換語言。然而其它組件中 ...
  • # Electron-builder打包和自動更新 # 前言 文本主要講述如何為 electron 打包出來軟體配置安裝引導和結合 github 的 release 配置自動更新。 electron-builder 是將 Electron 工程打包成相應平臺的軟體的工具,我的工程是使用 [elect ...
  • 最近前輩推薦我讀《設計模式之禪》這本書,原因是我寫的代碼質量實在是一言難盡,開發速度很快,但是bug數就很多了,設計原則這種知識就需要掌握 寫這篇文主要是記錄自己的學習以及督促自己 第一章【單一職責】 從我理解的層面來談談單一原則:明確每個類每個方法的任務,只做一件事,不能一法兩用 這也是我最大的一 ...
  • # 加油站圈存機系統 ​ 對於加油卡而言,圈存是將`用戶賬戶`中已存入的資金劃轉到所持的加油卡上後方可使用。通俗一點的說法就是您在網點把錢存入主卡中,再分配到下麵的副卡,由於副卡都在使用車輛的駕駛員手中,需要在加油的時候在加油站讓加油站員工劃一下即可,就是所謂的圈存。 #### 圈存操作流程 ​ 如 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...