這篇文章要跟大家介紹一下使用nodejs設計一個秒殺系統的方法。有一定的參考價值,有需要的朋友可以參考一下,希望對大家有幫助。
對前端來說,「並發」場景很少遇到,本文將從常見的秒殺場景,來講講一個真實線上的node應用程式遇到「並發」將會用到什麼技術。本文範例程式碼資料庫基於MongoDB,快取基於Redis。 【相關推薦:《nodejs 教學》】
規則:一個使用者只能領取一張券。
首先我們的思路是,用一個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沒有值,則將值設為進去,如果已有值就不做處理,提示失敗。這裡只是示範並發處理,實際線上服務還需要考慮:
套用名稱功能名稱userId
#規則:券總庫存一定,單一使用者不限領取數量
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用户后续请求的反而显示还有库存,成功抢到券,当然这只是理论上可能存在的情况。
设计一个秒杀系统,其实还要考虑很多情况。如大型电商的秒杀活动,一次有几万的并发请求,服务器可能都支撑不住,可能会再网关层直接舍弃部分用户请求,减少服务器压力,或结合kafka消息队列,或使用动态扩容等技术。
更多编程相关知识,请访问:编程入门!!
以上是淺談使用nodejs設計一個秒殺系統的方法的詳細內容。更多資訊請關注PHP中文網其他相關文章!