


Eine kurze Diskussion darüber, wie man NodeJS zum Entwerfen eines Flash-Sale-Systems verwendet
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“]
Szenario 1: Gutscheine erhalten
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没有值,则将值设置进去,如果已有值就不做处理,提示失败。这里只是演示并发处理,实际线上服务还需要考虑:
- key值不能与其他应用冲突使用,如
应用名称+功能名称+userId
- 服务下线后redis的key需要清理,或者直接在setnx第三个参数加上过期时间
- redis数据只在内存中,发券记录需要入库保存
场景二:库存限制
规则:券总库存一定,单个用户不限领取数量
有了上面的示例,类似并发也很好实现,直接上代码
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();
}
Nach dem Login kopierenNach dem Login kopierenTatsächlich ist dies eine atomare Operation von mongo. Der erste Parameter ist die Abfrageanweisung. query userId Der zweite Parameter $setOnInsert gibt an, welches Feld beim Hinzufügen eingefügt wurde. Der dritte Parameter upsert=true gibt an, dass der abgefragte Eintrag erstellt wird, wenn er nicht vorhanden ist, und nicht der geänderte ein. Eintrag. Dann müssen wir nur noch feststellen, dass der abgefragte Datensatz nicht vorhanden ist, und dann die Freigabelogik ausführen, und die Einfügeanweisung wird zusammen mit der Abfrageanweisung ausgeführt. Auch wenn zu diesem Zeitpunkt gleichzeitig Anfragen eingehen, erfolgt die nächste Abfrage nach der letzten Einfügeanweisung.
Atomic (atomar) bedeutet ursprünglich „Teilchen, die nicht weiter geteilt werden können“. Unter atomaren Operationen versteht man „eine oder mehrere Operationen, die nicht unterbrochen werden können“. Zwei atomare Operationen können nicht gleichzeitig auf dieselbe Variable einwirken. 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:
- Der Schlüsselwert darf nicht mit anderen Anwendungen in Konflikt geraten, z. B.
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 werden - Redis-Daten befinden sich nur im Speicher und im Coupon Ausgabedatensätze müssen in der Datenbank gespeichert werden < /ul>
什么情况下会走到第3步的左分支?
什么情况下会走到第4步的左分支?
思考:什么情况下会出现,先请求的用户没抢到券,反而靠后的用户能抢到券?
Szenario 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记录和查询库存数量,结果并不会出问题。从数据库优化来说,显然更改比查询更耗时,而且库存有限,最终库存消耗完,后面请求都会在前两步逻辑中走完。
场景举例:库存仅剩1个,此时用户A和用户B同时请求,此时A稍快一点,库存+1后=100,B库存+1=101;
场景举例: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!

Heiße KI -Werkzeuge

Undresser.AI Undress
KI-gestützte App zum Erstellen realistischer Aktfotos

AI Clothes Remover
Online-KI-Tool zum Entfernen von Kleidung aus Fotos.

Undress AI Tool
Ausziehbilder kostenlos

Clothoff.io
KI-Kleiderentferner

AI Hentai Generator
Erstellen Sie kostenlos Ai Hentai.

Heißer Artikel

Heiße Werkzeuge

Notepad++7.3.1
Einfach zu bedienender und kostenloser Code-Editor

SublimeText3 chinesische Version
Chinesische Version, sehr einfach zu bedienen

Senden Sie Studio 13.0.1
Leistungsstarke integrierte PHP-Entwicklungsumgebung

Dreamweaver CS6
Visuelle Webentwicklungstools

SublimeText3 Mac-Version
Codebearbeitungssoftware auf Gottesniveau (SublimeText3)

Heiße Themen



Die Hauptunterschiede zwischen Node.js und Tomcat sind: Laufzeit: Node.js basiert auf der JavaScript-Laufzeit, während Tomcat ein Java-Servlet-Container ist. E/A-Modell: Node.js verwendet ein asynchrones, nicht blockierendes Modell, während Tomcat synchrones Blockieren verwendet. Parallelitätsbehandlung: Node.js verarbeitet die Parallelität über eine Ereignisschleife, während Tomcat einen Thread-Pool verwendet. Anwendungsszenarien: Node.js eignet sich für Echtzeit-, datenintensive und Anwendungen mit hoher Parallelität, und Tomcat eignet sich für herkömmliche Java-Webanwendungen.

Node.js ist eine serverseitige JavaScript-Laufzeitumgebung, während Vue.js ein clientseitiges JavaScript-Framework zum Erstellen interaktiver Benutzeroberflächen ist. Node.js wird für die serverseitige Entwicklung verwendet, beispielsweise für die Entwicklung von Back-End-Service-APIs und die Datenverarbeitung, während Vue.js für die clientseitige Entwicklung verwendet wird, beispielsweise für Single-Page-Anwendungen und reaktionsfähige Benutzeroberflächen.

Node.js kann als Backend-Framework verwendet werden, da es Funktionen wie hohe Leistung, Skalierbarkeit, plattformübergreifende Unterstützung, ein umfangreiches Ökosystem und einfache Entwicklung bietet.

Um eine Verbindung zu einer MySQL-Datenbank herzustellen, müssen Sie die folgenden Schritte ausführen: Installieren Sie den MySQL2-Treiber. Verwenden Sie mysql2.createConnection(), um ein Verbindungsobjekt zu erstellen, das die Hostadresse, den Port, den Benutzernamen, das Passwort und den Datenbanknamen enthält. Verwenden Sie „connection.query()“, um Abfragen durchzuführen. Verwenden Sie abschließend Connection.end(), um die Verbindung zu beenden.

Die folgenden globalen Variablen sind in Node.js vorhanden: Globales Objekt: global Kernmodul: Prozess, Konsole, erforderlich Laufzeitumgebungsvariablen: __dirname, __filename, __line, __column Konstanten: undefiniert, null, NaN, Infinity, -Infinity

Die Hauptunterschiede zwischen Node.js und Java sind Design und Funktionen: Ereignisgesteuert vs. Thread-gesteuert: Node.js ist ereignisgesteuert und Java ist Thread-gesteuert. Single-Threaded vs. Multi-Threaded: Node.js verwendet eine Single-Threaded-Ereignisschleife und Java verwendet eine Multithread-Architektur. Laufzeitumgebung: Node.js läuft auf der V8-JavaScript-Engine, während Java auf der JVM läuft. Syntax: Node.js verwendet JavaScript-Syntax, während Java Java-Syntax verwendet. Zweck: Node.js eignet sich für I/O-intensive Aufgaben, während Java für große Unternehmensanwendungen geeignet ist.

Es gibt zwei npm-bezogene Dateien im Node.js-Installationsverzeichnis: npm und npm.cmd. Die Unterschiede sind wie folgt: unterschiedliche Erweiterungen: npm ist eine ausführbare Datei und npm.cmd ist eine Befehlsfensterverknüpfung. Windows-Benutzer: npm.cmd kann über die Eingabeaufforderung verwendet werden, npm kann nur über die Befehlszeile ausgeführt werden. Kompatibilität: npm.cmd ist spezifisch für Windows-Systeme, npm ist plattformübergreifend verfügbar. Nutzungsempfehlungen: Windows-Benutzer verwenden npm.cmd, andere Betriebssysteme verwenden npm.

Ja, Node.js ist eine Backend-Entwicklungssprache. Es wird für die Back-End-Entwicklung verwendet, einschließlich der Handhabung serverseitiger Geschäftslogik, der Verwaltung von Datenbankverbindungen und der Bereitstellung von APIs.
