redis rua解决库存问题_如何解决高并发下的库存安全问题,没你想得那么复杂(附源码)...
一、 問題
不知道大家該開發(fā)中有沒有遇到這樣的一個問題,在電影院購票或者去網(wǎng)上買東西的時候,比方說當(dāng)年哪吒大電影出來的時候,那搶票相當(dāng)火爆啊,一票難求,那購票系統(tǒng)的后臺是如何保證觀眾能買到自己喜歡的票同時不用擔(dān)心會被別人搶走呢?還有一個典型的例子,每年雙十一的瘋狂的時候,準(zhǔn)時搶購的時候,同一時間內(nèi)會有大量的訂單涌入,先不說負(fù)載的事情,那么后面再說,我們來說一下,他是如何保證庫存安全的呢?不會出現(xiàn)已經(jīng)沒有庫存但是依舊在頁面上顯示有庫存呢?
那我們首先要解決的就是在用戶準(zhǔn)備下單以及取消訂單的時候,我們后臺庫存都發(fā)生了什么
二、 下單的步驟
(1)下單
(2)下單同時預(yù)占庫存
(3)支付
(4)支付成功真正減扣庫存
(5)取消訂單
(6)回退預(yù)占庫存
那這樣在下單的過程中,我們中間對于庫存有一個合理的要求:預(yù)占。就是說當(dāng)顧客已經(jīng)選定了商品,這個時候會從庫存中暫時性的去去除掉,為顧客保留一個名額,那什么時候會產(chǎn)生這樣的問題呢?
三、 什么時候進(jìn)行預(yù)占庫存?
(1)方案一:加入購物車的時候去預(yù)占庫存
(2)方案二:下單的時候去預(yù)占庫存
(3)方案三:支付的時候去預(yù)占庫存
那知道了會產(chǎn)生問題的地方,那接下來是不是就該寫“BUG”了啊
四、 分析
(1)方案一:加入購物車并不代表用戶一定會購買,如果這個時候開始預(yù)占庫存,會導(dǎo)致想購買的無法加入購物車。而不想購買的人一直占用庫存。顯然這種做法是不可取的。
(2)方案二:商品加入購物車后,選擇下單,這個時候去預(yù)占庫存。用戶選擇去支付說明了,用戶購買欲望是比 方案一 要強烈的。訂單也有一個時效,例如半個小時。超過半個小時后,系統(tǒng)自動取消訂單,回退預(yù)占庫存。
(3)方案三:下單成功去支付的時候去預(yù)占庫存。只有100個用戶能支付成功,900個用戶支付失敗。用戶體驗不好,就像你走了一條光明大道,一路通暢,突然被告知此處不通行。而且支付流程也是一個比較復(fù)雜的流程,如果和減庫存放在一起,將會變得更復(fù)雜。
所以綜上所述:選擇方案二比較合理。
五、 重復(fù)下單問題
(1)用戶點擊過快,重復(fù)提交兩次
(2)網(wǎng)絡(luò)延時,用戶刷新或者點擊下單重復(fù)提交
(3)網(wǎng)絡(luò)框架重復(fù)請求,某些網(wǎng)絡(luò)框架,在延時比較高的情況下會自動重復(fù)請求
(4)用戶惡意行為
六、 解決辦法
在UI攔截,點擊后按鈕置灰,不能繼續(xù)點擊,防止用戶,連續(xù)點擊造成的重復(fù)下單。
1、在下單前獲取一個下單的唯一token,下單的時候需要這個token。后臺系統(tǒng)校驗這個 token是否有效,才繼續(xù)進(jìn)行下單操作。
/** * 先生成 token 保存到 Redis * token 作為 key , 并設(shè)置過期時間 時間長度 根據(jù)任務(wù)需求 * value 為數(shù)字 自增判斷 是否使用過 * * @param user * @return */public String createToken(User user) { String key = "placeOrder:token:" + user.getId(); String token = UUID.randomUUID().toString(); //保存到Redis redisService.set(key + token, 0, 1000L); return token; }/** * 校驗下單的token是否有效 * @param user * @param token * @return */public Boolean checkToken(User user, String token) { String key = "placeOrder:token:" + user.getId(); if (null != redisService.get(key + token)) { long times = redisService.increment(key + token, 1); if (times == 1) { //利用increment 原子性 判斷是否 該token 是否使用 return true; } else { // 已經(jīng)使用過了 } //刪除 redisService.remove(key + token); } return false; }2、如何安全的減扣庫存?
同一個用戶或者多個用戶同時搶購一個商品的時候,我們?nèi)绾巫龅讲l(fā)安全減扣庫存?
(1)數(shù)據(jù)庫操作商品庫存
/** * Created by Administrator on 2017/9/8. */public interface ProductDao extends JpaRepository{/** * @param pid 商品ID * @param num 購買數(shù)量 * @return */@Transactional@Modifying@Query("update Product set availableNum = availableNum - ?2 , reserveNum = reserveNum + ?2 where id = ?1")intreduceStock1(Integerpid,Integernum);/** * @param pid 商品ID * @param num 購買數(shù)量 * @return */@Transactional@Modifying@Query("update Product set availableNum = availableNum - ?2 , reserveNum = reserveNum + ?2 where id = ?1 and availableNum - ?2 >= 0")intreduceStock2(Integerpid,Integernum);}(2)下單
/** * 下單操作1 * * @param req */private int place(PlaceOrderReq req) { User user = userDao.findOne(req.getUserId()); Product product = productDao.findOne(req.getProductId()); //下單數(shù)量 Integer num = req.getNum(); //可用庫存 Integer availableNum = product.getAvailableNum(); //可用預(yù)定 if (availableNum >= num) { //減庫存 int count = productDao.reduceStock1(product.getId(), num); if (count == 1) { //生成訂單 createOrders(user, product, num); } else { logger.info("庫存不足 3"); } return 1; } else { logger.info("庫存不足 4"); return -1; }}/** * 下單操作2 * * @param req */private int place2(PlaceOrderReq req) { User user = userDao.findOne(req.getUserId()); Product product = productDao.findOne(req.getProductId()); //下單數(shù)量 Integer num = req.getNum(); //可用庫存 Integer availableNum = product.getAvailableNum(); //可用預(yù)定 if (availableNum >= num) { //減庫存 int count = productDao.reduceStock2(product.getId(), num); if (count == 1) { //生成訂單 createOrders(user, product, num); } else { logger.info("庫存不足 3"); } return 1; } else { logger.info("庫存不足 4"); return -1; }}方法1:不考慮庫存安全的寫法
/** * 方法 1 * 減可用 * 加預(yù)占 * 庫存數(shù)據(jù)不安全 * * @param req */ @Override @Transactional public void placeOrder(PlaceOrderReq req) { place1(req); }分析:在高并的場景下,假設(shè)庫存只有 2 件 ,兩個請求同時進(jìn)來,搶購該商品,購買數(shù)量都是 2. A請求 此時去獲取庫存,發(fā)現(xiàn)庫存剛好足夠,執(zhí)行扣庫存下單操作。在 A 請求未完成的時候(事務(wù)未提交),B請求 此時也去獲取庫存,發(fā)現(xiàn)庫存還有2. 此時也去執(zhí)行扣庫存,下單操作。庫存剩 2 ,但是賣出了 4 。最終數(shù)據(jù)庫庫存數(shù)量將變?yōu)?-2 ,所以庫存是不安全的(關(guān)注公眾號Java面試那些事兒,回復(fù)關(guān)鍵字面試,領(lǐng)取2020年最新面試題)。
方法2:這個操作可以保證庫存數(shù)據(jù)是安全的
/** * 方法 2 * 減可用 * 加預(yù)占 * 庫存數(shù)據(jù)不安全 * * @param req */@Override @Transactional public void placeOrder(PlaceOrderReq req) { place2(req);}分析:在方法1 的基礎(chǔ)上 ,更新庫存的語句,增加了可用庫存數(shù)量 大于 0, availableNum - num >= 0 ;實質(zhì)是使用了數(shù)據(jù)庫的樂觀鎖來控制庫存安全,在并發(fā)量不是很大的情況下可以這么做。但是如果是秒殺,搶購,瞬時流量很高的話,壓力會都到數(shù)據(jù)庫,可能拖垮數(shù)據(jù)庫。
方法3:該方法也可以保證庫存數(shù)量安全
/** * 方法 3 * 采用 Redis 鎖 通一個時間 只能一個 請求修改 同一個商品的數(shù)量 ** 缺點并發(fā)不高,同時只能一個用戶搶占操作,用戶體驗不好! * * @param req */@Override public void placeOrder2(PlaceOrderReq req) { String lockKey = "placeOrder:" + req.getProductId(); Boolean isLock = redisService.lock(lockKey); if (!isLock) { logger.info("系統(tǒng)繁忙稍后再試!"); return 2; } //place2(req); place1(req); //這兩個方法都可以 redisService.unLock(lockKey);}
分析:利用Redis 分布式鎖, 強制控制 同一個商品,同時只能一個請求處理下單。其他請求返回 ‘系統(tǒng)繁忙稍后再試!’;強制把處理請求串行化,缺點并發(fā)不高 ,處理比較慢,不適合搶購等方案 。用戶體驗也不好,明明看到庫存是充足的,就是搶不到。相比方案2減輕了數(shù)據(jù)庫的壓力。
方法4 :可以保證庫存安全,滿足高并發(fā)處理,但是相對復(fù)雜一點
/** * 方法 4 * 商品的數(shù)量 等其他信息 先保存 到 Redis * 檢查庫存 與 減少庫存 不是原子性, 以 increment > 0 為準(zhǔn) * * @param req */@Override public void placeOrder3(PlaceOrderReq req) { String key = "product:" + req.getProductId(); // 先檢查 庫存是否充足 Integer num = (Integer) redisService.get(key); if (num < req.getNum()) { logger.info("庫存不足 1"); } else{ //不可在這里下單減庫存,否則導(dǎo)致數(shù)據(jù)不安全, 情況類似 方法1; } //減少庫存 long value = redisService.increment(key, -req.getNum().longValue()); //庫存充足 if (value >= 0) { logger.info("成功搶購 ! "); //TODO 真正減 扣 庫存 等操作 下單等操作,這些操作可用通過 MQ 或 其他方式 place2(req); } else { //庫存不足,需要增加剛剛減去的庫存 redisService.increment(key, req.getNum().longValue()); logger.info("庫存不足 2 "); }}分析:利用Redis increment 的原子操作,保證庫存安全。事先需要把庫存的數(shù)量等其他信息保存到Redis,并保證更新庫存的時候,更新Redis。
進(jìn)來的時候 先 get 庫存數(shù)量是否充足,再執(zhí)行 increment。以 increment > 0 為準(zhǔn)。檢查庫存 與 減少庫存 不是原子性的。檢查庫存的時候技術(shù)庫存充足也不可下單;否則造成庫存不安全,原來類似 方法1. increment 是個原子操作,以這個為準(zhǔn)。
redisService.increment(key, -req.getNum().longValue()) >= 0 說明庫存充足,可以下單。
redisService.increment(key, -req.getNum().longValue()) < 0 的時候 不能下單,次數(shù)庫存不足。并且需要 回加剛剛減去的庫存數(shù)量,否則會導(dǎo)致剛才減扣的數(shù)量 一直賣不出去。數(shù)據(jù)庫與緩存的庫存不一致。
次方法可以滿足 高并搶購等一些方案,真正減扣庫存和下單可以異步執(zhí)行。
訂單時效問題,訂單取消等 為保證商家利益,同時把商品賣給有需要的人,訂單下單成功后,往往會有個有效時間。超過這個時間,訂單取消,庫存回滾。
訂單取消后,可利用MQ 回退庫存等。
這是在開發(fā)的過程中遇到的關(guān)于訂單的庫存安全問題的時候幾種解決方案,我個人比較喜歡通過redis鎖的形式,因為公司的業(yè)務(wù)不會牽扯太大的并發(fā)量,就是一個單純的網(wǎng)上商城
好了,今天就到這里吧,希望對大家有所幫助,麻煩點贊+關(guān)注+轉(zhuǎn)發(fā),給小編一點支持,謝謝
總結(jié)
以上是生活随笔為你收集整理的redis rua解决库存问题_如何解决高并发下的库存安全问题,没你想得那么复杂(附源码)...的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C++初始化vector<vector<
- 下一篇: 商用计算机低温工作,突破量子计算机瓶颈!