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.
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"]
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, }, });
The business process is also very simple:
##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, }); } }
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.
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(); } }
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(); } }
Rules: The total inventory of coupons is certain, and a single user is not limited to the number they can receive
MongoDB implementation
Use thestocks table To record the number of coupons issued, of course we need a couponId field to identify this record
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 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('coupon-1'); if (result <= total) { this.grantCoupon(); } }
count field still increase? How should it be optimized?
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开始支持事务,但这里作为演示,我们还是使用代码逻辑来控制并发
业务逻辑:
代码:
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消息队列,或使用动态扩容等技术。
更多编程相关知识,请访问:编程入门!!
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!