自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

高并發(fā)業(yè)務(wù)下的庫存扣減方案

開發(fā) 架構(gòu)
商品庫存數(shù)據(jù)在DB最終會落到單庫單表的一行數(shù)據(jù)。無法通過分庫分表提高請求的并行度。而在單節(jié)點(diǎn)場景,數(shù)據(jù)庫吞吐遠(yuǎn)不如Redis。最基礎(chǔ)的原因:IO效率不是一個量級,DB是磁盤操作,而且還可能要多次讀盤,Redis是一步到位的內(nèi)存操作。

扣減庫存需要查詢庫存是否足夠:

  • 足夠就占用庫存
  • 不夠則返回庫存不足(這里不區(qū)分庫存可用、占用、已消耗等狀態(tài),統(tǒng)一成扣減庫存數(shù)量,簡化場景)

并發(fā)場景,若 查詢庫存和扣減庫存不具備原子性,就可能超賣,而高并發(fā)場景超賣概率會增高,超賣數(shù)額也會增高。處理超賣的確麻煩:

  • 系統(tǒng)全鏈路刷數(shù)會很麻煩(多團(tuán)隊(duì)協(xié)作),客服外呼也有額外成本
  • 最主要原因,客戶搶到訂單又被取消,嚴(yán)重影響客戶體驗(yàn),甚至引發(fā)客訴產(chǎn)生公關(guān)危機(jī)

實(shí)現(xiàn)邏輯

常用方案redis+lua,借助redis單線程執(zhí)行+lua腳本中的邏輯,可在一次執(zhí)行中順序完成的特性達(dá)到原子性(叫排它性更準(zhǔn)確,因?yàn)椴痪邆浠貪L動作,異常情況需自己手動編碼回滾)。

lua腳本基本實(shí)現(xiàn)

圖片圖片

-- 1. 獲取庫存緩存key KYES[1] = hot_{itemCode-skuCode}_stock
local hot_item_stock = KYES[1]

-- 2. 獲取剩余庫存數(shù)量
local stock = tonumber(redis.call('get', hot_item_stock))

-- 3. 購買數(shù)量
local buy_qty = tonumber(ARGV[1])

-- 4. 如果庫存小于購買數(shù)量,則返回1,表達(dá)庫存不足
if stock < buy_qty then
  return 1
end

-- 5. 庫存足夠,更新庫存數(shù)量
stock = stock - buy_qty
redis.call('set', hot_item_stock, tostring(stock))

-- 6. 扣減成功則返回2,表達(dá)庫存扣減成功
return 2

但腳本還有一些問題:

  • 不具備冪等性,同個訂單多次執(zhí)行會導(dǎo)致重復(fù)扣減,手動回滾也無法判斷是否會回滾過,會出現(xiàn)重復(fù)增加的問題
  • 不具備可追溯性,不知道庫存被誰被哪個訂單扣減了

增強(qiáng)后的lua腳本:

-- 1. 獲取庫存扣減記錄緩存 key KYES[2] = hot_{itemCode-skuCode}_deduction_history
local  hot_deduction_history = KYES[2]

-- 2. 使用 Redis Cluster hash tag 保證 stock 和 history 在同一個槽
local exist = redis.call('hexists', hot_deduction_history, ARGV[2])
-- 3. 請求冪等判斷,存在返回0,表達(dá)已扣減過庫存
if exist == 1 then return 0 end

-- 4. 獲取庫存緩存key KYES[1] = hot_{itemCode-skuCode}_stock
local hot_item_stock = KYES[1]

-- 5. 獲取剩余庫存數(shù)量
local stock = tonumber(redis.call('get', hot_item_stock))

-- 6. 購買數(shù)量
local buy_qty = tonumber(ARGV[1])

-- 7. 如果庫存小于購買數(shù)量 則返回1,表達(dá)庫存不足
if stock < buy_qty then return 1 end

-- 8. 庫存足夠
-- 9. 1.更新庫存數(shù)量
-- 10. 2.插入扣減記錄 ARGV[2] = ${扣減請求唯一key} - ${扣減類型} 值為 buy_qty
stock = stock - buy_qty
redis.call('set', hot_item_stock, tostring(stock))
redis.call('hset', hot_deduction_history, ARGV[2], buy_qty)

-- 11. 如果剩余庫存等于0則返回2,表達(dá)庫存已為0
if stock == 0 then return 2 end

-- 12. 剩余庫存不為0返回 3 表達(dá)還有剩余庫存
return 3 end

利用Redis Cluster hash tag保證stock和history在同個槽,這樣lua腳本才能正常執(zhí)行。

因?yàn)檎R?Lua 腳本操作的鍵必須在同一個 slot 中。

@Override
public <T, R> RFuture<R> evalReadAsync(String key, Codec codec, RedisCommand<T> evalCommandType, String script, List<Object> keys, Object... params) {
 NodeSource source = getNodeSource(key);
 return evalAsync(source, true, codec, evalCommandType, script, keys, false, params);
}

 private NodeSource getNodeSource(String key) {
     int slot = connectionManager.calcSlot(key);
     return new NodeSource(slot);
 }”

利用hot_deduction_history,判斷扣減請求是否執(zhí)行過,以實(shí)現(xiàn)冪等性。

借助hot_deduction_history的V值判斷追溯扣減來源,如:用戶A的交易訂單A的扣減請求,或用戶B的借出單B的扣減請求。

回滾邏輯先判斷hot_deduction_history里有沒有 ${扣減請求唯一key}:

  • 有,則執(zhí)行回補(bǔ)邏輯
  • 沒有,則認(rèn)定回補(bǔ)成功

但該邏輯依舊有漏洞,如(消息亂序消費(fèi)),訂單扣減庫存超時成功觸發(fā)了重新扣減庫存,但同時訂單取消觸發(fā)了庫存扣減回滾,回滾邏輯先成功,超時成功的重新扣減庫存就會成為臟數(shù)據(jù)留在redis里。

處理方案

有兩種:

  • 追加對賬,定期校驗(yàn)hot_deduction_history中數(shù)據(jù)對應(yīng)單據(jù)的狀態(tài),對于已經(jīng)取消的單據(jù)追加一次回滾請求,存在時延(業(yè)務(wù)不一定接受)以及額外計算資源開銷
  • 使用順序消息,讓扣減庫存、回滾庫存都走同一個MQ topic的有序隊(duì)列,借助MQ消息的有序性保證回滾動作一定在扣減動作后面執(zhí)行,但有序串行必然帶來性能下降

高可用

Redis終究是內(nèi)存,一旦服務(wù)中斷,數(shù)據(jù)就消失。所以需要追加保護(hù)數(shù)據(jù)不丟失的方案。

運(yùn)用Redis部署的高可用方案:

  • 采用Redis Cluster(數(shù)據(jù)分片+ 多副本 + 同步多寫 + 主從自動選舉)
  • 多寫節(jié)點(diǎn)分(同城異地)多中心防止意外災(zāi)害

定期歸檔冷數(shù)據(jù)。定期 + 庫存為0觸發(fā)redis數(shù)據(jù)往DB同步,流程如下:

圖片圖片

CDC分發(fā)數(shù)據(jù)時,秒殺商品,hot_deduction_history的數(shù)據(jù)量不高,可以一次全量同步。但如果是普通大促商品,就需要再追加一個map動作分批處理,以保證每次執(zhí)行CDC的數(shù)據(jù)量恒定,不至于一次性數(shù)據(jù)量太大出現(xiàn)OOM。代碼如下:

/**
 * 對任務(wù)做分發(fā)
 * @param stockKey 目標(biāo)庫存的key值
 */
public void distribute(String stockKey) {
    final String historyKey = StrUtil.format("hot_{}_deduction_history", stockKey);
    // 獲取指定庫存key 所有扣減記錄的key(生產(chǎn)請分頁獲取,防止數(shù)據(jù)量太多)
    final List<String> keys = RedisUtil.hkeys(historyKey, stockKey);
    // 以 100 為大小,分片所有記錄key
    final List<List<String>> splitKeys = CollUtil.split(keys, 100);
    // 將集合分發(fā)給各個節(jié)點(diǎn)執(zhí)行
    map(historyKey, splitKeys);
}

/**
 * 對單頁任務(wù)做執(zhí)行
 * @param historyKey 目標(biāo)庫存的key值
 * @param stockKeys 要執(zhí)行的頁面大小
 */
public void mapExec(String historyKey, List<String> stockKeys) {
    // 獲取指定庫存key 指定扣減記錄 的map
    final Map<String, String> keys = RedisUtil.HmgetToMap(historyKey, stockKeys);
    keys.entrySet()
        .stream()
        .map(stockRecordFactory::of)
        .forEach(stockRecord -> {
            // (冪等 + 去重) 扣減 + 保存記錄
            stockConsumer.exec(stockRecord);
            // 刪除redis中的 key 釋放空間
            RedisUtil.hdel(historyKey, stockRecord.getRecordRedisKey());
        });
}

為啥不走DB

商品庫存數(shù)據(jù)在DB最終會落到單庫單表的一行數(shù)據(jù)。無法通過分庫分表提高請求的并行度。而在單節(jié)點(diǎn)場景,數(shù)據(jù)庫吞吐遠(yuǎn)不如Redis。最基礎(chǔ)的原因:IO效率不是一個量級,DB是磁盤操作,而且還可能要多次讀盤,Redis是一步到位的內(nèi)存操作。

同時,一般DB都是提交讀隔離級別,為保證原子性,執(zhí)行庫存扣減,得加鎖,無論悲觀樂觀。不僅性能差(搶不到鎖要等待),而且因?yàn)榉枪礁偁?,易出現(xiàn)線程饑餓。而redis是單線程操作,不存在共享變量競爭。

有些優(yōu)化思路,如合并扣減,走批降低請求的并行連接數(shù)。但伴隨的集單的時延,以及按庫分批的訴求;還有拆庫存行,商品A100個庫存拆成2行商品A50庫存,然后扣減時分發(fā)請求,以提高并行連接數(shù)(多行可落在不同庫來提高并行連接數(shù))。但伴隨的:

  • 復(fù)雜的庫存行拆分管理(把什么庫存行在什么時候拆分到哪些庫)
  • 部分庫存行超賣的問題(加鎖優(yōu)化就又串行了,不加總量還有庫存,個別庫存行不足是允許一定系數(shù)超賣還是返回庫存不足就是一個要決策的問題)

部分頭部電商采用弱緩存抗讀(非庫存不足,不實(shí)時更新),DB抗寫的方案。該方案前提在于,通過一系列技術(shù)方案,流量落到庫存已相對低且平滑了(扛得住,不用再自己實(shí)現(xiàn)操作原子性)。

作者簡介:魔都架構(gòu)師,多家大廠后端一線研發(fā)經(jīng)驗(yàn),在分布式系統(tǒng)設(shè)計、數(shù)據(jù)平臺架構(gòu)和AI應(yīng)用開發(fā)等領(lǐng)域都有豐富實(shí)踐經(jīng)驗(yàn)。

各大技術(shù)社區(qū)頭部專家博主。具有豐富的引領(lǐng)團(tuán)隊(duì)經(jīng)驗(yàn),深厚業(yè)務(wù)架構(gòu)和解決方案的積累。

負(fù)責(zé):

  • 中央/分銷預(yù)訂系統(tǒng)性能優(yōu)化
  • 活動&券等營銷中臺建設(shè)
  • 交易平臺及數(shù)據(jù)中臺等架構(gòu)和開發(fā)設(shè)計
  • 車聯(lián)網(wǎng)核心平臺-物聯(lián)網(wǎng)連接平臺、大數(shù)據(jù)平臺架構(gòu)設(shè)計及優(yōu)化
  • LLM Agent應(yīng)用開發(fā)
  • 區(qū)塊鏈應(yīng)用開發(fā)
  • 大數(shù)據(jù)開發(fā)挖掘經(jīng)驗(yàn)
  • 推薦系統(tǒng)項(xiàng)目

目前主攻市級軟件項(xiàng)目設(shè)計、構(gòu)建服務(wù)全社會的應(yīng)用系統(tǒng)。

參考:編程嚴(yán)選網(wǎng)

責(zé)任編輯:武曉燕 來源: JavaEdge
相關(guān)推薦

2022-09-19 09:49:17

MCube網(wǎng)絡(luò)引擎

2021-08-26 08:24:33

高并發(fā)秒殺系統(tǒng)

2021-06-09 18:52:05

方案設(shè)計庫存數(shù)

2017-06-16 16:16:36

庫存扣減查詢

2025-03-11 08:36:52

高并發(fā)場景性能

2025-01-27 00:40:41

2025-02-26 08:10:40

2025-03-10 09:20:00

庫存異常Redis架構(gòu)

2024-06-20 07:59:49

2024-01-31 13:02:00

高并發(fā)熱點(diǎn)散列庫存分桶

2024-10-10 08:32:28

Redis高并發(fā)Lua

2025-03-31 10:42:31

2023-10-07 08:54:28

項(xiàng)目httpPost對象

2025-01-20 00:00:03

高并發(fā)秒殺業(yè)務(wù)

2012-04-24 09:30:57

淘寶開發(fā)

2022-03-31 17:38:09

高并發(fā)系統(tǒng)架構(gòu)設(shè)計負(fù)載均衡

2025-02-28 00:03:22

高并發(fā)TPS系統(tǒng)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號