真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

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

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

創(chuàng)新互聯(lián)公司科技有限公司專業(yè)互聯(lián)網(wǎng)基礎(chǔ)服務(wù)商,為您提供遂寧服務(wù)器托管,高防服務(wù)器,成都IDC機(jī)房托管,成都主機(jī)托管等互聯(lián)網(wǎng)服務(wù)。

js的作用是什么

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

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

場景一:領(lǐng)券


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

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

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

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

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

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

MongoDB實(shí)現(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ā)場景,比如“用戶”并不會乖乖的點(diǎn)一下按鈕等待發(fā)券,而是快速點(diǎn)擊,又或者使用工具并發(fā)請求領(lǐng)券接口,我們的程序會出問題么?(并發(fā)問題前端可以用loading來規(guī)避,但是接口必要攔截住,防止黑客攻擊)

結(jié)果是,用戶可能會領(lǐng)取到多張券。問題就出在查詢r(jià)ecords新增領(lǐng)券記錄,這兩步是分開進(jìn)行的,也就是存在一個(gè)時(shí)間點(diǎn):查詢到用戶A無領(lǐng)券記錄,發(fā)券后A用戶又請求一次接口,此時(shí)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();
  }
}

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

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

Redis實(shí)現(xiàn)

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

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

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

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

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

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

場景二:庫存限制


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

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

MongoDB實(shí)現(xiàn)

使用stocks表來記錄券的發(fā)放數(shù)量,當(dāng)然我們需要一個(gè)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實(shí)現(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();
  }
}

思考一個(gè)問題,庫存全部消耗完后,count字段還會增加么?應(yīng)該如何優(yōu)化?

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


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

解析

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

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

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

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

如何使用nodejs設(shè)計(jì)一個(gè)秒殺系統(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();
}

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

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

場景舉例:庫存僅剩1個(gè),此時(shí)用戶A和用戶B同時(shí)請求,此時(shí)A稍快一點(diǎn),庫存+1后=100,B庫存+1=101;

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

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

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

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

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


本文題目:如何使用nodejs設(shè)計(jì)一個(gè)秒殺系統(tǒng)的方法
地址分享:http://weahome.cn/article/gpcepe.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部