搜尋
首頁web前端js教程淺談使用nodejs設計一個秒殺系統的方法

淺談使用nodejs設計一個秒殺系統的方法

Apr 21, 2021 am 09:45 AM
nodejs秒殺系統

這篇文章要跟大家介紹一下使用nodejs設計一個秒殺系統的方法。有一定的參考價值,有需要的朋友可以參考一下,希望對大家有幫助。

淺談使用nodejs設計一個秒殺系統的方法

對前端來說,「並發」場景很少遇到,本文將從常見的秒殺場景,來講講一個真實線上的node應用程式遇到「並發」將會用到什麼技術。本文範例程式碼資料庫基於MongoDB,快取基於Redis。 【相關推薦:《nodejs 教學》】

場景一:領券


規則:一個使用者只能領取一張券。

首先我們的思路是,用一個records表來保存用戶的領券記錄,用戶領券時在該表查詢是否已領取。

records架構如下

new Schema({
  // 用户id
  userId: {
    type: String,
    required: true,
  },
});

業務流程也很簡單:

淺談使用nodejs設計一個秒殺系統的方法

#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(&#39;coupon-1&#39;);
  if (result <= total) {
    this.grantCoupon();
  }
}

思考一個問題,庫存全部消耗完之後,

count欄位還會增加麼?應該如何優化?

場景三:用戶領券限制庫存限制


#規則:一個用戶只能領一張券,總庫存有限制

解析

單獨去解決“一個用戶只能領一張”或“總庫存限制”,我們都可以用原子操作去處理,當有兩個條件,那是否可以實現一個,類似原子操作將“一個用戶只能領一張”和“總庫存限制”合併操作,或者說是更類似於數據庫的“事務”

数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成

mongoDB已经从4.0开始支持事务,但这里作为演示,我们还是使用代码逻辑来控制并发

业务逻辑:

淺談使用nodejs設計一個秒殺系統的方法

代码:

async grantCoupon(userId: string) {
  const couponId = &#39;coupon-1&#39;;// 券标识
  const totalStock = 100;// 总库存
  // 查询用户是否已领过券
  const recordByFind = await this.recordModel.findOne({
    couponId,
    userId,
  });
  if (recordByFind) {
    return &#39;每位用户只能领一张&#39;;
  }
  // 查询已发放数量
  const grantedCount = await this.stockModel.findOne({
    couponId,
  });
  if (grantedCount >= totalStock) {
    return &#39;超过库存限制&#39;;
  }
  // 原子操作:已发放数量+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 &#39;超过库存限制&#39;;
  }
  // 原子操作: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 &#39;每位用户只能领一张&#39;;
  }
  // 上述条件都满足,才执行发放操作
  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中文網其他相關文章!

陳述
本文轉載於:掘金社区。如有侵權,請聯絡admin@php.cn刪除
JavaScript引擎:比較實施JavaScript引擎:比較實施Apr 13, 2025 am 12:05 AM

不同JavaScript引擎在解析和執行JavaScript代碼時,效果會有所不同,因為每個引擎的實現原理和優化策略各有差異。 1.詞法分析:將源碼轉換為詞法單元。 2.語法分析:生成抽象語法樹。 3.優化和編譯:通過JIT編譯器生成機器碼。 4.執行:運行機器碼。 V8引擎通過即時編譯和隱藏類優化,SpiderMonkey使用類型推斷系統,導致在相同代碼上的性能表現不同。

超越瀏覽器:現實世界中的JavaScript超越瀏覽器:現實世界中的JavaScriptApr 12, 2025 am 12:06 AM

JavaScript在現實世界中的應用包括服務器端編程、移動應用開發和物聯網控制:1.通過Node.js實現服務器端編程,適用於高並發請求處理。 2.通過ReactNative進行移動應用開發,支持跨平台部署。 3.通過Johnny-Five庫用於物聯網設備控制,適用於硬件交互。

使用Next.js(後端集成)構建多租戶SaaS應用程序使用Next.js(後端集成)構建多租戶SaaS應用程序Apr 11, 2025 am 08:23 AM

我使用您的日常技術工具構建了功能性的多租戶SaaS應用程序(一個Edtech應用程序),您可以做同樣的事情。 首先,什麼是多租戶SaaS應用程序? 多租戶SaaS應用程序可讓您從唱歌中為多個客戶提供服務

如何使用Next.js(前端集成)構建多租戶SaaS應用程序如何使用Next.js(前端集成)構建多租戶SaaS應用程序Apr 11, 2025 am 08:22 AM

本文展示了與許可證確保的後端的前端集成,並使用Next.js構建功能性Edtech SaaS應用程序。 前端獲取用戶權限以控制UI的可見性並確保API要求遵守角色庫

JavaScript:探索網絡語言的多功能性JavaScript:探索網絡語言的多功能性Apr 11, 2025 am 12:01 AM

JavaScript是現代Web開發的核心語言,因其多樣性和靈活性而廣泛應用。 1)前端開發:通過DOM操作和現代框架(如React、Vue.js、Angular)構建動態網頁和單頁面應用。 2)服務器端開發:Node.js利用非阻塞I/O模型處理高並發和實時應用。 3)移動和桌面應用開發:通過ReactNative和Electron實現跨平台開發,提高開發效率。

JavaScript的演變:當前的趨勢和未來前景JavaScript的演變:當前的趨勢和未來前景Apr 10, 2025 am 09:33 AM

JavaScript的最新趨勢包括TypeScript的崛起、現代框架和庫的流行以及WebAssembly的應用。未來前景涵蓋更強大的類型系統、服務器端JavaScript的發展、人工智能和機器學習的擴展以及物聯網和邊緣計算的潛力。

神秘的JavaScript:它的作用以及為什麼重要神秘的JavaScript:它的作用以及為什麼重要Apr 09, 2025 am 12:07 AM

JavaScript是現代Web開發的基石,它的主要功能包括事件驅動編程、動態內容生成和異步編程。 1)事件驅動編程允許網頁根據用戶操作動態變化。 2)動態內容生成使得頁面內容可以根據條件調整。 3)異步編程確保用戶界面不被阻塞。 JavaScript廣泛應用於網頁交互、單頁面應用和服務器端開發,極大地提升了用戶體驗和跨平台開發的靈活性。

Python還是JavaScript更好?Python還是JavaScript更好?Apr 06, 2025 am 12:14 AM

Python更适合数据科学和机器学习,JavaScript更适合前端和全栈开发。1.Python以简洁语法和丰富库生态著称,适用于数据分析和Web开发。2.JavaScript是前端开发核心,Node.js支持服务器端编程,适用于全栈开发。

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
3 週前By尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解鎖Myrise中的所有內容
4 週前By尊渡假赌尊渡假赌尊渡假赌

熱工具

MantisBT

MantisBT

Mantis是一個易於部署的基於Web的缺陷追蹤工具,用於幫助產品缺陷追蹤。它需要PHP、MySQL和一個Web伺服器。請查看我們的演示和託管服務。

MinGW - Minimalist GNU for Windows

MinGW - Minimalist GNU for Windows

這個專案正在遷移到osdn.net/projects/mingw的過程中,你可以繼續在那裡關注我們。 MinGW:GNU編譯器集合(GCC)的本機Windows移植版本,可自由分發的導入函式庫和用於建置本機Windows應用程式的頭檔;包括對MSVC執行時間的擴展,以支援C99功能。 MinGW的所有軟體都可以在64位元Windows平台上運作。

ZendStudio 13.5.1 Mac

ZendStudio 13.5.1 Mac

強大的PHP整合開發環境

EditPlus 中文破解版

EditPlus 中文破解版

體積小,語法高亮,不支援程式碼提示功能

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境