Java生鮮電商平臺-高并發核心技術訂單與庫存實戰

Java生鮮電商平臺-高并發核心技術訂單與庫存實戰

?

一、 問題

一件商品只有100個庫存,現在有1000或者更多的用戶來購買,每個用戶計劃同時購買1個到幾個不等商品。

如何保證庫存在高并發的場景下是安全的?

(1)不多發

(2)不少發

?

二、 下單的步驟

(1)下單

(2)下單同時預占庫存

(3)支付

(4)支付成功真正減扣庫存

(5)取消訂單

(6)回退預占庫存

三、 什么時候進行預占庫存?

(1)方案一:加入購物車的時候去預占庫存

(2)方案二:下單的時候去預占庫存

(3)方案三:支付的時候去預占庫存

四、 分析

(1)方案一:加入購物車并不代表用戶一定會購買,如果這個時候開始預占庫存,會導致想購買的無法加入購物車。而不想購買的人一直占用庫存。顯然這種做法是不可取的。

(2)方案二:商品加入購物車后,選擇下單,這個時候去預占庫存。用戶選擇去支付說明了,用戶購買欲望是比 方案一 要強烈的。訂單也有一個時效,例如半個小時。超過半個小時后,系統自動取消訂單,回退預占庫存。

(3)方案三:下單成功去支付的時候去預占庫存。只有100個用戶能支付成功,900個用戶支付失敗。用戶體驗不好,就像你走了一條光明大道,一路通暢,突然被告知此處不通行。而且支付流程也是一個比較復雜的流程,如果和減庫存放在一起,將會變的更復雜。

所以綜上所述: 選擇方案二比較合理。

五、 重復下單問題

(1)用戶點擊過快,重復提交兩次

(2)網絡延時,用戶刷新或者點擊下單重復提交

(3)網絡框架重復請求,某些網絡框架,在延時比較高的情況下會自動重復請求

(4)用戶惡意行為

六、 解決辦法

在UI攔截,點擊后按鈕置灰,不能繼續點擊,防止用戶,連續點擊造成的重復下單。

1、在下單前獲取一個下單的唯一token,下單的時候需要這個token。后臺系統校驗這個 token是否有效,才繼續進行下單操作。

/**? ? ?

* 先生成 token 保存到 Redis? ? ?

* token 作為 key , 并設置過期時間 時間長度 根據任務需求? ? ?

* value 為數字 自增判斷 是否使用過? ? ? *? ? ?

* @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 {

// 已經使用過了

}

//刪除

redisService.remove(key + token);

}

return false; }

2、如何安全的減扣庫存?

同一個用戶或者多個用戶同時搶購一個商品的時候,我們如何做到并發安全減扣庫存?

(1)數據庫操作商品庫存

/**? * Created by Administrator on 2017/9/8.? */

public interface ProductDao extends JpaRepository<Product,Integer>{

/**? ? ? * @param pid 商品ID? ? ??

* @param num 購買數量? ? ??

* @return? ? ??

*/

@Transactional

@Modifying

@Query("update Product set availableNum = availableNum - ?2 , reserveNum = reserveNum + ?2 where id = ?1")intreduceStock1(Integerpid,Integernum);

/**? ? ?

?* @param pid 商品ID? ? ?

?* @param num 購買數量? ? ?

?* @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());

//下單數量

Integer num = req.getNum();

//可用庫存

Integer availableNum = product.getAvailableNum();

//可用預定

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());

//下單數量 Integer num = req.getNum();

//可用庫存

Integer availableNum = product.getAvailableNum();

//可用預定

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? ? ? * 減可用? ? ? * 加預占? ? ? * 庫存數據不安全? ? ? *? ? ? * @param req? ? ? */? ?

@Override? ?

@Transactional? ?

public void placeOrder(PlaceOrderReq req) {? ? ? ?

place1(req);? ?

}

分析:?在高并的場景下,假設庫存只有 2 件 ,兩個請求同時進來,搶購改商品,購買數量都是 2. A請求 此時去獲取庫存,發現庫存剛好足夠,執行扣庫存下單操作。 在 A 請求為完成的時候(事務未提交),B請求 此時也去獲取庫存,發現庫存還有2. 此時也去執行扣庫存,下單操作。庫存剩 2 ,但是賣出了 4 。最終數據庫庫存數量將變為 -2 ,所以庫存是不安全的。

方法2:這個操作可以保證庫存數據是安全的

/**? ? ? * 方法 2? ? ? * 減可用? ? ? * 加預占? ? ? * 庫存數據不安全? ? ? *? ? ? * @param req? ? ? */

@Override? ?

@Transactional? ?

public void placeOrder(PlaceOrderReq req) {

place2(req);

}

分析:?在方法1 的基礎上 ,更新庫存的語句,增加了可用庫存數量 大于 0, availableNum - num >= 0 ;實質是使用了數據庫的樂觀鎖來控制庫存安全,在并發量不是很大的情況下可以這么做。但是如果是秒殺,搶購,瞬時流量很高的話,壓力會都到數據庫,可能拖垮數據庫。

方法3:該方法也可以保證庫存數量安全

/**? ?

* 方法 3? ? ?

* 采用 Redis 鎖? 通一個時間 只能一個 請求修改 同一個商品的數量? ? ?

* <p>? ? ?

* 缺點并發不高,同時只能一個用戶搶占操作,用戶體驗不好!? ? ? *? ? ?

* @param req? ? ?

*/

@Override? ?

public void placeOrder2(PlaceOrderReq req) {

String lockKey = "placeOrder:" + req.getProductId();

Boolean isLock = redisService.lock(lockKey);

if (!isLock) {

logger.info("系統繁忙稍后再試!");

return 2;

}

//place2(req); place1(req);

//這兩個方法都可以

redisService.unLock(lockKey);

}

分析:利用Redis 分布式鎖, 強制控制 同一個商品,同時只能一個請求處理下單。 其他請求返回 ‘系統繁忙稍后再試!’; 強制把處理請求串行化,缺點并發不高 ,處理比較慢,不適合搶購等方案 。 用戶體驗也不好,明明看到庫存是充足的,就是強不到。 相比方案2減輕了數據庫的壓力。

?

方法4?:可以保證庫存安全,滿足高并發處理,但是相對復雜一點

/**? ? ?

* 方法 4? ? ?

* 商品的數量 等其他信息 先保存 到 Redis? ? ?

* 檢查庫存 與 減少庫存 不是原子性,? 以 increment > 0 為準? ? ? *? ? ?

* @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{

//不可在這里下單減庫存,否則導致數據不安全, 情況類似 方法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 的原子操作,保證庫存安全。 事先需要把庫存的數量等其他信息保存到Redis,并保證更新庫存的時候,更新Redis。

進來的時候 先 get 庫存數量是否充足,再執行 increment。以 increment > 0 為準。 檢查庫存 與 減少庫存 不是原子性的。 檢查庫存的時候技術庫存充足也不可下單;否則造成庫存不安全,原來類似 方法1. increment 是個原子操作,已這個為準。

redisService.increment(key, -req.getNum().longValue()) >= 0 說明庫存充足,可以下單。

redisService.increment(key, -req.getNum().longValue()) < 0 的時候 不能下單,次數庫存不足。并且需要 回加剛剛減去的庫存數量,否則會導致剛才減扣的數量 一直買不出去。數據庫與緩存的庫存不一致。

次方法可以滿足 高并搶購等一些方案,真正減扣庫存和下單可以異步執行。

訂單時效問題,訂單取消等 為保證商家利益,同時把商品賣給有需要的人,訂單下單成功后,往往會有個有效時間。超過這個時間,訂單取消,庫存回滾。

訂單取消后,可利用MQ 回退庫存等。

轉載于:https://www.cnblogs.com/jurendage/p/11240172.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/247371.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/247371.shtml
英文地址,請注明出處:http://en.pswp.cn/news/247371.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Vue2 MVVM 雙向綁定(數據劫持+發布者-訂閱者模式)

參考文獻&#xff1a;https://www.cnblogs.com/libin-1/p/6893712.html https://juejin.im/post/5b2f0769e51d45589f46949e MVVM拆開來即為Model-View-ViewModel&#xff0c;有View&#xff0c;ViewModel&#xff0c;Model三部分組成。View層代表的是視圖、模版&#xff0c;負…

常用的激活函數

1.階躍函數 &#xff0c;值域{0,1} 1 def step_function(x): 2 return np.array(x>0,dtypenp.int) 2.sigmoid函數 &#xff0c;值域(0,1) 1 def sigmoid(x): 2 return 1/(1np.exp(-x)) 3.relu函數 &#xff0c;值域[0&#xff0c;∞&#xff09; 1 def relu(x): 2 …

前端優化-vue-cli4安裝webpack-bundle-analyzer分析包文件

使用vue-cli3創建了一個工程目錄&#xff0c;技術棧為vue-cli3vue-routervuexelement-uiv-chartsaxios。但是等到項目開發完后&#xff0c;發現生成的app.js特別大&#xff0c;接近10M。為了優化項目性能&#xff0c;需要使用webpack-bundle-analyzer分析包文件&#xff0c;找出…

今天剛查到的宏,學習

PVP常用的宏&#xff1d;&#xff1d;&#xff1d;&#xff1d;&#xff1d;&#xff1d;&#xff1d;&#xff1d;&#xff1d;&#xff1d;&#xff1d;&#xff1d;&#xff1d;&#xff1d;&#xff1d;&#xff1d;&#xff1d;&#xff1d;&#xff1d;&#xff1d;&…

不要再問我三次握手和四次揮手

三次握手和四次揮手是各個公司常見的考點&#xff0c;也具有一定的水平區分度&#xff0c;也被一些面試官作為熱身題。很多小伙伴說這個問題剛開始回答的挺好&#xff0c;但是后面越回答越冒冷汗&#xff0c;最后就歇菜了。 見過比較典型的面試場景是這樣的: 面試官&#xff1…

Apache ServiceComb

Apache ServiceComb 開源&#xff0c;全棧微服務解決方案。開箱即用&#xff0c;高性能&#xff0c;兼容流行的生態&#xff0c;多語言支持 ServiceComb是一個微服務框架&#xff0c;提供服務注冊&#xff0c;發現&#xff0c;配置和管理實用程序。 下載 &#xff1a;http://se…

VScode PowerShell運行腳本報錯禁止運行腳本解決方式圖文

今天在新Windows電腦上用VScode的終端PowerShell運行一個腳本的時候&#xff0c; 錯誤 在vscode終端運行vue -V查看版本失敗 PS C:\Users\11388> vue -V vue : 無法加載文件 E:\NodeJs\node_global\vue.ps1&#xff0c;因為在此系統上禁止運行腳本。有關詳細信息&#xf…

多線程的創建方式---繼承Thread和實現Runnable

繼承Thread類創建多線程 1 package cn.ftf.thread;2 /**3 * 多線程實現方式一 繼承Thread實現多線程&#xff0c;繼承Thread&#xff0c;重寫run方法4 * author 房廷飛5 *6 */7 public class StartThread extends Thread{ //對象繼承Thread8 public static void mai…

添加右鍵用Sublime Text3 打開文件和文件夾

最近重新裝了一下系統&#xff0c;右鍵沒有用Sublime Text 3打開的選項了&#xff0c;于是查了一下解決方案 1、環境 Win10和Win7都可以Sublime Text 3最新版本以下為Win10系統下截圖 2、添加右鍵打開文件 Win R&#xff0c;輸入regedit,打開注冊表 找到HKEY_CLASSESS_ROOT…

Windows Mobile Widget Emulator

今天Vimpyboy 在codeplex發布了Windows Mobile Widget Emulator。這是一個用來調試Windows Mobile 6.5 Widget的工具&#xff0c;我在做Windows Mobile 6.5 新功能widget開發 的時候就發現調試Widget很麻煩。也有想法做一個Emulator&#xff0c;其實這個Emulator目標很明顯&…

JS 常用字符串數組遍歷函數方法整理

目錄 一、concat() 二、join() 三、push() 四、pop() 五、shift() 六、unshift() 七、slice() 九、substring() 和 substr() 十、sort 排序 十一、reverse() 十二、indexOf 和 lastIndexOf 十三、every 十四、some 十五、filter 十六、map ES6新增新操作數組的…

AttributeError: Can only use .str accessor with string values, which use np.object_ dtype in pandas

忘記網址了…… 問題&#xff1a; 分析思路與解決方法&#xff1a; 轉載于:https://www.cnblogs.com/bravesunforever/p/11247988.html

vue app掃PC端二維碼登錄

通過接口獲取二維碼唯一標識&#xff0c;例如&#xff1a;qrcodeId通過 qrcodejs2插件生成 二維碼&#xff08;二維碼內容就是 qrcodeId&#xff0c;具體根據APP 需要&#xff09;循環調用接口&#xff0c;查看掃碼狀態&#xff08;app是否掃碼確認登錄&#xff09; //下載插件…

第八章 方法

1. 概述 本章重點講述類型中的各種方法&#xff0c;包括實例構造器、類型構造器、操作符/類型轉換重載、擴展方法、分部方法。 2. 名詞解釋 ① 構造器&#xff1a;是允許將類型的實例初始化為良好狀態的一種特殊方法。 3. 主要內容 3.1 實例構造器和類(引用類型) ① 創建一個引…

Java生鮮電商平臺-促銷架構以及秒殺解決方案實戰

Java生鮮電商平臺-促銷架構以及秒殺解決方案實戰 背景:隨著這幾年的電商的大熱,我們經常看到一些商家為了促銷和快速收益,紛紛推出了秒殺活動.不管是日常的超市里面的促銷,明星演唱會門票售賣,還是春節訂閱火車票,等等我們都能看到秒殺活動的影子. 1. 構建秒殺活動架構 1.1 說明…

PHP---錯誤處理(error)

錯誤的級別 1. notice&#xff1a;提示2. warning&#xff1a;警告3. error&#xff1a;致命錯誤12345 注&#xff1a;notice和warning報錯后繼續執行&#xff0c;error報錯后停止 錯誤的提示方法 方法一&#xff1a;顯示在瀏覽器上 方法二&#xff1a;記錄在日志中執行 錯…

對url給后臺傳數據的時候特殊字符需要轉義

URL中的字符只能是ASCII字符&#xff0c;但是ASCII字符比較少&#xff0c;而URL則常常包含ASCII字符集以外的字符&#xff0c;如非英語字符&#xff0c;漢字&#xff0c;特殊符號等等&#xff0c;所以要對URL進行轉換。這個過程就叫做URL編碼&#xff0c;或者叫URL轉義&#xf…

PHP Cookie處理

Cookie 是什么&#xff1f; cookie是保存在客戶端的信息包&#xff08;一個文件&#xff09; cookie 常用于識別用戶。 cookie 是一種服務器留在用戶計算機上的小文件。每當同一臺計算機通過瀏覽器請求頁面時&#xff0c;這臺計算機將會發送 cookie。通過 PHP&#xff0c;您能…

python裝飾器補充

帶參裝飾器 msg """ 1.QQ 2.wechat """ avg input(驗證方式&#xff1a;)def auth(avg):def wrapper(f):def inner(*args,**kwargs):if avg QQ:user input(name)pwd input(password)if user alex and pwd 123456:f()else:print(輸入錯誤)…

PHP-連接數據庫

1.2 連接數據庫 通過PHP做MySQL的客戶端 1.2.1 開啟mysqli擴展 在php.ini中開啟mysqli擴展 extensionphp_mysqli.dll開啟擴展后重啟服務器&#xff0c;就可以使用mysqli_函數了&#xff0c;1.2.2 連接數據庫 創建news數據庫 -- 創建表 drop table if exists news; create …