ホームページ >PHPフレームワーク >Laravel >フラッシュキルシステムの設計

フラッシュキルシステムの設計

步履不停
步履不停オリジナル
2019-06-25 15:06:333704ブラウズ

フラッシュキルシステムの設計

以前、プロモーションシステムの設計に関する記事を書き、フラッシュセール/直接割引/熟花スアンについて触れましたが、実際の仕事ではフラッシュセールシステムを実際にやったことがなく、そこで私は仮説を立てました。単純なフラッシュ セール システムは「欲求を解消する」ために作成され、プロモーションのアイデアは前の記事のデザインを引き続き踏襲しました。

分析

フラッシュ セール中に大量のトラフィックが流入し、フラッシュ セールが開始される前にクエリが頻繁に更新されます。大量のトラフィックが瞬時にデータベースに到達すると、非常に危険です。データベースを崩壊させるのは簡単です。したがって、フラッシュ セールの主な仕事は、トラフィックをレイヤーごとにフィルタリングし、最終的にはできるだけ少ない および トラフィックをできるだけスムーズにデータベースに入力できるようにすることです。 通常のフラッシュ セールでは、多数のユーザーが少量の商品を購入する必要があります。このようなニーズの場合、在庫をキャッシュするだけで、実際に注文を作成する前に大量のトラフィックをフィルタリングできます。

でもでもでも、全然大変そうにないですよ!少し難易度を高めるために、フラッシュ セールが Xiaomi 携帯電話を手に入れるようなものだと仮定します。100 万人が 100,000 台の携帯電話を手に入れたらどうなるでしょうか? Xiaomi のラッシュ セール中に列に並ぶのも 1 つの方法です (ただし、エクスペリエンスはあまり良くありません)。フラッシュ セールのデザインは、将来的にはこの考えに基づいて行われる予定です。

Xiaomi に関して言えば、「運も強さの一部だ!」ということを教えてくれたと言わざるを得ません。

フロントエンドの電流制限方法: ランダム( 0, 1) ? axios.post: wait(30, 'All completed!')

次はコードの詳細を分析したものですが、原則として元のビジネス ロジックは最小限の変更で済みます。できるだけ。
また、次の記事にはサービス サーキット ブレーカーやマルチレベル キャッシュなどの高度なゲームプレイはなく、比較的単純なビジネス設計のみが記載されています。

開始

オペレーターは、バックグラウンドでフラッシュ セール プロモーションにバリアントを追加し、フラッシュ セール在庫/フラッシュ セール割引率/開始時間と終了時間などを設定します。このようなデータを取得できます。

// promotion_variant (促销和变体表「sku」的一个中间表)
{
    'id': 1,
    'variant_id': 1,
    'promotion_id': 1,
    'promotion_type': 'snap_up',
    'discount_rate': 0.5,
    'stock': 100, // 秒杀库存
    'sold': 0, // 秒杀销量
    'quantity_limit': 1, // 限购
    'enabled': 1,
    'product_id': 1,
    'rest': {
        variant_name: 'xxx', // 秒杀期间变体名称
        image: 'xxx', // 秒杀期间变体图片
    }
}

最初に行うことは、フラッシュ セール プロモーションが正常に作成された後、プロモーション情報をキャッシュすることです。

# PromotionVariantObserver.php

public function saved(PromotionVariant $promotionVariant)
{
  if ($promotionVariant->promotion_type === PromotionType::SNAP_UP) {
    $seconds = $promotionVariant->ended_at->getTimestamp() - time();

    \Cache::put(
      "promotion_variants:$promotionVariant->id",
      $promotionVariant,
      $seconds
    );
  }
}

注文配置

既存の注文インターフェイスでは、バリアント情報を受信した後、現在のバリアント リストのどれがプロモーションに参加しているかわかりません。ここでの判断操作には、大量のデータベース クエリ操作が必要です。

そこで、フラッシュ セール用の新しい API を作成します。フロントエンドは、現在のバリアントがフラッシュ セール プロモーション中であることを検出すると、フラッシュ セール注文 API に切り替えます。

もちろん、オリジナルの注文 API を引き続き使用しており、フロントエンドでロゴを渡すことに問題はありません。

説明が必要な点は、通常、注文は 2 つのステップに分かれているということです。

最初のステップは、チェックアウト注文を生成する「チェックアウト (チェックアウト)」です。チェックアウト注文のアドレス、クーポン、支払い方法などを選択できます。

2 番目のステップは「確認」です。この時点で注文が確定し、在庫がロックされ、ユーザーは支払いを行うことができます。通常、規定の時間内に支払いが行われない場合、注文はキャンセルされ、在庫のロックが解除されます。

したがって、最初のステップでは、アドレスやクーポンの選択などの後続の操作がデータベースに影響を与えるのを防ぐために、ユーザーがフィルタリングされてキューに登録されます。

# CheckoutController.php

/**
 * @param Request $request
 * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response
 * @throws StockException
 */
public function snapUpCheckout(Request $request)
{
    $variantId = $request->input('variant_id');
    $quantity = $request->input('quantity', 1);

    // 加锁防止超卖
    $lock = \Cache::lock('snap_up:' . $variantId, 10);

    try {
        // 未获取锁的消费者将阻塞在这里
        $lock->block(10);

        $promotionVariant = \Cache::get('promotion_variants:' . $variantId);

        if ($promotionVariant->quantity release();

            throw new StockException('库存不足');
        }

        $promotionVariant->quantity -= $quantity;

        $seconds = $promotionVariant->ended_at->getTimestamp() - time();
        \Cache::put(
            "promotion_variants:$promotionVariant->id",
            $promotionVariant,
            $seconds
        );

    } catch (LockTimeoutException $e) {
        throw new StockException('库存不足');

    } finally {
        optional($lock)->release();
    }

    CheckoutOrder::dispatch([
        'user_id' => \Auth::id(),
        'variant_id' => $variantId,
        'quantity' => $quantity
    ]);

    return response('结账订单创建中');
}

フラッシュ セール チェックアウト API にはデータベース操作が含まれていないことがわかります。そして、オーダーを作成するタスクはディスパッチによってキューに分散され、ユーザーはキューに入った順に対応する時間列に並んで待ちます。

ここでの問題は、注文が正常に作成された後にクライアントにどのように通知するかということです。

クライアント通知

ここでの解決策はポーリングか WebSocket に他なりませんが、ここではサーバーパフォーマンスの消費が少ない WebSocket を選択し、laravel が提供する laravel-echo ( laravel-echo-server ) を使用します。ユーザーのフラッシュ セールが成功すると、フロントエンドとバックエンドが WebSocket リンクを確立し、バックエンドのチェックアウト注文が正常に作成されると、次のステップに進むようにフロントエンドに通知されます。

バックエンド

バックエンドが次に行う必要があるのは、WebSocket の対応するチャネルに「OrderChecked」イベントを送信して、「CheckoutOrder」ジョブでの注文が完了した後にチェックアウトを示すことです。注文が作成されたので、ユーザーは次のステップに進むことができます。

# Job/CheckoutOrder.php

// ...

public function handle()
{
  // 创建结账订单
  // ...

  // 通知客户端. websocket 编程本身就是以事件为导向的,和 laravel 的 event 非常契合。
  event(new OrderChecked($this->data->user_id));
}

// ...
# Event/OrderChecked.php

class OrderChecked implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    private $userId;

    /**
     * Create a new event instance.
     *
     * @param $userId
     */
    public function __construct($userId)
    {
        $this->userId = $userId;
    }

    /**
     * App.User.{id} 是 laravel 初始化时,默认的私有频道,直接使用即可
     * @return \Illuminate\Broadcasting\Channel|array
     */
    public function broadcastOn()
    {
        return new PrivateChannel('App.User.' . $this->userId);
    }
}

購入時の現在のユーザー ID が 1 であるとします。要約すると、上記のコードは、「OrderChecked」イベントを WebSocket のプライベート チャネル「App.User.1」にプッシュすることです。

フロントエンド

以下のコードは、vue-cli ツールを使用して初期化されたデフォルトのプロジェクトです。

// views/products/show.vue

<script>

import Echo from &#39;laravel-echo&#39;
import io from &#39;socket.io-client&#39;
window.io = io

export default {
  name: &#39;App&#39;,
  methods: {
    async snapUpCheckout () {
      try {
        // await post -> snap-up-checkout
        this.toCheckout()
      } catch (error) {
        // 秒杀失败
      }
    },
    toCheckout () {
      // 建立 websocket 连接
      const echo = new Echo({
        broadcaster: &#39;socket.io&#39;,
        host: &#39;http://api.e-commerce.test:6001&#39;,
        auth: {
          headers: {
            Authorization: &#39;Bearer &#39; + this.store.auth.token
          }
        }
      })

      // 监听私有频道 App.User.{id} 的 OrderChecked 事件
      echo.private(&#39;App.User.&#39; + this.store.user.id).listen(&#39;OrderChecked&#39;, (e) => {
        // redirect to checkou page
      })
    }
  }
}
</script>

laravel-echoを使用する際に注意すべき点の1つは、プライベートチャネルの使用により、laravel-echoはサーバーAPI

/broadcasting/auth

に投稿リクエストを送信することです。認証のデフォルト。ただし、ブレード テンプレートの代わりにフロントエンド カテゴリとバックエンド カテゴリが使用されるため、必要な認証を実行するための csrf トークンとセッションを簡単に取得することはできません。 したがって、ブロードキャストとlaravel-echo-serverの構成を少し変更する必要があります

# BroadcastServiceProvider.php

public function boot()
{
  // 将认证路由改为 /api/broadcasting/auth 从而避免 csrf 验证
  // 添加中间件 auth:api (jwt 使用 api.auth) 进行身份验证,避免访问 session ,并使 Auth::user() 生效。
  Broadcast::routes(["prefix" => "api", "middleware" => ["auth:api"]]);

  require base_path('routes/channels.php');
}
// laravel-echo-server.json

// 认证路由添加 api 前缀,与上面的修改对应
"authEndpoint": "/api/broadcasting/auth"

在庫のロック解除

この注文の「在庫」がロックされている場合、ユーザー WebSocket を切断したり、長時間放置したりする場合は、インベントリの無意味な占有を防ぐために、インベントリのロックを解除する必要があります。

ここでのインベントリは、データベース インベントリではなく、キャッシュ インベントリを指します。これは、この時点で注文が正常に作成されたとしても、まだチェックアウト状態 (住所、支払い方法などが選択されていない) であり、パーソナル センターには表示されないためです。データベースの在庫は、ユーザーが注文を確認した場合にのみロックされます。

したがって、ここでの理想的な実装は、ユーザーが WebSocket 接続を切断した後に、ロックされた注文の在庫を返すことです。チェックアウト注文が作成されると、長期間操作されなかった注文に在庫を戻すための遅延キューが作成されます。

しかし、laravel-echo はブロードキャスト システムであり、クライアント切断イベントのコールバックを提供しません。laravel-echo-server にフックを追加するなど、laravel がリッスンするクライアント イベントを実装する方法がいくつかあります。 laravel ですが、laravel-echo-server の実装を変更する必要があります。ここでは詳細には触れませんが、フラッシュ セールのアイデアを提供することに重点を置いています。

概要

フラッシュキルシステムの設計

上の図は、フラッシュ キル システムの論理的な概要です。この時点で、フラッシュセールのプロセス全体が終了しますが、一般に、コードの量はそれほど多くなく、ロジックは比較的単純です。

図からわかるように、プロセス全体で、キュー内でのみ mysql と対話し、キューの 電流制限を通じて、耐久性に適応できます。 mysql を最大限に活用します。 mysql のパフォーマンスが十分である場合、ユーザーは多数のキューを通じて同時に注文を消費でき、ユーザーはキューのプロセスをまったく意識しません。

ご質問やより良いアイデアがある場合は、ディスカッション用のメッセージを残してください~

Laravel 関連の技術記事の詳細については、Laravel チュートリアル 列にアクセスして学習してください。 !

以上がフラッシュキルシステムの設計の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。