Maison > interface Web > js tutoriel > Une brève discussion sur la façon d'utiliser nodejs pour concevoir un système de vente flash

Une brève discussion sur la façon d'utiliser nodejs pour concevoir un système de vente flash

青灯夜游
Libérer: 2021-04-21 09:45:33
avant
3135 Les gens l'ont consulté

Cet article vous présentera comment utiliser nodejs pour concevoir un système de vente flash. Il a une certaine valeur de référence. Les amis dans le besoin peuvent s'y référer. J'espère qu'il sera utile à tout le monde.

Une brève discussion sur la façon d'utiliser nodejs pour concevoir un système de vente flash

Pour le front-end, les scénarios de « concurrence » sont rarement rencontrés. Cet article parlera d'une véritable rencontre d'application de nœud en ligne à partir d'un scénario de vente flash commun. être utilisé pour la « concurrence » ? L'exemple de base de données de code présenté dans cet article est basé sur MongoDB et le cache est basé sur Redis. [Recommandations associées : "Tutoriel nodejs"]

Scénario 1 : Recevoir des coupons


Règle : Un seul utilisateur peut Obtenez un coupon.

Tout d'abord, notre idée est d'utiliser un tableau d'enregistrements pour enregistrer l'enregistrement du coupon de l'utilisateur. Lorsque l'utilisateur reçoit le coupon, il peut vérifier si le coupon a été reçu. le tableau.

La structure des enregistrements est la suivante

new Schema({
  // 用户id
  userId: {
    type: String,
    required: true,
  },
});
Copier après la connexion

Le processus métier est également très simple :

Une brève discussion sur la façon dutiliser nodejs pour concevoir un système de vente flash

Implémentation de MongoDB

L'exemple de code est le suivant :

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

Testez-le avec le facteur, ça semble aller. Ensuite, nous envisageons des scénarios simultanés. Par exemple, « l'utilisateur » ne se contente pas de cliquer sur le bouton et d'attendre que le coupon soit émis, mais clique rapidement ou utilise un outil pour demander simultanément l'interface du coupon. Y aura-t-il un problème avec notre interface de coupon. programme? (Les problèmes de concurrence peuvent être évités dès le début en chargeant, mais l'interface doit être interceptée pour empêcher les attaques de pirates)

En conséquence, l'utilisateur peut recevoir plusieurs coupons. Le problème réside dans 查询records et 新增领券记录. Ces deux étapes sont effectuées séparément, c'est-à-dire qu'il y a un moment donné : il est demandé que l'utilisateur A n'a pas d'enregistrement de coupon, et une fois le coupon émis, l'utilisateur A demande. l'interface à ce moment, la table des enregistrements L'opération d'insertion des données n'est pas terminée, ce qui entraîne des problèmes d'émission répétés.

La solution est également très simple, c'est-à-dire comment exécuter la requête et l'instruction d'insertion ensemble pour éliminer le processus asynchrone au milieu. Mongoose nous fournit findOneAndUpdate, ce qui signifie rechercher et modifier. Jetons un coup d'œil à l'instruction réécrite :

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

En fait, il s'agit d'une opération atomique de mongo. Le premier paramètre est l'instruction de requête, query. userId. Entry, le deuxième paramètre $setOnInsert indique le champ inséré lors de l'ajout d'un nouveau, le troisième paramètre upsert=true indique que si l'entrée interrogée n'existe pas, elle sera créée, new=false indique que l'entrée interrogée est renvoyée. au lieu de l'entrée modifiée. Ensuite, il nous suffit de juger que l'enregistrement demandé n'existe pas, puis d'exécuter la logique de publication et l'instruction d'insertion est exécutée avec l'instruction de requête. Même si des requêtes simultanées arrivent à ce moment-là, la requête suivante aura lieu après la dernière instruction d'insertion.

Atomique (atomique) signifie à l'origine « particules qui ne peuvent pas être davantage divisées ». Une opération atomique signifie « une ou une série d’opérations qui ne peuvent être interrompues ». Deux opérations atomiques ne peuvent pas agir sur la même variable en même temps.

Implémentation de Redis

Non seulement MongoDB, redis est également très adapté à cette logique. Utilisons redis pour l'implémenter :

async grantCoupon(userId: string) {
  const result = await this.redis.setnx(userId, 'true');
  if (result === 1) {
    this.grantCoupon();
  }
}
Copier après la connexion

De même, setnx est une opération atomique de redis, ce qui signifie : si la clé n'a pas de valeur, la valeur sera définie. S'il y a déjà une valeur, elle ne sera pas traitée et un échec sera provoqué. . Il s'agit simplement d'une démonstration du traitement simultané. Les services en ligne réels doivent également prendre en compte :

  • les valeurs clés ne peuvent pas entrer en conflit avec d'autres applications, telles que 应用名称+功能名称+userId
  • les besoins en clés Redis. à utiliser une fois le service hors ligne Nettoyer ou ajouter directement le délai d'expiration au troisième paramètre de setnx
  • Les données Redis sont uniquement en mémoire et les enregistrements d'émission de coupons doivent être stockés dans la base de données

Scénario 2 : Restrictions d'inventaire


Règles : L'inventaire total des coupons est certain, et un seul utilisateur n'est pas limité au nombre de rachats

Avec l'exemple ci-dessus, une concurrence similaire est également possible C'est facile à mettre en œuvre, il suffit d'aller dans le code

Implémentation de MongoDB

Utilisez la table stocks pour enregistrer le nombre de coupons émis, bien sûr nous avons besoin d'un champ couponId pour identifier cet enregistrement

Structure de la table :

new Schema({
  /* 券标识 */
  couponId: {
    type: String,
    required: true,
  },
  /* 已发放数量 */
  count: {
    type: Number,
    default: 0,
  },
});
Copier après la connexion

Logique du problème :

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();
  }
}
Copier après la connexion

Implémentation de Redis

incr : Opération atomique, +1 la valeur de la clé Si la valeur n'existe pas, il sera initialisé à 0 ;

async grantCoupon(userId: string) {
  const total = 100; // 总库存
  const result = await this.redis.incr(&#39;coupon-1&#39;);
  if (result <= total) {
    this.grantCoupon();
  }
}
Copier après la connexion

En pensant à une question, une fois tout l'inventaire consommé, le champ count augmentera-t-il encore ? Comment faut-il l’optimiser ?

Scénario 3 : Restrictions de coupon utilisateur + restrictions d'inventaire


Règle : Un utilisateur ne peut recevoir qu'un seul coupon et l'inventaire total est limité

Analyse

Pour résoudre seul "un utilisateur ne peut recevoir qu'une seule pièce" ou "limite totale d'inventaire", nous Tout peut être traité avec des opérations atomiques. Lorsqu'il y a deux conditions, peut-on en mettre en œuvre une, similaire à l'opération atomique qui combine « un utilisateur ne peut en recevoir qu'un » et « limite totale d'inventaire », ou est-elle plus similaire à une base de données ? Affaires"

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

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

业务逻辑:

Une brève discussion sur la façon dutiliser nodejs pour concevoir un système de vente flash

代码:

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();
}
Copier après la connexion

其实我们可以舍去前两部查询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消息队列,或使用动态扩容等技术。

更多编程相关知识,请访问:编程入门!!

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Étiquettes associées:
source:juejin.cn
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal