In diesem Artikel erfahren Sie, wie Sie mit nodejs ein Flash-Sale-System entwerfen. Es hat einen gewissen Referenzwert. Freunde in Not können sich darauf beziehen. Ich hoffe, es wird für alle hilfreich sein.
Im Front-End treten „Parallelitäts“-Szenarien selten auf. In diesem Artikel werden gängige Flash-Sale-Szenarien verwendet, um zu erläutern, welche Technologien eine echte Online-Knotenanwendung verwenden wird, wenn sie auf „Parallelität“ stößt. Die Beispielcodedatenbank in diesem Artikel basiert auf MongoDB und der Cache basiert auf Redis. [Verwandte Empfehlungen: „nodejs Tutorial“]
Regeln: Ein Benutzer kann nur einen Gutschein erhalten.
Zuallererst besteht unsere Idee darin, eine Datensatztabelle zu verwenden, um den Gutscheindatensatz des Benutzers zu speichern. Wenn der Benutzer den Gutschein erhält, kann er oder sie in der Tabelle überprüfen, ob der Gutschein eingegangen ist.
Die Struktur der Datensätze ist wie folgt
new Schema({ // 用户id userId: { type: String, required: true, }, });
Der Geschäftsprozess ist ebenfalls sehr einfach:
MongoDB-Implementierung
Der Beispielcode lautet wie folgt:
async grantCoupon(userId: string) { const record = await this.recordsModel.findOne({ userId, }); if (record) { return false; } else { this.grantCoupon(); this.recordModel.create({ userId, }); } }
Testen Sie es mit Postman und es scheint in Ordnung sein. Dann betrachten wir gleichzeitige Szenarien. Beispielsweise klickt der „Benutzer“ nicht einfach auf die Schaltfläche und wartet auf die Ausstellung des Gutscheins, sondern klickt schnell oder verwendet ein Tool, um gleichzeitig die Gutscheinschnittstelle anzufordern Programm? (Parallelitätsprobleme können im Frontend durch Laden vermieden werden, die Schnittstelle muss jedoch abgefangen werden, um Hackerangriffe zu verhindern)
Daher erhalten Benutzer möglicherweise mehrere Gutscheine. Das Problem liegt im Abfragen von Datensätzen
und im Hinzufügen von Coupon-Datensätzen
. Das heißt, es gibt einen Zeitpunkt: Benutzer A hat keinen Coupon-Datensatz . Nachdem der Coupon ausgestellt wurde, fordert Benutzer A die Schnittstelle erneut an. Zu diesem Zeitpunkt ist der Vorgang zum Einfügen der Datensatztabellendaten noch nicht abgeschlossen, was zu wiederholten Ausgabeproblemen führt. 查询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没有值,则将值设置进去,如果已有值就不做处理,提示失败。这里只是演示并发处理,实际线上服务还需要考虑:
应用名称+功能名称+userId
规则:券总库存一定,单个用户不限领取数量
有了上面的示例,类似并发也很好实现,直接上代码
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('coupon-1'); if (result <= total) { this.grantCoupon(); } }
思考一个问题,库存全部消耗完后,count
findOneAndUpdate
zur Verfügung, was das Suchen und Ändern bedeutet: 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(); }
Redis-Implementierung
Nicht nur MongoDB, sondern auch Redis ist für diese Logik sehr gut geeignet: rrreeeÄhnlich ist setnx eine atomare Operation von Redis, was bedeutet: if Der Schlüssel hat keinen Wert. Wenn bereits ein Wert vorhanden ist, wird dieser nicht verarbeitet und eine Fehlermeldung angezeigt. Dies ist nur eine Demonstration der gleichzeitigen Verarbeitung. Tatsächliche Online-Dienste müssen außerdem Folgendes berücksichtigen:
Anwendungsname + Funktionsname + Benutzer-ID
< /li>< li>Nachdem der Dienst offline gegangen ist, muss der Redis-Schlüssel bereinigt werden, oder die Ablaufzeit kann direkt zum dritten Parameter von setnx hinzugefügt werdenSzenario 2: Bestandsgrenze
🎜🎜🎜🎜Regel: Der Gesamtbestand an Gutscheinen ist sicher und ein einzelner Benutzer ist nicht auf die Anzahl beschränkt kann empfangen🎜🎜🎜🎜Mit dem obigen Beispiel ist eine ähnliche Parallelität ebenfalls einfach zu implementieren. Geben Sie einfach den Code ein ausgegeben. Natürlich benötigen wir ein CouponId-Feld, um diesen Datensatz zu identifizieren. Tabellenstruktur: 🎜rrreee. Ausgabelogik: 🎜rrreee. 🎜🎜Redis-Implementierung der Wert existiert nicht, er wird auf 0 initialisiert; 🎜rrreee🎜Denken Sie an ein Problem, nachdem der gesamte Bestand aufgebraucht ist,count
Werden Felder hinzugefügt? Wie soll es optimiert werden? 🎜🎜🎜Szenario 3: Benutzer-Coupon-Limit + Bestandslimit🎜🎜🎜🎜🎜Regel: Ein Benutzer kann nur einen Coupon erhalten und der Gesamtbestand ist begrenzt🎜🎜🎜🎜🎜🎜Analyse🎜🎜🎜🎜 Lösen Sie „ein Benutzer allein“ Wir können atomare Operationen verwenden, um sowohl „kann nur ein Stück erhalten“ als auch „Gesamtinventarlimit“ zu handhaben. Wenn es zwei Bedingungen gibt, können ähnliche atomare Operationen „ein Benutzer kann nur ein Stück erhalten“ und „Gesamtinventar“ kombinieren Limit“. Zusammenführungsvorgang „Inventarlimit“ oder eine „Transaktion“, die eher einer Datenbank ähnelt 🎜数据库事务( 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消息队列,或使用动态扩容等技术。
更多编程相关知识,请访问:编程入门!!
Das obige ist der detaillierte Inhalt vonEine kurze Diskussion darüber, wie man NodeJS zum Entwerfen eines Flash-Sale-Systems verwendet. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!