Maison  >  Article  >  cadre php  >  Conception du système Flash Kill

Conception du système Flash Kill

步履不停
步履不停original
2019-06-25 15:06:333678parcourir

Conception du système Flash Kill

J'ai déjà écrit un article sur la conception du système de promotion et mentionné les ventes flash/remises directes/juhuasuan, mais dans le travail réel, je n'ai jamais vraiment fait de système de vente flash, donc J'ai fait une hypothèse. Un simple système de vente flash a été créé pour « étancher l'envie », et les idées promotionnelles suivaient toujours la conception de l'article précédent.

Analyse

Une grande quantité de trafic afflue pendant la vente flash. La requête est fréquemment actualisée avant le début de la vente flash. Si une grande quantité de trafic atteint instantanément la base de données, c'est très. facile de provoquer l'effondrement de la base de données. Par conséquent, le travail principal de la vente flash est de filtrer le trafic couche par couche et enfin de permettre au trafic le moins et doux possible d'entrer dans la base de données.

Les ventes flash habituelles impliquent qu'un grand nombre d'utilisateurs s'emparent d'une petite quantité de marchandises. Pour des besoins comme celui-ci, la simple mise en cache de l'inventaire peut filtrer une grande quantité de trafic avant de créer une commande.

Mais mais mais, cela ne semble pas du tout être un défi ! Pour augmenter un peu la difficulté, supposons que notre vente flash soit comme s'emparer de téléphones portables Xiaomi, et si 1 million de personnes s'emparaient de 100 000 téléphones portables ? Faire la queue pendant les ventes urgentes de Xiaomi est une méthode (même si l'expérience n'est pas très bonne), et la conception de notre vente flash sera basée sur cette idée à l'avenir.

En ce qui concerne Xiaomi, je dois dire que cela m'a fait comprendre que « la chance fait aussi partie de la force ! »

Méthode de limitation de courant frontale : aléatoire ( 0, 1) ? axios.post : wait(30, 'Tout est terminé !')

Commençons par l'analyse de certains détails du code. En principe, la logique métier d'origine devrait être modifiée le moins possible. que possible. De plus, il n'y a pas de gameplay avancé tel qu'un disjoncteur de service ou une mise en cache multi-niveaux dans l'article suivant, il s'agit simplement d'une conception commerciale relativement simple.

Démarrer

L'opérateur ajoute une variante à la promotion de la vente flash en arrière-plan et définit l'inventaire de la vente flash/le taux de remise de la vente flash/l'heure de début et l'heure de fin, etc. peut obtenir quelque chose comme de telles données.

// 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', // 秒杀期间变体图片
    }
}

La première chose est de mettre en cache les informations de promotion une fois la promotion de vente flash créée avec succès

# 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
    );
  }
}

Passer une commande

L'interface de commande existante, après avoir reçu les informations de variante , nous ne savons pas laquelle des listes de variantes actuelles participe à la promotion. L'opération de jugement nécessite ici un grand nombre d'opérations de requête de base de données.

Nous écrivons donc ici une nouvelle API pour la vente flash. Lorsque le front-end détecte que la variante actuelle est en promotion de vente flash, il passe à l'API de commande de vente flash.

Bien sûr, nous utilisons toujours l'API de commande d'origine, et il n'y a aucun problème à passer un logo sur le front-end.

Un point qui nécessite une explication est que le passage d'une commande est généralement divisé en deux étapes

La première étape consiste à "passer à la caisse" pour générer une commande de paiement. adresse pour la commande, coupons, modes de paiement, etc.

La deuxième étape est "confirmer". A ce moment, la commande sera confirmée, l'inventaire sera verrouillé et l'utilisateur pourra effectuer le paiement. Généralement si le paiement n'est pas effectué dans le délai imparti, la commande est annulée et l'inventaire est débloqué.

Ainsi, dans un premier temps, les utilisateurs seront filtrés et mis en file d'attente pour empêcher les opérations ultérieures telles que la sélection d'adresses et de coupons d'avoir un impact sur la base de données.

# 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('结账订单创建中');
}

Vous pouvez voir qu'aucune opération de base de données n'est impliquée dans l'API de paiement de la vente flash. Et la tâche de création d'une commande est répartie dans la file d'attente via la répartition, et les utilisateurs font la queue pendant le temps correspondant dans l'ordre d'entrée dans la file d'attente.

La question est maintenant de savoir comment informer le client une fois la commande créée avec succès ?

Notification client

La solution ici n'est rien de plus qu'un sondage ou un websocket. Ici, nous choisissons un websocket qui consomme moins de performances du serveur et utilisons laravel-echo fourni par laravel ( laravel-echo-server ). Lorsque la vente flash de l'utilisateur réussit, le front-end et le back-end établissent un lien websocket. Une fois la commande de paiement back-end créée avec succès, le front-end est informé de passer à l'étape suivante.

Backend

La prochaine chose que le backend doit faire est d'envoyer un événement "OrderChecked" au canal correspondant du websocket pour indiquer que la commande est effectuée après que la commande dans le travail "CheckoutOrder" soit créé avec succès. La commande a été créée et l'utilisateur peut passer à l'étape suivante.

# 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);
    }
}

Supposons que l'ID utilisateur qui se précipite actuellement pour acheter est 1. Pour résumer, le code ci-dessus consiste à transmettre un événement "OrderChecked" au canal privé "App.User.1" de websocket.

Front end

Le code ci-dessous est le projet par défaut initialisé à l'aide de l'outil 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>

Une chose à noter lors de l'utilisation de laravel-echo est qu'en raison de l'utilisation d'un canal privé, laravel-echo enverra une demande de publication à l'API du serveur /broadcasting/auth par défaut pour l'authentification. Cependant, étant donné que les catégories front-end et back-end sont utilisées à la place des modèles de lame, nous ne pouvons pas facilement obtenir le jeton csrf et la session pour effectuer certaines authentifications nécessaires.

La configuration de la diffusion et de laravel-echo-server doit donc être légèrement modifiée

# 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"

Déverrouillage de l'inventaire

Lorsque "l'inventaire" a été verrouillé pour cette commande, si l'utilisateur Lorsque vous déconnectez le websocket ou que vous partez pendant une longue période, vous devez déverrouiller l'inventaire pour éviter une occupation inutile de l'inventaire.

L'inventaire ici fait référence à l'inventaire du cache, et non à l'inventaire de la base de données. En effet, à ce moment, même si la commande est créée avec succès, elle est toujours en statut de paiement (aucune adresse, aucun mode de paiement, etc. n'a été sélectionné) et n'est pas visible dans l'espace personnel. L'inventaire de la base de données ne sera verrouillé que lorsque l'utilisateur confirmera la commande.

La mise en œuvre idéale ici est donc de renvoyer l'inventaire verrouillé pour cette commande après que l'utilisateur a déconnecté la connexion Websocket. Une fois la commande de paiement créée, une file d'attente différée est créée pour renvoyer le stock aux commandes qui n'ont pas été traitées depuis longtemps.

Mais laravel-echo est un système de diffusion et ne fournit pas de rappels pour les événements de déconnexion client. Il existe certaines méthodes pour implémenter les événements client que laravel écoute, comme l'ajout de hooks à laravel-echo-server Notify. laravel, mais l'implémentation de laravel-echo-server doit être modifiée. Je n'entrerai pas dans les détails ici. L'objectif est de fournir des idées de vente flash.

Résumé

Conception du système Flash Kill

L'image ci-dessus est le résumé logique du système Flash Kill. À ce stade, tout le processus de vente flash est terminé. De manière générale, la quantité de code n'est pas grande et la logique est relativement simple.

Comme le montre la figure, dans l'ensemble du processus, ce n'est que dans la file d'attente qu'il interagira avec MySQL Grâce à la limitation actuelle de la file d'attente, il peut s'adapter à l'endurance de MySQL. l'étendue maximale. Lorsque les performances de MySQL sont suffisantes, les utilisateurs peuvent consommer des commandes via un grand nombre de files d'attente en même temps, et l'utilisateur ne sera pas du tout conscient du processus de mise en file d'attente.

Si vous avez des questions ou de meilleures idées, veuillez laisser un message pour discussion~

Pour plus d'articles techniques liés à Laravel, veuillez visiter la colonne Tutoriel Laravel pour apprendre !

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn