分布式锁和mysql事物扣库存_这个是真的厉害,高并发场景下的订单和库存处理方案,讲的很详细了!...
前言
之前一直有小伙伴私信我問我高并發(fā)場景下的訂單和庫存處理方案,我最近也是因?yàn)榧影嗟脑虮容^忙,就一直沒來得及回復(fù)。今天好不容易閑了下來想了想不如寫篇文章把這些都列出來的,讓大家都能學(xué)習(xí)到,說一千道一萬都不如滿滿的干貨來的實(shí)在,干貨都下面了!
介紹
前提:分布式系統(tǒng),高并發(fā)場景
商品A只有100庫存,現(xiàn)在有1000或者更多的用戶購買。如何保證庫存在高并發(fā)的場景下是安全的。
預(yù)期結(jié)果:1.不超賣 2.不少賣 3.下單響應(yīng)快 4.用戶體驗(yàn)好
下單思路:
下單時(shí)生成訂單,減庫存,同時(shí)記錄庫存流水,在這里需要先進(jìn)行庫存操作再生成訂單數(shù)據(jù),這樣庫存修改成功,響應(yīng)超時(shí)的特殊情況也可以通過第四步定時(shí)校驗(yàn)庫存流水來完成最終一致性。
支付成功刪除庫存流水,處理完成刪除可以讓庫存流水?dāng)?shù)據(jù)表數(shù)據(jù)量少,易于維護(hù)。
未支付取消訂單,還庫存+刪除庫存流水
定時(shí)校驗(yàn)庫存流水,結(jié)合訂單狀態(tài)進(jìn)行響應(yīng)處理,保證最終一致性
(退單有單獨(dú)的庫存流水,申請(qǐng)退單插入流水,退單完成刪除流水+還庫存)
什么時(shí)候進(jìn)行減庫存
方案一:加購時(shí)減庫存。
方案二:確認(rèn)訂單頁減庫存。
方案三:提交訂單時(shí)減庫存。
方案四:支付時(shí)減庫存。
分析:
方案一:在這個(gè)時(shí)間內(nèi)加入購物車并不代表用戶一定會(huì)購買,如果這個(gè)時(shí)候處理庫存,會(huì)導(dǎo)致想購買的用戶顯示無貨。而不想購買的人一直占著庫存。顯然這種做法是不可取的。唯品會(huì)購物車鎖庫存,但是他們是另一種做法,加入購物車后會(huì)有一定時(shí)效,超時(shí)會(huì)從購物車清除。
方案二:確認(rèn)訂單頁用戶有購買欲望,但是此時(shí)沒有提交訂單,減庫存會(huì)增加很大的復(fù)雜性,而且確認(rèn)訂單頁的功能是讓用戶確認(rèn)信息,減庫存不合理,希望大家對(duì)該方案發(fā)表一下觀點(diǎn),本人暫時(shí)只想到這么多。
方案三:提交訂單時(shí)減庫存。用戶選擇提交訂單,說明用戶有強(qiáng)烈的購買欲望。生成訂單會(huì)有一個(gè)支付時(shí)效,例如半個(gè)小時(shí)。超過半個(gè)小時(shí)后,系統(tǒng)自動(dòng)取消訂單,還庫存。
方案四:支付時(shí)去減庫存。比如:只有100個(gè)用戶可以支付,900個(gè)用戶不能支付。用戶體驗(yàn)太差,同時(shí)生成了900個(gè)無效訂單數(shù)據(jù)。
所以綜上所述:
選擇方案三比較合理。
重復(fù)下單問題
用戶點(diǎn)擊過快,重復(fù)提交。
網(wǎng)絡(luò)延時(shí),用戶重復(fù)提交。
網(wǎng)絡(luò)延時(shí)高的情況下某些框架自動(dòng)重試,導(dǎo)致重復(fù)請(qǐng)求。
用戶惡意行為。
解決辦法
前端攔截,點(diǎn)擊后按鈕置灰。
后臺(tái):
(1)redis 防重復(fù)點(diǎn)擊,在下單前獲取用戶token,下單的時(shí)候后臺(tái)系統(tǒng)校驗(yàn)這個(gè) token是否有效,導(dǎo)致的問題是一個(gè)用戶多個(gè)設(shè)備不能同時(shí)下單。
//key , 等待獲取鎖的時(shí)間 ,鎖的時(shí)間
redis.lock("shop-oms-submit" + token, 1L, 10L);
redis的key用token + 設(shè)備編號(hào) 一個(gè)用戶多個(gè)設(shè)備可以同時(shí)下單。
//key , 等待獲取鎖的時(shí)間 ,鎖的時(shí)間
redis.lock("shop-oms-submit" + token + deviceType, 1L, 10L);
(2)防止惡意用戶,惡意攻擊 : 一分鐘調(diào)用下單超過50次 ,加入臨時(shí)黑名單 ,10分鐘后才可繼續(xù)操作,一小時(shí)允許一次跨時(shí)段弱校驗(yàn)。使用reids的list結(jié)構(gòu),過期時(shí)間一小時(shí)
/**
* @param token
* @return true 可下單
*/
public boolean judgeUserToken(String token) {
//獲取用戶下單次數(shù) 1分鐘50次
String blackUser = "shop-oms-submit-black-" + token;
if (redis.get(blackUser) != null) {
return false;
}
String keyCount = "shop-oms-submit-count-" + token;
Long nowSecond = LocalDateTime.now().toEpochSecond(ZoneOffset.of("+8"));
//每一小時(shí)清一次key 過期時(shí)間1小時(shí)
Long count = redis.rpush(keyCount, String.valueOf(nowSecond), 60 * 60);
if (count < 50) {
return true;
}
//獲取第50次的時(shí)間
List secondString = redis.lrange(keyCount, count - 50, count - 49);
Long oldSecond = Long.valueOf(secondString.get(0));
//now > oldSecond + 60 用戶可下單
boolean result = nowSecond.compareTo(oldSecond + 60) > 0;
if (!result) {
//觸發(fā)限制,加入黑名單,過期時(shí)間10分鐘
redis.set(blackUser, String.valueOf(nowSecond), 10 * 60);
}
return result;
}
如何安全的減庫存
多用戶搶購時(shí),如何做到并發(fā)安全減庫存?
方案1: 數(shù)據(jù)庫操作商品庫存采用樂觀鎖防止超賣:
sql:update sku_stock set stock = stock - num where sku_code = '' and stock - num > 0;
分析:
高并發(fā)場景下,假設(shè)庫存只有 1件 ,兩個(gè)請(qǐng)求同時(shí)進(jìn)來,搶購該商品.
數(shù)據(jù)庫層面會(huì)限制只有一個(gè)用戶扣庫存成功。在并發(fā)量不是很大的情況下可以這么做。但是如果是秒殺,搶購,瞬時(shí)流量很高的話,壓力會(huì)都到數(shù)據(jù)庫,可能拖垮數(shù)據(jù)庫。
方案2:利用Redis單線程 強(qiáng)制串行處理
/**
* 缺點(diǎn)并發(fā)不高,同時(shí)只能一個(gè)用戶搶占操作,用戶體驗(yàn)不好!
*
* @param orderSkuAo
*/
public boolean subtractStock(OrderSkuAo orderSkuAo) {
String lockKey = "shop-product-stock-subtract" + orderSkuAo.getOrderCode();
if(redis.get(lockKey)){
return false;
}
try {
lock.lock(lockKey, 1L, 10L);
//處理邏輯
}catch (Exception e){
LogUtil.error("e=",e);
}finally {
lock.unLock(lockKey);
}
return true;
}
分析:
利用Redis 分布式鎖,強(qiáng)制控制同一個(gè)商品處理請(qǐng)求串行化,缺點(diǎn)并發(fā)不高 ,處理比較慢,不適合搶購,高并發(fā)場景。用戶體驗(yàn)差,但是減輕了數(shù)據(jù)庫的壓力。
方案3 :redis + mq + mysql 保證庫存安全,滿足高并發(fā)處理,但相對(duì)復(fù)雜。
/**
* 扣庫存操作,秒殺的處理方案
* @param orderCode
* @param skuCode
* @param num
* @return
*/
public boolean subtractStock(String orderCode,String skuCode, Integer num) {
String key = "shop-product-stock" + skuCode;
Object value = redis.get(key);
if (value == null) {
//前提 提前將商品庫存放入緩存 ,如果緩存不存在,視為沒有該商品
return false;
}
//先檢查 庫存是否充足
Integer stock = (Integer) value;
if (stock < num) {
LogUtil.info("庫存不足");
return false;
}
//不可在這里直接操作數(shù)據(jù)庫減庫存,否則導(dǎo)致數(shù)據(jù)不安全
//因?yàn)榇藭r(shí)可能有其他線程已經(jīng)將redis的key修改了
//redis 減少庫存,然后才能操作數(shù)據(jù)庫
Long newStock = redis.increment(key, -num.longValue());
//庫存充足
if (newStock >= 0) {
LogUtil.info("成功搶購");
//TODO 真正扣庫存操作 可用MQ 進(jìn)行 redis 和 mysql 的數(shù)據(jù)同步,減少響應(yīng)時(shí)間
} else {
//庫存不足,需要增加剛剛減去的庫存
redis.increment(key, num.longValue());
LogUtil.info("庫存不足,并發(fā)");
return false;
}
return true;
}
分析:
利用Redis increment 的原子操作,保證庫存安全,利用MQ保證高并發(fā)響應(yīng)時(shí)間。但是事需要把庫存的信息保存到Redis,并保證Redis 和 Mysql 數(shù)據(jù)同步。缺點(diǎn)是redis宕機(jī)后不能下單。
increment 是個(gè)原子操作。
綜上所述:
方案三滿足秒殺、高并發(fā)搶購等熱點(diǎn)商品的處理,真正減扣庫存和下單可以異步執(zhí)行。在并發(fā)情況不高,平常商品或者正常購買流程,可以采用方案一數(shù)據(jù)庫樂觀鎖的處理,或者對(duì)方案三進(jìn)行重新設(shè)計(jì),設(shè)計(jì)成支持單訂單多商品即可,但復(fù)雜性提高,同時(shí)redis和mysql數(shù)據(jù)一致性需要定期檢查。
訂單時(shí)效問題
超過訂單有效時(shí)間,訂單取消,可利用MQ或其他方案回退庫存。
設(shè)置定時(shí)檢查
Spring task 的cron表達(dá)式定時(shí)任務(wù)
MQ消息延時(shí)隊(duì)列
訂單與庫存涉及的幾個(gè)重要知識(shí)
TCC 模型:Try/Confirm/Cancel:不使用強(qiáng)一致性的處理方案,最終一致性即可,下單減庫存,成功后生成訂單數(shù)據(jù),如果此時(shí)由于超時(shí)導(dǎo)致庫存扣成功但是返回失敗,則通過定時(shí)任務(wù)檢查進(jìn)行數(shù)據(jù)恢復(fù),如果本條數(shù)據(jù)執(zhí)行次數(shù)超過某個(gè)限制,人工回滾。還庫存也是這樣。
冪等性:分布式高并發(fā)系統(tǒng)如何保證對(duì)外接口的冪等性,記錄庫存流水是實(shí)現(xiàn)庫存回滾,支持冪等性的一個(gè)解決方案,訂單號(hào)+skuCode為唯一主鍵(該表修改頻次高,少建索引)
樂觀鎖:where stock + num>0
消息隊(duì)列:實(shí)現(xiàn)分布式事務(wù) 和 異步處理(提升響應(yīng)速度)
redis:限制請(qǐng)求頻次,高并發(fā)解決方案,提升響應(yīng)速度
分布式鎖:防止重復(fù)提交,防止高并發(fā),強(qiáng)制串行化
分布式事務(wù):最終一致性,同步處理(Dubbo)/異步處理(MQ)修改 + 補(bǔ)償機(jī)制
寫在最后的話
大家看完有什么不懂的可以在下方留言討論,也可以私信問我一般看到后我都會(huì)回復(fù)的。最后覺得文章對(duì)你有幫助的話記得點(diǎn)個(gè)贊哦,點(diǎn)點(diǎn)關(guān)注不迷路,每天都有新鮮的干貨分享!
總結(jié)
以上是生活随笔為你收集整理的分布式锁和mysql事物扣库存_这个是真的厉害,高并发场景下的订单和库存处理方案,讲的很详细了!...的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: redis关键字删除_Redis批量删除
- 下一篇: mysql 39 t.id 39_Day