> 웹 프론트엔드 > JS 튜토리얼 > nodejs를 사용하여 플래시 세일 시스템을 설계하는 방법에 대한 간략한 토론

nodejs를 사용하여 플래시 세일 시스템을 설계하는 방법에 대한 간략한 토론

青灯夜游
풀어 주다: 2021-04-21 09:45:33
앞으로
3134명이 탐색했습니다.

이 글에서는 nodejs를 사용하여 플래시 세일 시스템을 설계하는 방법을 소개합니다. 도움이 필요한 친구들이 모두 참고할 수 있기를 바랍니다.

nodejs를 사용하여 플래시 세일 시스템을 설계하는 방법에 대한 간략한 토론

프런트 엔드의 경우 "동시성" 시나리오는 거의 발생하지 않습니다. 이 기사에서는 일반적인 플래시 판매 시나리오를 사용하여 "동시성"이 발생할 때 실제 온라인 노드 애플리케이션이 사용할 기술에 대해 설명합니다. 이 기사의 샘플 코드 데이터베이스는 MongoDB를 기반으로 하며 캐시는 Redis를 기반으로 합니다. [관련 추천: "nodejs Tutorial"]

시나리오 1: 쿠폰 받기


규칙: 한 명의 사용자는 하나의 쿠폰만 받을 수 있습니다.

우선, 사용자의 쿠폰 기록을 저장하기 위해 기록 테이블을 사용하는 것이 좋습니다. 사용자가 쿠폰을 받으면 테이블에서 쿠폰 수신 여부를 확인할 수 있습니다.

레코드 구조는 다음과 같습니다

new Schema({
  // 用户id
  userId: {
    type: String,
    required: true,
  },
});
로그인 후 복사

비즈니스 프로세스도 매우 간단합니다.

nodejs를 사용하여 플래시 세일 시스템을 설계하는 방법에 대한 간략한 토론

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没有值,则将值设置进去,如果已有值就不做处理,提示失败。这里只是演示并发处理,实际线上服务还需要考虑:

  • 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(&#39;coupon-1&#39;);
  if (result <= total) {
    this.grantCoupon();
  }
}
로그인 후 복사

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

해결 방법도 매우 쉽습니다. 즉, 쿼리와 삽입 문을 함께 실행하여 중간에 비동기 프로세스를 제거하는 방법입니다. mongoose는 찾기 및 수정을 의미하는 findOneAndUpdate를 제공합니다. 다시 작성된 명령문을 살펴보겠습니다.

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();
}
로그인 후 복사
로그인 후 복사
사실 이것은 mongo의 원자적 연산입니다. query userId 두 번째 매개변수 $setOnInsert는 필드를 추가할 때 삽입됨을 나타냅니다. 세 번째 매개변수 upsert=true는 쿼리된 항목이 존재하지 않는 경우 해당 항목이 생성됨을 나타냅니다. new=false는 쿼리된 항목이 수정된 항목 대신 반환됨을 나타냅니다. 하나. 입장. 그런 다음 쿼리된 레코드가 존재하지 않는다고 판단한 후 릴리스 로직을 실행하면 쿼리문과 함께 insert 문이 실행됩니다. 이때 동시 요청이 들어오더라도 다음 쿼리는 마지막 insert 문 이후가 됩니다.

Atomic(원자)은 원래 "더 이상 나눌 수 없는 입자"를 의미합니다. 원자적 연산은 "중단될 수 없는 하나 또는 일련의 작업"을 의미합니다. 두 개의 원자적 연산은 동일한 변수에 동시에 작용할 수 없습니다.

Redis 구현

MongoDB뿐만 아니라 redis도 이 논리에 매우 적합합니다. redis를 사용하여 이를 구현해 보겠습니다. rrreee마찬가지로 setnx는 redis의 원자적 연산입니다. 즉, 다음을 의미합니다. 키에 값이 없으면 값을 설정하세요. 이미 값이 있으면 처리되지 않고 실패 메시지가 표시됩니다. 이는 동시 처리에 대한 예시일 뿐입니다. 실제 온라인 서비스도 고려해야 할 사항:

  • 키 값은 응용 프로그램 이름 + 함수 이름 + userId와 같은 다른 응용 프로그램과 충돌할 수 없습니다. /li>< li>서비스가 오프라인이 된 후 redis 키를 정리해야 합니다. 그렇지 않으면 만료 시간을 setnx의 세 번째 매개변수에 직접 추가할 수 있습니다.
  • Redis 데이터는 메모리에만 있고 쿠폰 발급 기록을 데이터베이스에 저장해야 함
  • < /ul>

    시나리오 2: 재고 한도

    🎜🎜🎜🎜규칙: 쿠폰의 총 재고는 확실하며, 단일 사용자는 쿠폰 수에 제한을 두지 않습니다. 받을 수 있다🎜🎜🎜🎜위 예시를 보면 유사 동시성도 쉽게 구현 가능하니 코드만 입력하면 된다🎜🎜 🎜🎜MongoDB 구현🎜🎜🎜🎜stocks 테이블을 이용해 쿠폰 개수를 기록한다 물론, 이 레코드를 식별하려면 쿠폰 ID 필드가 필요합니다🎜🎜테이블 구조: 🎜rrreee🎜발행 논리: 🎜rrreee🎜 🎜🎜Redis 구현🎜🎜🎜🎜incr: 원자적 연산, 키 값을 1씩 늘립니다. 값이 존재하지 않으면 0으로 초기화됩니다. 🎜rrreee🎜문제가 있는지 생각해 보세요. 인벤토리를 모두 소모한 후 count 필드가 추가되나요? 어떻게 최적화해야 합니까? 🎜🎜🎜시나리오 3: 사용자 쿠폰 한도 + 인벤토리 한도🎜🎜🎜🎜🎜규칙: 한 명의 사용자는 한 개의 쿠폰만 받을 수 있으며, 전체 인벤토리는 제한되어 있습니다.🎜🎜🎜🎜🎜🎜Analytic🎜🎜🎜🎜 "한 명의 사용자만"을 해결하세요 원자적 작업을 사용하여 "한 개만 받을 수 있음" 또는 "총 재고 한도"를 모두 처리할 수 있습니다. 두 가지 조건이 있는 경우 유사한 원자적 작업을 통해 "한 명의 사용자가 한 개만 받을 수 있음"과 "총 재고 수 제한"을 결합할 수 있습니다. 한도". 재고 한도" 병합 작업 또는 데이터베이스와 더 유사한 "트랜잭션" 🎜

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

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

    业务逻辑:

    nodejs를 사용하여 플래시 세일 시스템을 설계하는 방법에 대한 간략한 토론

    代码:

    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();
    }
    로그인 후 복사
    로그인 후 복사

    其实我们可以舍去前两部查询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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:juejin.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
최신 이슈
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿