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.
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"]
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, }, });
Le processus métier est également très simple :
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, }); } }
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(); } }
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(); } }
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 :
应用名称+功能名称+userId
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, }, });
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(); } }
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('coupon-1'); if (result <= total) { this.grantCoupon(); } }
En pensant à une question, une fois tout l'inventaire consommé, le champ count
augmentera-t-il encore ? Comment faut-il l’optimiser ?
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开始支持事务,但这里作为演示,我们还是使用代码逻辑来控制并发
业务逻辑:
代码:
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消息队列,或使用动态扩容等技术。
更多编程相关知识,请访问:编程入门!!
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!