Home >PHP Framework >Laravel >Design of flash kill system

Design of flash kill system

步履不停
步履不停Original
2019-06-25 15:06:333703browse

Design of flash kill system

I have written an article before about the design of the promotion system and mentioned flash sales/direct discounts/juhuasuan, but in actual work, I have never really done a flash sale system, so I made a hypothetical A simple flash sale system was created to “quench the craving”, and the promotional ideas still followed the design of the previous article.

Analysis

A large amount of traffic flows in during the flash sale, and queries are frequently refreshed before the flash sale starts. If a large amount of traffic hits the database instantly, it is very easy to cause the database to collapse. Therefore, the main job of the flash sale is to filter the traffic layer by layer and finally allow as little and as smooth traffic as possible to enter the database.

Usual flash sales involve a large number of users snapping up a small amount of goods. For needs like this, simply caching the inventory can filter a large amount of traffic before actually creating an order.

But but but, it doesn’t seem to be challenging at all! To increase the difficulty a little bit, suppose our flash sale is like grabbing Xiaomi mobile phones, what if 1 million people grab 100,000 mobile phones? Queuing up during Xiaomi rush sales is one method (although the experience is not very good), and our flash sale design will be based on this idea in the future.

When it comes to Xiaomi, I have to say that it made me know what "luck is also a part of strength!"

Front-end current limiting method: random(0, 1) ? axios.post: wait(30, 'All finished!')

The following is an analysis of some code details. In principle, the original business logic should be changed as little as possible.

In addition, there are no advanced gameplay such as service circuit breaker and multi-level caching in the following article, just a relatively simple business design.

Start

The operator adds a variant to the flash sale promotion in the background and sets the flash sale inventory/flash sale discount rate/start time and end time, etc. We can get something like Such data.

// 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', // 秒杀期间变体图片
    }
}
The first thing is to cache the promotion information after the flash sale promotion is successfully created

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

Existing order interface, after receiving the variant information , we don’t know which of the current variant list is participating in the promotion. The judgment operation here requires a large number of database query operations.

So here we write a new API for flash sale. When the front end detects that the current variant is in flash sale promotion, it switches to the flash sale ordering API.

Of course, we still use the original order api, and there is no problem in passing a logo on the front end.

One thing that needs explanation is that placing an order is usually divided into two steps

The first step is to "checkout (checkout)" to generate a checkout order. The user can select the address for the checkout order. , coupons, payment methods, etc.

The second step is "confirm". At this time, the order will become confirmed, the inventory will be locked, and the user can make payment. Usually if payment is not made within the stipulated time, the order is canceled and the inventory is unlocked.

So in the first step, users will be filtered and queued to prevent subsequent operations such as selecting addresses and coupons from impacting the database.

# 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('结账订单创建中');
}
You can see that there is no database operation involved in the flash sale checkout api. And the task of creating an order is distributed to the queue through dispatch, and users wait in line for the corresponding time in the order of entering the queue.

The question now is, how to notify the client after the order is successfully created?

Client Notification

The solution here is nothing more than polling or websocket. Here we choose websocket that consumes less server performance and use laravel-echo provided by laravel ( laravel-echo-server ). When the user's flash sale is successful, the front-end and back-end establish a websocket link. After the back-end checkout order is successfully created, the front-end is notified to proceed with the next step.

Backend

The next thing the backend needs to do is to send an "OrderChecked" event to the corresponding channel of the websocket to indicate the checkout after the order in the "CheckoutOrder" Job is successfully created. The order has been created and the user can proceed to the next step.

# 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);
    }
}
Assume that the current user ID of the purchase is 1. To summarize, the above code is to push an "OrderChecked" event to the private channel "App.User.1" of websocket.

Front end

The code below is the default project initialized using the vue-cli tool.

// 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>
One thing to note when using laravel-echo is that due to the use of a private channel, laravel-echo will send a post request to the server api

/broadcasting/auth by default for authentication. . However, because the front-end and back-end categories are used instead of blade templates, we cannot easily obtain the csrf token and session to perform some necessary authentication.

So you need to slightly modify the configuration of broadcast and 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"
Inventory unlocking

When the "inventory" has been locked for this order, if the user When you disconnect the websocket or leave for a long time, you need to unlock the inventory to prevent meaningless occupation of the inventory.

The inventory here refers to cache inventory, not database inventory. This is because even if the order is successfully created at this time, it is still in checkout status (no address, payment method, etc. have been selected) and is not visible in the personal center. The database inventory will be locked only when the user confirms the order.

So the ideal implementation here is to return the locked inventory of the order after the user disconnects the websocket connection. After the checkout order is created, a delay queue is created to return inventory to orders that have not been operated for a long time.

But, laravel-echo is a broadcast system and does not provide callbacks for client disconnect events. There are some methods to implement client events that laravel listens to, such as adding hooks to laravel-echo-server. Notify laravel, but the implementation of laravel-echo-server needs to be modified. I won’t go into details here. The focus is to provide flash sale ideas.

Summary

Design of flash kill system

The above picture is a logical summary of the flash kill system. At this point, the entire flash sale process is over. Generally speaking, the amount of code is not much and the logic is relatively simple.

As can be seen from the figure, in the entire process, it interacts with mysql only in the queue. Through the current limiting of the queue, it can adapt to the endurance of mysql to the maximum extent. When mysql performance is sufficient, users can consume orders through a large number of queues at the same time, and the user will not be aware of the queuing process at all.

If you have any questions or better ideas, please leave a message for discussion~

For more Laravel-related technical articles, please visit the Laravel Tutorial column to learn!

The above is the detailed content of Design of flash kill system. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn