フラッシュ キル システムは大量の同時実行性が特徴で、1 秒間に数万のリクエストが届く可能性があります。 、システムは数分で崩壊します。プレイ可能なフラッシュキルシステムを設計する方法について話し合いましょう。
1. 電流制限:
まず、ビジネス ロジックを考慮せず、次のような最も単純なインターフェイスがあるとします:
@GetMapping("/test") public String test() { return "success"; }
このインターフェイスは非常にシンプルですが、にはロジックがありません。何千ものリクエストが同時にアクセスした場合、サーバーがクラッシュする可能性もあります。したがって、同時実行性の高いシステムが最初に行うべきことは、電流の流れを制限することです。 Springcloud プロジェクトは電流制限に hystrix を使用でき、springcloud alibaba は電流制限に Sentinel を使用できます。 Guava は、電流を制限できる RateLimiter ツール クラスを提供します。主にリーキーバケットアルゴリズムとトークンバケットアルゴリズムが含まれます。
Leaky Bucket Algorithm: 穴の開いたバケツに蛇口の下で水が満たされています。満たされると少し漏れます。ただし、蛇口内の水が非常に大きいとバケツから漏れが発生し、遅かれ早かれ水は溢れ、溢れた時には流量が制限されます。これは、アップロード速度とダウンロード速度を制限するのに適しています。
トークン バケット アルゴリズム: 一定のレートでトークンをバケットに入れます。リクエストが受信されるたびに、最初にトークンをバケットから取得する必要があります。トークンが取得できない場合、リクエストはブロックされます。これは電流制限、つまり QPS の制限に適しています。
フローを制限するには、ここでトークン バケット アルゴリズムを使用する必要があります。トークンが取得できない場合は、「多すぎる人が押し込めません」というプロンプトが表示されます。直接戻ってきました。
2. ユーザーがログインしているかどうかを確認します:
電流制限の最初のステップの後、受信リクエストはユーザーがログインしているかどうかを確認する必要があります。このプロジェクトでは、最初に JWT を使用します。ログイン インターフェイスを要求し、ログイン後に戻ります。トークン。リクエスト ヘッダーにトークンを含む他のすべてのインターフェイスを要求すると、トークンを介してユーザー情報を取得できます。ユーザー情報が取得できない場合は、ユーザーに再ログインを求めるメッセージが表示されます: トークンが無効です。
3. 商品が売り切れているかどうかを確認します:
最初の 2 つの検証ステップに合格した場合は、商品が売り切れているかどうかを確認する必要があります。 「申し訳ありません」というメッセージが返されますが、商品は即完売です。」製品が売り切れているかどうかをデータベースで確認することはできません。そうしないと、非常に時間がかかります。製品 ID をキーとして使用して、辞書を使用して製品 ID を保存できます。アイテムが売り切れの場合は値を True に設定し、それ以外の場合は False に設定します。
4. フラッシュ セールに参加している製品を redis に追加します:
最初に ISINREDIS
のキーを取得して、各製品が Redis に追加されているかどうかを示します。受信するリクエストごとにこの操作を繰り返します。 ISINREDIS
値が false の場合、redis にフラッシュ セール製品が存在しないことを意味します。次に、フラッシュ セールに参加しているすべての製品をクエリし、キーとして製品 ID、値として製品在庫を使用し、それらを Redis に保存します。同時に、キーとして製品 ID、値として false を使用します。 、それらを 3 番目のステップでマップに追加し、その製品が販売されていないことを示します。最後に、ISINREDIS
の値を true に設定します。これは、フラッシュ セールに参加しているすべての製品が Redis に追加されたことを意味します。
5. 源泉徴収:
redis の decr 関数を使用して商品の数量を減らし、減額された金額を判定します。自己減分後の結果が 0 未満の場合は、商品が売り切れたことを意味し、マップ内の対応する商品 ID の値が true に設定され、「遅くなりました。商品が完売しました」というプロンプトが表示されます。完売しました」と返されます。
6. フラッシュ セールが繰り返されるかどうかを決定します:
ユーザーのフラッシュ セールが成功した場合、フラッシュ セール注文がデータベースに保存された後、ユーザー ID と製品 ID が使用されます。キーとして true が値として redis に保存され、このユーザーがすでにこの製品を即座に販売したことを示します。そこで、ユーザー ID とプロダクト ID に基づいて redis にアクセスし、フラッシュ セールが繰り返されるかどうかを判断します。繰り返される場合は、「フラッシュ セールを繰り返さないでください」というプロンプトが返されます。
7. 非同期処理:
上記の検証に合格すると、フラッシュ セールを処理できます。フラッシュセールリクエストごとに在庫を差し引いて注文を作成すると、速度が非常に遅くなるだけでなく、データベースがクラッシュする可能性があります。つまり、上記の検証に合格した後、ユーザー ID と製品 ID がメッセージとして MQ に送信され、すぐに「キューイング」プロンプトがユーザーに返されます。次に、MQ のコンシューマ側でメッセージを消費し、ユーザー ID と製品 ID を取得し、製品 ID に基づいて在庫をクエリして、十分な在庫を再度確保します。その後、フラッシュ セールを繰り返すかどうかも決定できます。判定通過後はデータベースを操作し、在庫を差し引いてフラッシュセール注文を作成します。在庫の差し引きとフラッシュセール注文の作成は同じトランザクション内で行う必要があることに注意してください。
8. 売られすぎの問題:
売られすぎの問題は、商品の在庫がマイナスになることです。たとえば、在庫が 1 の場合、10 人のユーザーが同時に販売し、在庫を判断するときはすべて 1 なので、10 人が正常に注文でき、最終的な在庫は -9 になります。の解き方?実際、このシステムでは、最初に在庫を事前に削減するために redis が使用され、redis コマンドのコア モジュールがシングルスレッドであるため、そのような問題はまったく発生しません。したがって、売れすぎないことが保証されます。 Redis が使用されていない場合は、製品にバージョン フィールドを追加することもできます。毎回在庫を差し引く前にバージョンを確認してください。在庫を差し引くための条件を SQL に追加します。つまり、バージョンが今見つかったバージョンと同じである必要があります。
@RestController @RequestMapping("/seckill") public class SeckillController { @Autowired private UserService userService; @Autowired private SeckillService seckillService; @Autowired private RabbitMqSender mqSender; // 用来标记商品是否已经加入到redis中的key private static final String ISINREDIS = "isInRedis"; // 用goodsId作为key,标记该商品是否已经卖完 private Map<integer> seckillOver = new HashMap<integer>(); // 用RateLimiter做限流,create(10),可以理解为QPS阈值为10 private RateLimiter rateLimiter = RateLimiter.create(10); @PostMapping("/{sgId}") public JsonResult> seckillGoods(@PathVariable("sgId") Integer sgId, HttpServletRequest httpServletRequest){ // 1. 如果QPS阈值超过10,即1秒钟内没有拿到令牌,就返回“人太多了,挤不进去”的提示 if (!rateLimiter.tryAcquire(1, TimeUnit.SECONDS)) { return new JsonResult(SeckillGoodsEnum.TRY_AGAIN.getCode(), SeckillGoodsEnum.TRY_AGAIN.getMessage()); } // 2. 检查用户是否登录(用户登录后,访问每个接口都应该在请求头带上token,根据token再去拿user) String token = httpServletRequest.getHeader("token"); String userId = JWT.decode(token).getAudience().get(0); User user = userService.findUserById(Integer.valueOf(userId)); if (user == null) { return new JsonResult(SeckillGoodsEnum.INVALID_TOKEN.getCode(), SeckillGoodsEnum.INVALID_TOKEN.getMessage()); } // 3. 如果商品已经秒杀完了,就不执行下面的逻辑,直接返回商品已秒杀完的提示 if (!seckillOver.isEmpty() && seckillOver.get(sgId)) { return new JsonResult(SeckillGoodsEnum.SECKILL_OVER.getCode(), SeckillGoodsEnum.SECKILL_OVER.getMessage()); } // 4. 将所有参加秒杀的商品信息加入到redis中 if (!RedisUtil.isExist(ISINREDIS)) { List<seckillgoods> goods = seckillService.getAllSeckillGoods(); for (SeckillGoods seckillGoods : goods) { RedisUtil.set(String.valueOf(seckillGoods.getSgId()), seckillGoods.getSgSeckillNum()); seckillOver.put(seckillGoods.getSgId(), false); } RedisUtil.set(ISINREDIS, true); } // 5. 先自减,预扣库存,判断预扣后库存是否小于0,如果是,表示秒杀完了 Long stock = RedisUtil.decr(String.valueOf(sgId)); if (stock (SeckillGoodsEnum.SECKILL_OVER.getCode(), SeckillGoodsEnum.SECKILL_OVER.getMessage()); } // 6. 判断是否重复秒杀(成功秒杀并创建订单后,会将userId和goodsId作为key放到redis中) if (RedisUtil.isExist(userId + sgId)) { return new JsonResult(SeckillGoodsEnum.REPEAT_SECKILL.getCode(), SeckillGoodsEnum.REPEAT_SECKILL.getMessage()); } // 7. 以上校验都通过了,就将当前请求加入到MQ中,然后返回“排队中”的提示 String msg = userId + "," + sgId; mqSender.send(msg); return new JsonResult(SeckillGoodsEnum.LINE_UP.getCode(), SeckillGoodsEnum.LINE_UP.getMessage()); } } </seckillgoods></integer></integer>
用jmeter模拟并发请求,测试高并发情况下系统能否扛得住。由于只有一个id为1的商品,所以商品id固定写死1。但是每个用户都要先请求登录接口获取到token才能进行秒杀请求,有点儿麻烦,所以可以先把jwt模块注释掉,把userId当成参数传进去。jmeter配置如下图:
以上がRedisでフラッシュセールシステムを実装する方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。