您好,登錄后才能下訂單哦!
小編給大家分享一下如何使用nodejs設計一個秒殺系統的方法,希望大家閱讀完這篇文章之后都有所收獲,下面讓我們一起去探討吧!
1、能夠嵌入動態文本于HTML頁面。2、對瀏覽器事件做出響應。3、讀寫HTML元素。4、在數據被提交到服務器之前驗證數據。5、檢測訪客的瀏覽器信息。6、控制cookies,包括創建和修改等。7、基于Node.js技術進行服務器端編程。
對于前端來說,“并發”場景很少遇到,本文將從常見的的秒殺場景,來講講一個真實線上的node應用遇到“并發”將會用到什么技術。本文示例代碼數據庫基于MongoDB,緩存基于Redis。
規則:一個用戶只能領取一張券。
首先我們的思路是,用一個records表來保存用戶的領券記錄,用戶領券時在該表查詢是否已領取。
records結構如下
new Schema({ // 用戶id userId: { type: String, required: true, }, });
業務流程也很簡單:
MongoDB實現
示例代碼如下:
async grantCoupon(userId: string) { const record = await this.recordsModel.findOne({ userId, }); if (record) { return false; } else { this.grantCoupon(); this.recordModel.create({ userId, }); } }
postman測試一下,好像沒問題。然后我們考慮并發場景,比如“用戶”并不會乖乖的點一下按鈕等待發券,而是快速點擊,又或者使用工具并發請求領券接口,我們的程序會出問題么?(并發問題前端可以用loading來規避,但是接口必要攔截住,防止黑客攻擊)
結果是,用戶可能會領取到多張券。問題就出在查詢records
與新增領券記錄
,這兩步是分開進行的,也就是存在一個時間點:查詢到用戶A無領券記錄,發券后A用戶又請求一次接口,此時records表數據插入操作還未完成,導致重復發放問題。
解決也很容易,就是如何讓查詢和插入語句一起執行,消除中間的異步過程。mongoose為我們提供了findOneAndUpdate
,即查找并修改,下面看一下改寫后的語句:
async grantCoupon(userId: string) { const record = await this.recordModel.findOneAndUpdate({ userId, }, { $setOnInsert: { userId, }, }, { new: false, upsert: true, }); if (! record) { this.grantCoupon(); } }
實際上這是一個mongo的原子操作,第一個參數是查詢語句,查詢userId的條目,第二個參數$setOnInsert表示新增的時候插入的字段,第三個參數upsert=true表示如果查詢的條目不存在,將新建它,new=false表示返回查詢的條目而不是修改后的條目。那我們只用判斷查詢的record不存在,就執行發放邏輯,而插入語句是和查詢語句一起執行的。即使此時有并發請求進來,下一次查詢是在上次插入語句之后了。
原子(atomic),本意是指“不能被進一步分割的粒子”。原子操作意味著“不可被中斷的一個或一系列操作”,兩個原子操作不可能同時作用于同一個變量。
Redis實現
不止MongoDB,redis也很適合這種邏輯,下面用redis實現一下:
async grantCoupon(userId: string) { const result = await this.redis.setnx(userId, 'true'); if (result === 1) { this.grantCoupon(); } }
同樣setnx是redis的一個原子操作,表示:如果key沒有值,則將值設置進去,如果已有值就不做處理,提示失敗。這里只是演示并發處理,實際線上服務還需要考慮:
key值不能與其他應用沖突使用,如應用名稱+功能名稱+userId
服務下線后redis的key需要清理,或者直接在setnx第三個參數加上過期時間
redis數據只在內存中,發券記錄需要入庫保存
規則:券總庫存一定,單個用戶不限領取數量
有了上面的示例,類似并發也很好實現,直接上代碼
MongoDB實現
使用stocks
表來記錄券的發放數量,當然我們需要一個couponId字段去標識這條記錄
表結構:
new Schema({ /* 券標識 */ couponId: { type: String, required: true, }, /* 已發放數量 */ count: { type: Number, default: 0, }, });
發放邏輯:
async grantCoupon(userId: string) { const couponId = 'coupon-1'; // 券標識 const total = 100; // 總庫存 const result = await this.stockModel.findOneAndUpdate({ couponId, }, { $inc: { count: 1, }, $setOnInsert: { couponId, }, }, { new: true, // 返回modify后結果 upsert: true, // 不存在則新增 }); if (result.count <= total) { this.grantCoupon(); } }
Redis實現
incr: 原子操作,將key的值+1,如果值不存在,將初始化為0;
async grantCoupon(userId: string) { const total = 100; // 總庫存 const result = await this.redis.incr('coupon-1'); if (result <= total) { this.grantCoupon(); } }
思考一個問題,庫存全部消耗完后,count
字段還會增加么?應該如何優化?
規則:一個用戶只能領一張券,總庫存有限制
解析
單獨去解決“一個用戶只能領一張”或“總庫存限制”,我們都可以用原子操作去處理,當有兩個條件,那是否可以實現一個,類似原子操作將“一個用戶只能領一張”和“總庫存限制”合并操作,或者說是更類似于數據庫的“事務”
數據庫事務( transaction)是訪問并可能操作各種數據項的一個數據庫操作序列,這些操作要么全部執行,要么全部不執行,是一個不可分割的工作單位。事務由事務開始與事務結束之間執行的全部數據庫操作組成
mongoDB已經從4.0開始支持事務,但這里作為演示,我們還是使用代碼邏輯來控制并發
業務邏輯:
代碼:
async grantCoupon(userId: string) { const couponId = 'coupon-1';// 券標識 const totalStock = 100;// 總庫存 // 查詢用戶是否已領過券 const recordByFind = await this.recordModel.findOne({ couponId, userId, }); if (recordByFind) { return '每位用戶只能領一張'; } // 查詢已發放數量 const grantedCount = await this.stockModel.findOne({ couponId, }); if (grantedCount >= totalStock) { return '超過庫存限制'; } // 原子操作:已發放數量+1,并返回+1后的結果 const result = await this.stockModel.findOneAndUpdate({ couponId, }, { $inc: { count: 1, }, $setOnInsert: { couponId, }, }, { new: true, // 返回modify后結果 upsert: true, // 如果不存在就新增 }); // 根據+1后的的結果判斷是否超出庫存 if (result.count > totalStock) { // 超出后執行-1操作,保證數據庫中記錄的已發放數量準確。 this.stockModel.findOneAndUpdate({ couponId, }, { $inc: { count: -1, }, }); return '超過庫存限制'; } // 原子操作:records表新增用戶領券記錄,并返回新增前的查詢結果 const recordBeforeModify = await this.recordModel.findOneAndUpdate({ couponId, userId, }, { $setOnInsert: { userId, }, }, { new: false, // 返回modify后結果 upsert: true, // 如果不存在就新增 }); if (recordBeforeModify) { // 超出后執行-1操作,保證數據庫中記錄的已發放數量準確。 this.stockModel.findOneAndUpdate({ couponId, }, { $inc: { count: -1, }, }); return '每位用戶只能領一張'; } // 上述條件都滿足,才執行發放操作 this.grantCoupon(); }
其實我們可以舍去前兩部查詢records記錄和查詢庫存數量,結果并不會出問題。從數據庫優化來說,顯然更改比查詢更耗時,而且庫存有限,最終庫存消耗完,后面請求都會在前兩步邏輯中走完。
什么情況下會走到第3步的左分支?
場景舉例:庫存僅剩1個,此時用戶A和用戶B同時請求,此時A稍快一點,庫存+1后=100,B庫存+1=101;
什么情況下會走到第4步的左分支?
場景舉例:A用戶同時發出兩個請求,庫存+1后均小于100,則稍快的一次請求會成功,另一個會查詢到已有領券記錄
思考:什么情況下會出現,先請求的用戶沒搶到券,反而靠后的用戶能搶到券?
庫存還剩4個,A用戶發起大量請求,最終導致數據庫記錄的已發放庫存大于100,-1操作還全部執行完成,而此時B、C、D用戶也同時請求,則會返回超出庫存,待到庫存回滾操作完成,E、F、G用戶后續請求的反而顯示還有庫存,成功搶到券,當然這只是理論上可能存在的情況。
看完了這篇文章,相信你對“如何使用nodejs設計一個秒殺系統的方法”有了一定的了解,如果想了解更多相關知識,歡迎關注億速云行業資訊頻道,感謝各位的閱讀!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。