引言:在庫(kù)存的變動(dòng)中锌唾,最關(guān)鍵的節(jié)點(diǎn)是庫(kù)存的扣減,在什么時(shí)候扣減庫(kù)存非常重要触徐。目前通用的庫(kù)存扣減方案有以下幾種
支付后扣減庫(kù)存咪鲜,缺點(diǎn):成功下單的用戶(hù),到支付時(shí)沒(méi)有庫(kù)存可用撞鹉,導(dǎo)致交易失敗疟丙。
下單時(shí)就減庫(kù)存,訂單取消再把庫(kù)存加回來(lái)鸟雏,缺點(diǎn):惡意刷單不支付導(dǎo)致大量庫(kù)存被占用享郊,影響商品售賣(mài)。
下單時(shí)先預(yù)減庫(kù)存(對(duì)應(yīng)數(shù)據(jù)庫(kù) 占用庫(kù)存加庫(kù)存操作),支付完成時(shí) 釋放占用庫(kù)存(減操作)孝鹊,扣減可用庫(kù)存炊琉。 同時(shí)商品在下單時(shí)判斷商品的實(shí)際可售庫(kù)存 = 可用庫(kù)存 - 占用庫(kù)存,如果 > 0,表示可以下訂單,這樣就不會(huì)導(dǎo)致 下單成功苔咪,但是支付時(shí)沒(méi)有庫(kù)存導(dǎo)致失敗的場(chǎng)景锰悼。
具體方案
- 訂單創(chuàng)建時(shí),通過(guò)調(diào)用商品接口預(yù)占庫(kù)存团赏,如果預(yù)占庫(kù)存成功箕般,則創(chuàng)建訂單,如果預(yù)占庫(kù)存失敗舔清,則提示商品庫(kù)存不足丝里。這個(gè)接口必須是同步實(shí)時(shí)的,因?yàn)橛唵我鶕?jù)預(yù)占庫(kù)存的結(jié)果來(lái)判斷訂單能否創(chuàng)建鸠踪。
- 訂單支付丙者、取消時(shí),發(fā)送mq消息到商品营密,商品異步消費(fèi)消息械媒,根據(jù)消息的類(lèi)別去操作庫(kù)存。如果是未支付取消訂單评汰,則釋放預(yù)占用庫(kù)存纷捞。如果是支付后退款,則需要將可用庫(kù)存加回來(lái)被去。如果是成功支付主儡,則釋放預(yù)占庫(kù)存,并同時(shí)扣減可用庫(kù)存惨缆。
需要注意的問(wèn)題
-
商品超賣(mài)問(wèn)題
:
正常情況下糜值,一個(gè)訂單過(guò)來(lái),其中A商品買(mǎi)了n個(gè)坯墨,那么我們操作數(shù)據(jù)庫(kù)的時(shí)候寂汇,直接 set stock = stock -n 。這種當(dāng)然是有問(wèn)題的捣染,有可能會(huì)超賣(mài)導(dǎo)致商品庫(kù)存為負(fù)數(shù)骄瓣。當(dāng)然我們可以在更新db之前,判斷庫(kù)存數(shù)是否 > n耍攘,如果大于n榕栏,再去扣減庫(kù)存。這總當(dāng)然可以蕾各,不過(guò)高并發(fā)下扒磁,可能依然會(huì)導(dǎo)致超賣(mài)。當(dāng)然你可以加鎖去保證單線(xiàn)程式曲,不過(guò)這樣就導(dǎo)致了接口的性能下降渗磅。其實(shí)sql 可以換個(gè)寫(xiě)法 set stock = stock -n where stock >= n。 這樣利用了數(shù)據(jù)庫(kù)的天然寫(xiě)法保證了商品不超賣(mài)检访。 -
高并發(fā)下的接口性能問(wèn)題
步驟一訂單創(chuàng)建實(shí)時(shí)調(diào)用商品占用庫(kù)存接口始鱼,因?yàn)槭菍?shí)時(shí)調(diào)用,如果是高并發(fā)情況下脆贵,對(duì)于db占用庫(kù)存的更新操作可能就會(huì)成為性能瓶頸(訂單支付或者取消時(shí)医清,因?yàn)樽吡讼㈥?duì)列異步更新數(shù)據(jù)庫(kù),就不存在性能問(wèn)題)卖氨。
如果解決這個(gè)問(wèn)題呢会烙?業(yè)界常用的做法是,將商品的可用庫(kù)存放到redis中筒捺,當(dāng)訂單創(chuàng)建調(diào)用占用庫(kù)存接口時(shí)柏腻,我們可以利用redis去抗并發(fā),并且redis的命令支持原子性系吭。
1.創(chuàng)建訂單扣減緩存中的可用庫(kù)存
緩存中更新庫(kù)存和我們?nèi)ジ聰?shù)據(jù)庫(kù)時(shí)遇到的場(chǎng)景一樣五嫂,因?yàn)橐袛鄮?kù)存是否大于下單購(gòu)買(mǎi)數(shù)的邏輯要保持原子性,同時(shí)一個(gè)訂單中需要判斷多個(gè)商品的庫(kù)存也是需要原子性肯尺,可以結(jié)合lua腳本來(lái)實(shí)現(xiàn)沃缘。
首先根據(jù)訂單明細(xì)id查詢(xún)扣減流水,是否已經(jīng)操作過(guò)则吟,做冪等性校驗(yàn)
然后查詢(xún)sku的剩余庫(kù)存槐臀,并根據(jù)下單購(gòu)買(mǎi)數(shù)做校驗(yàn),只要有一個(gè)sku 數(shù)量不足氓仲,則返回失敗
修改緩存中的剩余庫(kù)存數(shù)
緩存中插入扣減流水記錄
2.支付成功后消息隊(duì)列異步更新數(shù)據(jù)庫(kù)中的可用庫(kù)存
3.訂單未支付取消時(shí)則需要將緩存中的庫(kù)存加回來(lái)水慨。根據(jù)訂單明細(xì)id查詢(xún)扣減流水,有扣減流水敬扛,則繼續(xù)查詢(xún)出訂單中所有sku商品的庫(kù)存晰洒,將扣減的庫(kù)存再加回來(lái)。如果沒(méi)有扣減流水舔哪,則跳過(guò)不處理欢顷。
4、訂單已支付退款時(shí)捉蚤,需要同時(shí)更新緩存中的庫(kù)存和db中的庫(kù)存抬驴。
缺點(diǎn):上面我們說(shuō)的用lua腳本執(zhí)行命令,如果一個(gè)訂單中的多個(gè)商品缆巧,布持,一部分成功,一部分扣減庫(kù)存失敗陕悬,那么是無(wú)法進(jìn)行回滾操作的题暖,雖然這種可能性很小,所以這種方案我們只能盡量保證redis集群的高可用。以上方案解決了高并發(fā)下的接口性能瓶頸胧卤,但是因?yàn)槠鋸?fù)雜性有可能會(huì)導(dǎo)致redis中可用庫(kù)存和數(shù)據(jù)庫(kù)中的可用庫(kù)存不一致(這里說(shuō)的不一致是指沒(méi)有未支付訂單占用庫(kù)存的情況)唯绍,我們可能還需要定時(shí)任務(wù)去定時(shí)維護(hù)redis中可用庫(kù)存和數(shù)據(jù)庫(kù)中可用庫(kù)存的一致性,用數(shù)據(jù)庫(kù)中的庫(kù)存 - 未支付訂單的占用庫(kù)存枝誊,然后更新到redis中
總結(jié):以上基于緩存扣減庫(kù)存况芒,大部分情況是對(duì)一些活動(dòng)的秒殺商品可能才會(huì)有如此高的并發(fā),正常情況下也不可能將所用商戶(hù)的所有商品庫(kù)存都緩存到redis中叶撒,這樣也不現(xiàn)實(shí)绝骚。所以絕大部分流量不高的情況下,我們可以采用數(shù)據(jù)庫(kù)占用庫(kù)存的方式,這種方式簡(jiǎn)單高效祠够,不易出錯(cuò)压汪。對(duì)于一些活動(dòng)商品,我們則可以單獨(dú)走緩存扣減可用庫(kù)存的方式古瓤。