이 글에서는 nodejs를 사용하여 플래시 세일 시스템을 설계하는 방법을 소개합니다. 도움이 필요한 친구들이 모두 참고할 수 있기를 바랍니다.
프런트 엔드의 경우 "동시성" 시나리오는 거의 발생하지 않습니다. 이 기사에서는 일반적인 플래시 판매 시나리오를 사용하여 "동시성"이 발생할 때 실제 온라인 노드 애플리케이션이 사용할 기술에 대해 설명합니다. 이 기사의 샘플 코드 데이터베이스는 MongoDB를 기반으로 하며 캐시는 Redis를 기반으로 합니다. [관련 추천: "nodejs Tutorial"]
규칙: 한 명의 사용자는 하나의 쿠폰만 받을 수 있습니다.
우선, 사용자의 쿠폰 기록을 저장하기 위해 기록 테이블을 사용하는 것이 좋습니다. 사용자가 쿠폰을 받으면 테이블에서 쿠폰 수신 여부를 확인할 수 있습니다.
레코드 구조는 다음과 같습니다
new Schema({ // 用户id userId: { type: String, required: true, }, });
비즈니스 프로세스도 매우 간단합니다.
MongoDB 구현
샘플 코드는 다음과 같습니다.
async grantCoupon(userId: string) { const record = await this.recordsModel.findOne({ userId, }); if (record) { return false; } else { this.grantCoupon(); this.recordModel.create({ userId, }); } }
Postman으로 테스트해 보면 그럴 것 같습니다. 괜찮아요. 그런 다음 동시 시나리오를 고려합니다. 예를 들어, "사용자"가 버튼을 클릭하고 쿠폰이 발행될 때까지 기다리는 것이 아니라 빠르게 클릭하거나 도구를 사용하여 쿠폰 인터페이스를 동시에 요청하는 경우 문제가 발생합니까? 프로그램? (프런트 엔드에서는 로딩을 통해 동시성 문제를 피할 수 있지만 해커 공격을 방지하려면 인터페이스를 가로채야 합니다.)
결과적으로 사용자는 여러 개의 쿠폰을 받을 수 있습니다. 문제는 기록 조회
와 쿠폰 수집 기록 추가
에 있습니다. 이 두 단계는 별도로 수행됩니다. 즉, 사용자 A는 쿠폰 수집 기록이 없는 특정 시점이 있습니다. , 쿠폰이 발행된 후, 사용자 A가 다시 인터페이스를 요청하는데, 이때 기록 테이블 데이터 삽입 작업이 완료되지 않아 발행 문제가 반복되었습니다. 查询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
를 제공합니다. 다시 작성된 명령문을 살펴보겠습니다. 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 구현
MongoDB뿐만 아니라 redis도 이 논리에 매우 적합합니다. redis를 사용하여 이를 구현해 보겠습니다. rrreee마찬가지로 setnx는 redis의 원자적 연산입니다. 즉, 다음을 의미합니다. 키에 값이 없으면 값을 설정하세요. 이미 값이 있으면 처리되지 않고 실패 메시지가 표시됩니다. 이는 동시 처리에 대한 예시일 뿐입니다. 실제 온라인 서비스도 고려해야 할 사항:
응용 프로그램 이름 + 함수 이름 + userId
와 같은 다른 응용 프로그램과 충돌할 수 없습니다. /li>< li>서비스가 오프라인이 된 후 redis 키를 정리해야 합니다. 그렇지 않으면 만료 시간을 setnx의 세 번째 매개변수에 직접 추가할 수 있습니다.시나리오 2: 재고 한도
🎜🎜🎜🎜규칙: 쿠폰의 총 재고는 확실하며, 단일 사용자는 쿠폰 수에 제한을 두지 않습니다. 받을 수 있다🎜🎜🎜🎜위 예시를 보면 유사 동시성도 쉽게 구현 가능하니 코드만 입력하면 된다🎜🎜 🎜🎜MongoDB 구현🎜🎜🎜🎜stocks
테이블을 이용해 쿠폰 개수를 기록한다 물론, 이 레코드를 식별하려면 쿠폰 ID 필드가 필요합니다🎜🎜테이블 구조: 🎜rrreee🎜발행 논리: 🎜rrreee🎜 🎜🎜Redis 구현🎜🎜🎜🎜incr: 원자적 연산, 키 값을 1씩 늘립니다. 값이 존재하지 않으면 0으로 초기화됩니다. 🎜rrreee🎜문제가 있는지 생각해 보세요. 인벤토리를 모두 소모한 후 count
필드가 추가되나요? 어떻게 최적화해야 합니까? 🎜🎜🎜시나리오 3: 사용자 쿠폰 한도 + 인벤토리 한도🎜🎜🎜🎜🎜규칙: 한 명의 사용자는 한 개의 쿠폰만 받을 수 있으며, 전체 인벤토리는 제한되어 있습니다.🎜🎜🎜🎜🎜🎜Analytic🎜🎜🎜🎜 "한 명의 사용자만"을 해결하세요 원자적 작업을 사용하여 "한 개만 받을 수 있음" 또는 "총 재고 한도"를 모두 처리할 수 있습니다. 두 가지 조건이 있는 경우 유사한 원자적 작업을 통해 "한 명의 사용자가 한 개만 받을 수 있음"과 "총 재고 수 제한"을 결합할 수 있습니다. 한도". 재고 한도" 병합 작업 또는 데이터베이스와 더 유사한 "트랜잭션" 🎜数据库事务( 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消息队列,或使用动态扩容等技术。
更多编程相关知识,请访问:编程入门!!
위 내용은 nodejs를 사용하여 플래시 세일 시스템을 설계하는 방법에 대한 간략한 토론의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!