Home > Web Front-end > JS Tutorial > body text

A brief discussion on how to use nodejs to design a flash sale system

青灯夜游
Release: 2021-04-21 09:45:33
forward
3087 people have browsed it

This article will introduce to you how to use nodejs to design a flash sale system. It has certain reference value. Friends in need can refer to it. I hope it will be helpful to everyone.

A brief discussion on how to use nodejs to design a flash sale system

For the front-end, "concurrency" scenarios are rarely encountered. This article will talk about a real online node application encounter from a common flash sale scenario. What technology will be used for "concurrency"? The sample code database in this article is based on MongoDB, and the cache is based on Redis. [Related recommendations: "nodejs Tutorial"]

Scenario 1: Receive coupons


Rules: One user can only Get a coupon.

First of all, our idea is to use a records table to save the user's coupon record. When the user receives the coupon, he or she can check whether the coupon has been received in the table.

The records structure is as follows

new Schema({
  // 用户id
  userId: {
    type: String,
    required: true,
  },
});
Copy after login

The business process is also very simple:

A brief discussion on how to use nodejs to design a flash sale system

##MongoDB implementation

The sample code is as follows:

  async grantCoupon(userId: string) {
    const record = await this.recordsModel.findOne({
      userId,
    });
    if (record) {
      return false;
    } else {
      this.grantCoupon();
      this.recordModel.create({
        userId,
      });
    }
  }
Copy after login

Test it with postman, it seems to be OK. Then we consider concurrent scenarios. For example, the "user" does not just click the button and wait for the coupon to be issued, but clicks quickly, or uses a tool to concurrently request the coupon interface. Will our program have problems? (Concurrency issues can be avoided on the front end by loading, but the interface must be intercepted to prevent hacker attacks)

As a result, the user may receive multiple coupons. The problem lies in

querying records and adding coupon collection records. These two steps are performed separately, that is, there is a point in time: the query finds that user A has no coupon collection record, and sends After the coupon, user A requested the interface again. At this time, the data insertion operation in the records table was not completed, resulting in a duplicate issuance problem.

The solution is also very easy, that is, how to execute the query and the insert statement together to eliminate the asynchronous process in the middle. mongoose provides us with

findOneAndUpdate, which means finding and modifying. Let’s take a look at the rewritten statement:

async grantCoupon(userId: string) {
  const record = await this.recordModel.findOneAndUpdate({
    userId,
  }, {
    $setOnInsert: {
      userId,
    },
  }, {
    new: false,
    upsert: true,
  });
  if (! record) {
    this.grantCoupon();
  }
}
Copy after login

In fact, this is an atomic operation of mongo. The first parameter is the query statement. , query the entry of userId. The second parameter $setOnInsert indicates the field inserted when adding it. The third parameter upsert=true indicates that if the queried entry does not exist, it will be created. new=false indicates that the queried entry is returned instead of Modified entry. Then we only need to judge that the queried record does not exist, and then execute the release logic, and the insert statement is executed together with the query statement. Even if there are concurrent requests coming in at this time, the next query will be after the last insert statement.

Atomic (atomic) originally means "particles that cannot be further divided." Atomic operation means "one or a series of operations that cannot be interrupted". Two atomic operations cannot act on the same variable at the same time.

Redis implementation

Not only MongoDB, redis is also very suitable for this logic. Let’s use redis to implement it:

async grantCoupon(userId: string) {
  const result = await this.redis.setnx(userId, 'true');
  if (result === 1) {
    this.grantCoupon();
  }
}
Copy after login

Similarly, setnx is an atomic operation of redis, which means: if the key has no value, the value will be set. If there is already a value, it will not be processed and a failure will be prompted. This is just a demonstration of concurrent processing. Actual online services also need to consider:

    key value cannot conflict with other applications, such as
  • Application name function name userId
  • After the service goes offline, the redis key needs to be cleaned, or the expiration time can be added directly to the third parameter of setnx
  • Redis data is only in memory, and coupon issuance records need to be stored in the database

Scenario 2: Inventory limit


Rules: The total inventory of coupons is certain, and a single user is not limited to the number they can receive

Yes With the above example, similar concurrency is also easy to implement. Directly enter the code

MongoDB implementation

Use the

stocks table To record the number of coupons issued, of course we need a couponId field to identify this record

Table structure:

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

Issuance logic:

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();
  }
}
Copy after login

Redis implementation

incr: Atomic operation, set the value of key to 1, if the value does not exist, it will be initialized to 0;

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

Think about a problem, the inventory is all consumed After that, will the

count field still increase? How should it be optimized?

Scenario 3: User coupon limit Inventory limit


Rule: A user can only receive one coupon, and the total inventory is limited

Analysis

We can solve the problem of "one user can only receive one piece" or "total inventory limit" alone. It can be processed with atomic operations. When there are two conditions, can one be implemented, similar to the atomic operation that combines "one user can only receive one" and "total inventory limit", or is more similar to the database? Transaction”

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

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

业务逻辑:

A brief discussion on how to use nodejs to design a flash sale system

代码:

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();
}
Copy after login

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

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

The above is the detailed content of A brief discussion on how to use nodejs to design a flash sale system. For more information, please follow other related articles on the PHP Chinese website!

Related labels:
source:juejin.cn
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template