電商技術(shù) -- 庫存設(shè)計指北

前言

最近在解決一套老電商系統(tǒng)的庫存"超賣"問題孔庭。一直以為超賣問題忿偷,最難解決的是庫存扣減趣些,實則不然仿荆,我們的系統(tǒng)在解決了庫存扣減問題之后,還會一直有“超賣”現(xiàn)象坏平?這一切的背后到底是道德的淪喪拢操,還是人性的扭曲,歡迎收看本期走近科學(xué)

本文帶你解決以下電商場景問題

  1. 保證庫存線程安全的扣減
  2. 防止庫存的多次扣減功茴、回滾
  3. 超時未支付被取消的訂單(取消會回滾庫存)庐冯, 如果收到了支付回調(diào)怎么辦

如何線程安全的扣減庫存

先來說說庫存扣減的問題孽亲,這是我們原來老系統(tǒng)的邏輯坎穿,注意!這里是錯誤的示例

// 以下是偽代碼,錯誤的示例
// 查詢出Goods對象
$goods = selectGoodsById($id);
if ($goods->num - $order_num > 0) {
    // 計算出扣減后的庫存
    $goods->num = $goods->num - $order_num;
    // 保存
    save($goods);
}

上述代碼犯了大忌玲昧,并發(fā)情況會導(dǎo)致多個線程讀到相同的庫存數(shù)栖茉,然后扣減,然后保存到DB孵延,下面我們來說下正確的姿勢

正確的做法

利用MySQL update 會持有當(dāng)前記錄鎖的特點吕漂,保證線程安全的扣減

SQL 示例:

update kucun set num = num - ? where id = ? and num - ? >= 0

我們的這條記錄根據(jù)主鍵更新,當(dāng)事務(wù)A update 這條記錄時尘应,會持有當(dāng)前記錄的鎖惶凝,當(dāng)事務(wù)A未提交時,其他想要更新這條記錄的事務(wù)只能等待鎖釋放

關(guān)于MySQL update 鎖的細(xì)節(jié)犬钢,本文不討論苍鲜,可以參考MySQL文檔

https://dev.mysql.com/doc/refman/8.0/en/innodb-locks-set.html
https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html
https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html

雖然MySQL可以保證數(shù)據(jù)的準(zhǔn)確性,但是大并發(fā)量場景下玷犹,大量的鎖競爭混滔,導(dǎo)致庫存的扣減可能成為系統(tǒng)性能的瓶頸

使用 Redis 庫存扣減

使用Redis的優(yōu)勢很多,單線程的文件事件處理器保證了并發(fā)下可以線程的安全扣減歹颓、回滾庫存坯屿, 以及Redis高性能。

雖然Redis解決了線程安全和性能的問題巍扛,但是Redis并不能做到像MySQL那樣一條SQL命令完成庫存扣減领跛,我們需要先讀出已有庫存,再和當(dāng)前下單庫存做一個判斷是否可以庫存扣減撤奸。所以最佳的實現(xiàn)方案是通過Redis 執(zhí)行l(wèi)ua腳本隔节,保證整個邏輯處理期間,不會有其他客戶端插進(jìn)來

    /**
     *
     * 扣減庫存Lua腳本
     * 庫存(stock)-1:表示不限庫存
     * 庫存(stock)0:表示沒有庫存
     * 庫存(stock)大于0:表示剩余庫存
     *
     * @params 庫存key
     * @return
     *      -3:庫存未初始化
     *      -2:庫存不足
     *      -1:不限庫存
     *      大于等于0:剩余庫存(扣減之后剩余的庫存)
     */
    const SUB_STOCK_LUA = "
        if (redis.call('exists', KEYS[1]) == 1) then
            local stock = tonumber(redis.call('get', KEYS[1]));
            local num = tonumber(ARGV[1]);   
            if (stock == -1) then
                return -1;
            end;
            
            if (stock >= num) then
                return redis.call('incrby', KEYS[1], 0 - num);
            end;
            
            return -2;
        end;
        
        return -3;
    ";

注意:
當(dāng)對一個訂單中的 good_list 扣減庫存的時候寂呛,需要注意怎诫,當(dāng)某一個商品庫存扣減失敗時,之前的扣減的商品庫存需要回滾贷痪。這會涉及到對redis的多次操作幻妓,你可以把整體邏輯寫到一個lua腳本中

使用Redis 做庫存扣減會有一個問題(偽代碼如下),Redis數(shù)據(jù)和MySQL數(shù)據(jù)并不能保證強一致性劫拢,因為Redis的數(shù)據(jù)相當(dāng)于直接寫進(jìn)去了肉津,如果在需要回滾的時候,Redis不可用了導(dǎo)致數(shù)據(jù)無法回滾舱沧,最終會造成MySQL沒有寫入訂單數(shù)據(jù)妹沙,Redis卻扣減了庫存

try {
    $db->beginTransaction();
    
    $db->saveOrder();
    $redis->reduceStock();

    $db->commit();
        
} catch (Exception $e) {
    $db->rollback();
    $redis->rollbackStock();
}

這種情況并沒有什么好的解決辦法,這是一個幾率非常小的故障熟吏,首先我們肯定要盡可能的保證Redis的高可用性距糖,其次在發(fā)生這種情況后玄窝,我們要想辦法恢復(fù)Redis中的數(shù)據(jù),例如我們可以在整個邏輯之后悍引,選擇異步的方式(例如MQ)向MySQL中同步庫存恩脂,當(dāng)發(fā)生故障后,以MySQL數(shù)據(jù)為準(zhǔn)恢復(fù)數(shù)據(jù)

所以Redis是一把雙刃劍趣斤,提升性能的同時俩块,也帶來了問題

AliSQL

這是后來我在網(wǎng)上看到的方案,AliSQL 是阿里自研 MySQL 分支浓领,AliSQL 針對并發(fā)修改同一記錄的情況玉凯,使用數(shù)據(jù)庫層面的緩沖隊列,避免大量爭鎖的代價联贩。感興趣的同學(xué)可以試下(阿里云MySQL 8.0 集成了這一功能)壮啊,如果AliSQL解決了性能問題的話,那么這個方案相比Redis要更好

關(guān)于庫存多次扣減的問題

當(dāng)訂單的提交和庫存的扣減同步進(jìn)行的時候撑蒜,不需要考慮這個問題歹啼。

舉例:訂單系統(tǒng)生成訂單之后,通過MQ通知庫存系統(tǒng)座菠,庫存系統(tǒng)異步扣減庫存狸眼,這個時候庫存系統(tǒng)可能會多次消費,這個時候就需要考慮這個問題了浴滴。

或者我們上面說的通過MQ同步MySQL庫存也需要考慮可能發(fā)生多次扣減

解決方案如圖拓萌,通過訂單做為唯一索引保證流水記錄的唯一性,從而保證只能有一次成功的扣減

image.png

庫存回滾問題

多數(shù)博客對于超賣的講解只在于庫存的扣減升略,但是庫存扣減安全了微王,真的就可以保證不超賣嗎?我們的系統(tǒng)在解決了庫存扣減問題后品嚣,還是出現(xiàn)成交訂單 > 庫存的問題炕倘,為此我也是絞盡腦汁,抓破了頭

在對下單進(jìn)行壓力測試之后翰撑,我堅信下單不會出現(xiàn)超賣的問題罩旋,后來我懷疑問題出在了庫存回滾,如果一個訂單回滾了兩次庫存(取消超時未支付訂單的線程和用戶線程同時取消一個訂單)眶诈,同樣也會出現(xiàn)超賣的現(xiàn)象涨醋。

解決方法:
和防止多次扣減一樣,采用寫入訂單回滾流水的方式逝撬,個人認(rèn)為這種方法比較加鎖要好浴骂,數(shù)據(jù)有跡可循

超時未支付被取消的訂單收到了支付回調(diào)

在解決了庫存回滾問題之后,超賣問題還沒有解決宪潮,最后通過日志定位到了這個問題溯警。

問題描述:用戶在系統(tǒng)即將自動取消訂單的前一瞬間完成了支付趣苏,系統(tǒng)取消了該訂單并回滾了庫存,同時系統(tǒng)收到了該訂單的支付回調(diào)愧膀,該訂單的狀態(tài)更改為已支付,因為不該出現(xiàn)的庫存回滾導(dǎo)致了“超賣”

下面說下我們的解決方案谣光,以微信支付為例

我們的系統(tǒng)在提交訂單之后檩淋,會調(diào)用微信的統(tǒng)一下單接口,這時候微信收到了我們的商戶訂單號(微信已經(jīng)生成訂單)萄金,用戶選擇不支付蟀悦。超時自動取消邏輯處理之前,先調(diào)用微信的關(guān)閉訂單接口氧敢,如果關(guān)閉成功日戈,則這個時候用戶后續(xù)無法對該訂單發(fā)起支付。如果返回訂單已支付孙乖,則無需處理該訂單浙炼,該訂單會收到微信支付的回調(diào)

參考

http://www.reibang.com/p/76bc0e963172
https://www.zhihu.com/question/268937734
https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_3

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市唯袄,隨后出現(xiàn)的幾起案子弯屈,更是在濱河造成了極大的恐慌,老刑警劉巖恋拷,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件资厉,死亡現(xiàn)場離奇詭異,居然都是意外死亡蔬顾,警方通過查閱死者的電腦和手機宴偿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來诀豁,“玉大人窄刘,你說我怎么就攤上這事∠鲜ぃ” “怎么了都哭?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長逞带。 經(jīng)常有香客問我欺矫,道長,這世上最難降的妖魔是什么展氓? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任穆趴,我火速辦了婚禮,結(jié)果婚禮上遇汞,老公的妹妹穿的比我還像新娘未妹。我一直安慰自己簿废,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布络它。 她就那樣靜靜地躺著族檬,像睡著了一般。 火紅的嫁衣襯著肌膚如雪化戳。 梳的紋絲不亂的頭發(fā)上单料,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天,我揣著相機與錄音点楼,去河邊找鬼扫尖。 笑死,一個胖子當(dāng)著我的面吹牛掠廓,可吹牛的內(nèi)容都是我干的换怖。 我是一名探鬼主播,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼蟀瞧,長吁一口氣:“原來是場噩夢啊……” “哼沉颂!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起悦污,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤兆览,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后塞关,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體抬探,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年帆赢,在試婚紗的時候發(fā)現(xiàn)自己被綠了小压。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡椰于,死狀恐怖怠益,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情瘾婿,我是刑警寧澤蜻牢,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站偏陪,受9級特大地震影響抢呆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜笛谦,卻給世界環(huán)境...
    茶點故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一抱虐、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧饥脑,春花似錦恳邀、人聲如沸懦冰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽刷钢。三九已至,卻和暖如春乳附,著一層夾襖步出監(jiān)牢的瞬間内地,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工许溅, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留瓤鼻,地道東北人秉版。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓贤重,卻偏偏與公主長得像,于是被迫代替她去往敵國和親清焕。 傳聞我的和親對象是個殘疾皇子并蝗,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,486評論 2 348