Heim > Web-Frontend > js-Tutorial > Hauptteil

Eine kurze Diskussion darüber, wie man NodeJS zum Entwerfen eines Flash-Sale-Systems verwendet

青灯夜游
Freigeben: 2021-04-21 09:45:33
nach vorne
3090 Leute haben es durchsucht

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.

Eine kurze Diskussion darüber, wie man NodeJS zum Entwerfen eines Flash-Sale-Systems verwendet

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,
  },
});
Nach dem Login kopieren

Der Geschäftsprozess ist ebenfalls sehr einfach:

Eine kurze Diskussion darüber, wie man NodeJS zum Entwerfen eines Flash-Sale-Systems verwendet

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,
      });
    }
  }
Nach dem Login kopieren

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();
  }
}
Nach dem Login kopieren

实际上这是一个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();
  }
}
Nach dem Login kopieren

同样setnx是redis的一个原子操作,表示:如果key没有值,则将值设置进去,如果已有值就不做处理,提示失败。这里只是演示并发处理,实际线上服务还需要考虑:

  • key值不能与其他应用冲突使用,如应用名称+功能名称+userId
  • 服务下线后redis的key需要清理,或者直接在setnx第三个参数加上过期时间
  • redis数据只在内存中,发券记录需要入库保存

场景二:库存限制


规则:券总库存一定,单个用户不限领取数量

有了上面的示例,类似并发也很好实现,直接上代码

MongoDB实现

使用stocks表来记录券的发放数量,当然我们需要一个couponId字段去标识这条记录

表结构:

new Schema({
  /* 券标识 */
  couponId: {
    type: String,
    required: true,
  },
  /* 已发放数量 */
  count: {
    type: Number,
    default: 0,
  },
});
Nach dem Login kopieren

发放逻辑:

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();
  }
}
Nach dem Login kopieren

Redis实现

incr: 原子操作,将key的值+1,如果值不存在,将初始化为0;

async grantCoupon(userId: string) {
  const total = 100; // 总库存
  const result = await this.redis.incr(&#39;coupon-1&#39;);
  if (result <= total) {
    this.grantCoupon();
  }
}
Nach dem Login kopieren

思考一个问题,库存全部消耗完后,count

Die Lösung ist auch sehr einfach, nämlich wie man die Abfrage und die Einfügeanweisung zusammen ausführt, um den asynchronen Prozess in der Mitte zu eliminieren. mongoose stellt uns findOneAndUpdate zur Verfügung, was das Suchen und Ändern bedeutet:

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();
}
Nach dem Login kopieren
Nach dem Login kopieren
Tatsä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.

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>

    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, countWerden 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开始支持事务,但这里作为演示,我们还是使用代码逻辑来控制并发

    业务逻辑:

    Eine kurze Diskussion darüber, wie man NodeJS zum Entwerfen eines Flash-Sale-Systems verwendet

    代码:

    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();
    }
    Nach dem Login kopieren
    Nach dem Login kopieren

    其实我们可以舍去前两部查询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!

Verwandte Etiketten:
Quelle:juejin.cn
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage