溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何使用nodejs設(shè)計一個秒殺系統(tǒng)的方法

發(fā)布時間:2021-04-21 11:37:43 來源:億速云 閱讀:269 作者:小新 欄目:web開發(fā)

小編給大家分享一下如何使用nodejs設(shè)計一個秒殺系統(tǒng)的方法,希望大家閱讀完這篇文章之后都有所收獲,下面讓我們一起去探討吧!

js的作用是什么

1、能夠嵌入動態(tài)文本于HTML頁面。2、對瀏覽器事件做出響應(yīng)。3、讀寫HTML元素。4、在數(shù)據(jù)被提交到服務(wù)器之前驗證數(shù)據(jù)。5、檢測訪客的瀏覽器信息。6、控制cookies,包括創(chuàng)建和修改等。7、基于Node.js技術(shù)進(jìn)行服務(wù)器端編程。

對于前端來說,“并發(fā)”場景很少遇到,本文將從常見的的秒殺場景,來講講一個真實線上的node應(yīng)用遇到“并發(fā)”將會用到什么技術(shù)。本文示例代碼數(shù)據(jù)庫基于MongoDB,緩存基于Redis

場景一:領(lǐng)券


規(guī)則:一個用戶只能領(lǐng)取一張券。

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

records結(jié)構(gòu)如下

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

業(yè)務(wù)流程也很簡單:

如何使用nodejs設(shè)計一個秒殺系統(tǒng)的方法

MongoDB實現(xiàn)

示例代碼如下:

  async grantCoupon(userId: string) {
    const record = await this.recordsModel.findOne({
      userId,
    });
    if (record) {
      return false;
    } else {
      this.grantCoupon();
      this.recordModel.create({
        userId,
      });
    }
  }

postman測試一下,好像沒問題。然后我們考慮并發(fā)場景,比如“用戶”并不會乖乖的點一下按鈕等待發(fā)券,而是快速點擊,又或者使用工具并發(fā)請求領(lǐng)券接口,我們的程序會出問題么?(并發(fā)問題前端可以用loading來規(guī)避,但是接口必要攔截住,防止黑客攻擊)

結(jié)果是,用戶可能會領(lǐng)取到多張券。問題就出在查詢records新增領(lǐng)券記錄,這兩步是分開進(jìn)行的,也就是存在一個時間點:查詢到用戶A無領(lǐng)券記錄,發(fā)券后A用戶又請求一次接口,此時records表數(shù)據(jù)插入操作還未完成,導(dǎo)致重復(fù)發(fā)放問題。

解決也很容易,就是如何讓查詢和插入語句一起執(zhí)行,消除中間的異步過程。mongoose為我們提供了findOneAndUpdate,即查找并修改,下面看一下改寫后的語句:

async grantCoupon(userId: string) {
  const record = await this.recordModel.findOneAndUpdate({
    userId,
  }, {
    $setOnInsert: {
      userId,
    },
  }, {
    new: false,
    upsert: true,
  });
  if (! record) {
    this.grantCoupon();
  }
}

實際上這是一個mongo的原子操作,第一個參數(shù)是查詢語句,查詢userId的條目,第二個參數(shù)$setOnInsert表示新增的時候插入的字段,第三個參數(shù)upsert=true表示如果查詢的條目不存在,將新建它,new=false表示返回查詢的條目而不是修改后的條目。那我們只用判斷查詢的record不存在,就執(zhí)行發(fā)放邏輯,而插入語句是和查詢語句一起執(zhí)行的。即使此時有并發(fā)請求進(jìn)來,下一次查詢是在上次插入語句之后了。

原子(atomic),本意是指“不能被進(jìn)一步分割的粒子”。原子操作意味著“不可被中斷的一個或一系列操作”,兩個原子操作不可能同時作用于同一個變量。

Redis實現(xiàn)

不止MongoDB,redis也很適合這種邏輯,下面用redis實現(xiàn)一下:

async grantCoupon(userId: string) {
  const result = await this.redis.setnx(userId, 'true');
  if (result === 1) {
    this.grantCoupon();
  }
}

同樣setnx是redis的一個原子操作,表示:如果key沒有值,則將值設(shè)置進(jìn)去,如果已有值就不做處理,提示失敗。這里只是演示并發(fā)處理,實際線上服務(wù)還需要考慮:

  • key值不能與其他應(yīng)用沖突使用,如應(yīng)用名稱+功能名稱+userId

  • 服務(wù)下線后redis的key需要清理,或者直接在setnx第三個參數(shù)加上過期時間

  • redis數(shù)據(jù)只在內(nèi)存中,發(fā)券記錄需要入庫保存

場景二:庫存限制


規(guī)則:券總庫存一定,單個用戶不限領(lǐng)取數(shù)量

有了上面的示例,類似并發(fā)也很好實現(xiàn),直接上代碼

MongoDB實現(xiàn)

使用stocks表來記錄券的發(fā)放數(shù)量,當(dāng)然我們需要一個couponId字段去標(biāo)識這條記錄

表結(jié)構(gòu):

new Schema({
  /* 券標(biāo)識 */
  couponId: {
    type: String,
    required: true,
  },
  /* 已發(fā)放數(shù)量 */
  count: {
    type: Number,
    default: 0,
  },
});

發(fā)放邏輯:

async grantCoupon(userId: string) {
  const couponId = 'coupon-1'; // 券標(biāo)識
  const total = 100; // 總庫存
  const result = await this.stockModel.findOneAndUpdate({
    couponId,
  }, {
    $inc: {
      count: 1,
    },
    $setOnInsert: {
      couponId,
    },
  }, {
    new: true, // 返回modify后結(jié)果
    upsert: true, // 不存在則新增
  });
  if (result.count <= total) {
    this.grantCoupon();
  }
}

Redis實現(xiàn)

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字段還會增加么?應(yīng)該如何優(yōu)化?

場景三:用戶領(lǐng)券限制+庫存限制


規(guī)則:一個用戶只能領(lǐng)一張券,總庫存有限制

解析

單獨去解決“一個用戶只能領(lǐng)一張”或“總庫存限制”,我們都可以用原子操作去處理,當(dāng)有兩個條件,那是否可以實現(xiàn)一個,類似原子操作將“一個用戶只能領(lǐng)一張”和“總庫存限制”合并操作,或者說是更類似于數(shù)據(jù)庫的“事務(wù)”

數(shù)據(jù)庫事務(wù)( transaction)是訪問并可能操作各種數(shù)據(jù)項的一個數(shù)據(jù)庫操作序列,這些操作要么全部執(zhí)行,要么全部不執(zhí)行,是一個不可分割的工作單位。事務(wù)由事務(wù)開始與事務(wù)結(jié)束之間執(zhí)行的全部數(shù)據(jù)庫操作組成

mongoDB已經(jīng)從4.0開始支持事務(wù),但這里作為演示,我們還是使用代碼邏輯來控制并發(fā)

業(yè)務(wù)邏輯:

如何使用nodejs設(shè)計一個秒殺系統(tǒng)的方法

代碼:

async grantCoupon(userId: string) {
  const couponId = 'coupon-1';// 券標(biāo)識
  const totalStock = 100;// 總庫存
  // 查詢用戶是否已領(lǐng)過券
  const recordByFind = await this.recordModel.findOne({
    couponId,
    userId,
  });
  if (recordByFind) {
    return '每位用戶只能領(lǐng)一張';
  }
  // 查詢已發(fā)放數(shù)量
  const grantedCount = await this.stockModel.findOne({
    couponId,
  });
  if (grantedCount >= totalStock) {
    return '超過庫存限制';
  }
  // 原子操作:已發(fā)放數(shù)量+1,并返回+1后的結(jié)果
  const result = await this.stockModel.findOneAndUpdate({
    couponId,
  }, {
    $inc: {
      count: 1,
    },
    $setOnInsert: {
      couponId,
    },
  }, {
    new: true, // 返回modify后結(jié)果
    upsert: true, // 如果不存在就新增
  });
  // 根據(jù)+1后的的結(jié)果判斷是否超出庫存
  if (result.count > totalStock) {
    // 超出后執(zhí)行-1操作,保證數(shù)據(jù)庫中記錄的已發(fā)放數(shù)量準(zhǔn)確。
    this.stockModel.findOneAndUpdate({
      couponId,
    }, {
      $inc: {
        count: -1,
      },
    });
    return '超過庫存限制';
  }
  // 原子操作:records表新增用戶領(lǐng)券記錄,并返回新增前的查詢結(jié)果
  const recordBeforeModify = await this.recordModel.findOneAndUpdate({
    couponId,
    userId,
  }, {
    $setOnInsert: {
      userId,
    },
  }, {
    new: false, // 返回modify后結(jié)果
    upsert: true, // 如果不存在就新增
  });
  if (recordBeforeModify) {
    // 超出后執(zhí)行-1操作,保證數(shù)據(jù)庫中記錄的已發(fā)放數(shù)量準(zhǔn)確。
    this.stockModel.findOneAndUpdate({
      couponId,
    }, {
      $inc: {
        count: -1,
      },
    });
    return '每位用戶只能領(lǐng)一張';
  }
  // 上述條件都滿足,才執(zhí)行發(fā)放操作
  this.grantCoupon();
}

其實我們可以舍去前兩部查詢records記錄和查詢庫存數(shù)量,結(jié)果并不會出問題。從數(shù)據(jù)庫優(yōu)化來說,顯然更改比查詢更耗時,而且?guī)齑嬗邢?,最終庫存消耗完,后面請求都會在前兩步邏輯中走完。

  • 什么情況下會走到第3步的左分支?

場景舉例:庫存僅剩1個,此時用戶A和用戶B同時請求,此時A稍快一點,庫存+1后=100,B庫存+1=101;

  • 什么情況下會走到第4步的左分支?

場景舉例:A用戶同時發(fā)出兩個請求,庫存+1后均小于100,則稍快的一次請求會成功,另一個會查詢到已有領(lǐng)券記錄

  • 思考:什么情況下會出現(xiàn),先請求的用戶沒搶到券,反而靠后的用戶能搶到券?

庫存還剩4個,A用戶發(fā)起大量請求,最終導(dǎo)致數(shù)據(jù)庫記錄的已發(fā)放庫存大于100,-1操作還全部執(zhí)行完成,而此時B、C、D用戶也同時請求,則會返回超出庫存,待到庫存回滾操作完成,E、F、G用戶后續(xù)請求的反而顯示還有庫存,成功搶到券,當(dāng)然這只是理論上可能存在的情況。

看完了這篇文章,相信你對“如何使用nodejs設(shè)計一個秒殺系統(tǒng)的方法”有了一定的了解,如果想了解更多相關(guān)知識,歡迎關(guān)注億速云行業(yè)資訊頻道,感謝各位的閱讀!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI