Skip to content

隊列(Queue)改善

乾太 edited this page Aug 23, 2021 · 1 revision

隊列(Queue)改善

為了改善原本的系統架構,我們再撰寫第二版本的路由(Router),這裡稱之為「v2」版本,並且路由器(Router)去指定一個控制器(Controller),事件作用隨機從使用者當中抽選一名使用者、隨機抽選數個商品,並且建立訂單,與系統架構的功能一樣,差別在於實做的方式不同。

1. 使用者建立訂單

首先路由器(Router)的部分,指定 /testing/v2/shopping 為主要負責處理建立訂單的控制器(Controller),另外指定 /testing/v2/shopping/delete 為主要負責處理建立訂單,建立完畢後自動刪除資料的控制器(Controller)。

/*
 * Shopping Testing Controllers
 * All route names are prefixed with 'frontend.testing'.
 */
Route::group([
    'prefix' => 'testing',
    'as' => 'testing.',
], function () {
    /**
     * All route names are prefixed with 'frontend.testing.v2'.
     */
    Route::group([
        'prefix' => 'v2',
        'as' => 'v2.',
    ], function () {
        Route::get('shopping', [ShoppingV2Controller::class, 'shopping'])
            ->name('shopping');

        Route::get('shopping/delete', [ShoppingV2Controller::class, 'shoppingDelete'])
            ->name('shopping.delete');
    });
});

控制器(Controller)的部分,主要撰寫 shopping 負責處理建立訂單的事件,以及 shoppingDelete 負責處理建立訂單並刪除的事件,這邊會發現沒有負責處理新增訂單的 createOrder 方法,我們把這件是移動到了 AsyncCreateOrderAsyncCreateAndDeleteOrder 去隊列(Queue)處理。

/**
 * Class ShoppingV2Controller.
 */
class ShoppingV2Controller extends Controller
{
    /**
     * @return \Illuminate\View\View
     */
    public function shopping()
    {
        $number = Str::random(32);

        AsyncCreateOrder::dispatch($number);

        return view('frontend.order.index-v2')
            ->with('number', $number);
    }

    /**
     * @return \Illuminate\View\View
     */
    public function shoppingDelete()
    {
        $number = Str::random(32);

        AsyncCreateAndDeleteOrder::dispatch($number);

        return view('frontend.order.index-v2')
            ->with('number', $number);
    }
}

首先負責建立訂單的 AsyncCreateOrder 隊列(Queue),會明顯看到原本 createOrder 方法內的程式被移動到此處,並稍加修改,負責做隨機抽選商品及數量並建立訂單的動作。

/**
 * Class AsyncCreateOrder.
 */
class AsyncCreateOrder implements ShouldQueue
{
    use Dispatchable,
        InteractsWithQueue,
        Queueable,
        SerializesModels;

    /**
     * @var string
     */
    protected $number;

    /**
     * Create a new job instance.
     *
     * @param string $number
     *
     * @return void
     */
    public function __construct(string $number)
    {
        $this->number = $number;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        $container = Container::getInstance();
        $orderService = $container->make(OrderService::class);
        $productService = $container->make(ProductService::class);

        $products = array();
        for ($i = 0; $i < mt_rand(1, 10); $i++) {
            $product = $productService->firstActive();
            if ($product->count >= 10) {
                array_push($products, array(
                    'id' => $product->id,
                    'count' => mt_rand(1, 10),
                ));
            } else {
                array_push($products, array(
                    'id' => $product->id,
                    'count' => mt_rand(1, $product->count),
                ));
            }
        }

        $orderService->store(array(
            'model_id' => mt_rand(2, 11),
            'type' => Orders::UNPAID,
            'active' => true,
            'items' => $products,
            'number' => $this->number,
        ));
    }
}

另外 AsyncCreateAndDeleteOrder 的部分則是多了刪除的功能。

/**
 * Class AsyncCreateAndDeleteOrder.
 */
class AsyncCreateAndDeleteOrder implements ShouldQueue
{
    use Dispatchable,
        InteractsWithQueue,
        Queueable,
        SerializesModels;

    /**
     * @var string
     */
    protected $number;

    /**
     * Create a new job instance.
     *
     * @param string $number
     *
     * @return void
     */
    public function __construct(string $number)
    {
        $this->number = $number;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        $container = Container::getInstance();
        $orderService = $container->make(OrderService::class);
        $productService = $container->make(ProductService::class);

        $products = array();
        for ($i = 0; $i < mt_rand(1, 10); $i++) {
            $product = $productService->firstActive();
            if ($product->count >= 10) {
                array_push($products, array(
                    'id' => $product->id,
                    'count' => mt_rand(1, 10),
                ));
            } else {
                array_push($products, array(
                    'id' => $product->id,
                    'count' => mt_rand(1, $product->count),
                ));
            }
        }

        $order = $orderService->store(array(
            'model_id' => mt_rand(2, 11),
            'type' => Orders::UNPAID,
            'active' => true,
            'items' => $products,
            'number' => $this->number,
        ));

        $orderService->delete($order);
        $orderService->destroy($order);
    }
}

最後因為是異步處理,所以控制器(Controller)去觸發隊列(Queue)之後,並不會等待隊列(Queue)完成任務才接著進行,而是觸發隊列(Queue)之後,繼續進行接下來的項目,所以我們需要讓前端(Frontend)有個等待的機制,不能回應就直接抓資料,這會導致資料可能是空白的,因此我們在這邊寫了一個 Vue 元件(Component)負責處理這段。

<template>
    <article class="card">
        <header class="card-header"> My Orders / Tracking </header>
        <div class="card-body">
            <h1 class="text-center py-5">Thank you for your order!</h1>
            <h6>Order ID: {{ number }}</h6>
            <article class="card">
                <div class="card-body row">
                    <div class="col"> <strong>Estimated Delivery time:</strong> <br>-</div>
                    <div class="col"> <strong>Shipping BY:</strong> <br>-</div>
                    <div class="col"> <strong>Status:</strong> <br>{{ order.type }}</div>
                    <div class="col"> <strong>Tracking:</strong> <br>-</div>
                </div>
            </article>
            <hr>

            <article class="card">
                <header class="card-header"> Payment Summary </header>
                <div class="row row-main p-2 mt-2" v-for="(item, index) in order.items" v-bind:key="index">
                    <div class="col-2 text-center">
                        <img class="img-fluid" style="height: 128px;" src="/img/product/default.png">
                    </div>
                    <div class="col-8">
                        <div class="row d-flex">
                            <p class="w-100"><b>{{ item.name.substr(0, 24) }} ...</b></p>
                        </div>
                        <div class="row d-flex">
                            <p class="w-100 text-muted">{{ item.description.substr(0, 128) }} ...</p>
                        </div>
                    </div>
                    <div class="col-2 d-flex justify-content-end">
                        <p class="text-center">
                            <b>{{ item.count }}</b> × <b>$ {{ item.price.toLocaleString('en-US') }}.00</b>
                        </p>
                    </div>
                </div>
                <hr class="mx-3">
                <div class="total p-2">
                    <div class="row">
                        <div class="col"><b>Total:</b></div>
                        <div class="col d-flex justify-content-end"><b>$ {{ order.price.toLocaleString('en-US') }}.00</b></div>
                    </div>
                </div>
            </article>
            <hr>
            <a href="/" class="btn btn-warning" data-abc="true">
                <i class="fa fa-chevron-left"></i> Back to Dashborad
            </a>
        </div>
    </article>
</template>

<script>
    export default {
        name: "OrderInfoV2",
        props:{
            number: {
                type: String,
                require: true,
            },
        },
        data() {
            return {
                order: {
                    type: null,
                    price: null,
                    items: [],
                },
            }
        },
        mounted() {
            axios.get(`/api/testing/v2/order/${this.number}`)
                .then(response => {
                    this.order = response.data.data;
                })
                .catch(error => console.log(error));
        },
    }
</script>

為了讓 Vue 元件(Component)能夠讀取訂單資料,我們需要再撰寫一支應用程式介面(Application Programming Interface, API),其功能是將訂單資料回應出來。

/**
 * Class ShoppingV2Controller.
 */
class ShoppingV2Controller extends Controller
{
    /**
     * @var OrderService
     */
    protected $orderService;

    /**
     * ShoppingV2Controller constructor.
     *
     * @param OrderService $orderService
     */
    public function __construct(OrderService $orderService)
    {
        $this->orderService = $orderService;
    }

    /**
     * @param string $number
     *
     * @return \Illuminate\View\View
     */
    public function order(string $number)
    {
        if ($order = $this->orderService->findByNumber($number)) {
            return new OrderResource($order);
        }

        return [
            'data' => [],
        ];
    }
}

這樣我們就完成了具有隊列(Queue)功能的「v2」版本,接下來我們需要測試這個版本的數值,首先從 10 個連線 300 次連續點擊開始。

This is ApacheBench, Version 2.3 <$Revision: 1874286 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking laravel-typical-high-load-exam.herokuapp.com (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Finished 300 requests

Server Software:        nginx
Server Hostname:        laravel-typical-high-load-exam.herokuapp.com
Server Port:            80

Document Path:          /testing/v2/shopping
Document Length:        9408 bytes

Concurrency Level:      10
Time taken for tests:   65.461 seconds
Complete requests:      300
Failed requests:        0
Total transferred:      3182100 bytes
HTML transferred:       2822400 bytes
Requests per second:    4.58 [#/sec] (mean)
Time per request:       2182.046 [ms] (mean)
Time per request:       218.205 [ms] (mean, across all concurrent requests)
Transfer rate:          47.47 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:      210  215   3.8    214     232
Processing:   347 1908 197.0   1935    2478
Waiting:      344 1213 476.2   1282    1961
Total:        564 2123 197.3   2149    2696

Percentage of the requests served within a certain time (ms)
  50%   2149
  66%   2156
  75%   2161
  80%   2164
  90%   2168
  95%   2172
  98%   2182
  99%   2187
 100%   2696 (longest request)

接著測試 100 個連線 600 次連續點擊,我們可以發現連線時間(Connection Times)的部分急遽上升,但數值比起原本的系統架構,反而更趨近於初始數值。

This is ApacheBench, Version 2.3 <$Revision: 1874286 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking laravel-typical-high-load-exam.herokuapp.com (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Finished 600 requests

Server Software:        nginx
Server Hostname:        laravel-typical-high-load-exam.herokuapp.com
Server Port:            80

Document Path:          /testing/v2/shopping
Document Length:        9408 bytes

Concurrency Level:      100
Time taken for tests:   128.526 seconds
Complete requests:      600
Failed requests:        0
Total transferred:      6364200 bytes
HTML transferred:       5644800 bytes
Requests per second:    4.67 [#/sec] (mean)
Time per request:       21420.971 [ms] (mean)
Time per request:       214.210 [ms] (mean, across all concurrent requests)
Transfer rate:          48.36 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:      208  213   3.8    211     227
Processing:   340 19336 4671.4  21083   21397
Waiting:      330 10535 6117.7  10477   21200
Total:        550 19549 4671.6  21297   21613

Percentage of the requests served within a certain time (ms)
  50%  21297
  66%  21312
  75%  21322
  80%  21327
  90%  21351
  95%  21391
  98%  21410
  99%  21414
 100%  21613 (longest request)

最後我們測試 300 個連線 1500 次連續點擊,這裡我們可以清楚看到除了數值比起原本的系統架構,反而更趨近於初始數值以外,平均每秒可回應要求(Requests per second)從原本大約 4 上升到大約 4.6 個回應,更接近於初始數值的 5 個回應。

This is ApacheBench, Version 2.3 <$Revision: 1874286 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking laravel-typical-high-load-exam.herokuapp.com (be patient)
Completed 150 requests
Completed 300 requests
Completed 450 requests
Completed 600 requests
Completed 750 requests
Completed 900 requests
Completed 1050 requests
Completed 1200 requests
Completed 1350 requests
Completed 1500 requests
Finished 1500 requests

Server Software:        nginx
Server Hostname:        laravel-typical-high-load-exam.herokuapp.com
Server Port:            80

Document Path:          /testing/v2/shopping
Document Length:        9408 bytes

Concurrency Level:      300
Time taken for tests:   320.740 seconds
Complete requests:      1500
Failed requests:        0
Total transferred:      15910500 bytes
HTML transferred:       14112000 bytes
Requests per second:    4.68 [#/sec] (mean)
Time per request:       64147.904 [ms] (mean)
Time per request:       213.826 [ms] (mean, across all concurrent requests)
Transfer rate:          48.44 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:      208  213   3.9    211     234
Processing:   435 57364 15166.5  63695   64168
Waiting:      423 31848 18444.4  31841   63909
Total:        646 57577 15166.3  63908   64386

Percentage of the requests served within a certain time (ms)
  50%  63908
  66%  63951
  75%  64003
  80%  64017
  90%  64040
  95%  64052
  98%  64069
  99%  64078
 100%  64386 (longest request)

2. 查詢訂單內容

接著我們需要改善查詢訂單的功能,這邊我們不直接透過訂單 UUID 數值來查找模型(Model),這會直接接觸到資料庫,我們先透過 Redis 去查找訂單資料有沒有已經先存入快取(Cache),如果沒有存入(Cache),才會接著往下查找資料庫的資訊,並將結果存入 Redis 的快取(Cache)當中。

/**
 * Class ShoppingV2Controller.
 */
class ShoppingV2Controller extends Controller
{
    /**
     * @param string $uuid
     *
     * @return \Illuminate\View\View
     */
    public function order(string $uuid)
    {
        if ($order = Redis::get('order:' . $uuid)) {
            return view('frontend.order.index-v2r')
                ->with('order', json_decode($order));
        }

        if ($order = Orders::uuid($uuid)->first()) {
            $items = $order->items;
            foreach ($items as $item) {
                $item->product;
            }
            Redis::set("order:$uuid", $order);

            return view('frontend.order.index-v2r')
                ->with('order', $order);
        }

        return redirect()->route('frontend.index')->withFlashDanger(__('Order is Not Found.'));
    }
}

最後單獨測試 300 個連線 1500 次連續點擊,這裡可以明顯看到平均每秒可回應要求(Requests per second)與寫入的測試相對接近,也跟原本架構所跑出來的數值差不多,推測有可能是抓得資料不夠多、運算量不夠大而導致。

This is ApacheBench, Version 2.3 <$Revision: 1874286 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking laravel-typical-high-load-exam.herokuapp.com (be patient)
Completed 150 requests
Completed 300 requests
Completed 450 requests
Completed 600 requests
Completed 750 requests
Completed 900 requests
Completed 1050 requests
Completed 1200 requests
Completed 1350 requests
Completed 1500 requests
Finished 1500 requests

Server Software:        nginx
Server Hostname:        laravel-typical-high-load-exam.herokuapp.com
Server Port:            80

Document Path:          /testing/v2/order/76c399c1-042a-46e9-81f3-75845368ca24
Document Length:        16341 bytes

Concurrency Level:      300
Time taken for tests:   303.997 seconds
Complete requests:      1500
Failed requests:        0
Total transferred:      26310000 bytes
HTML transferred:       24511500 bytes
Requests per second:    4.93 [#/sec] (mean)
Time per request:       60799.313 [ms] (mean)
Time per request:       202.664 [ms] (mean, across all concurrent requests)
Transfer rate:          84.52 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:      198  202   2.4    202     217
Processing:   495 54337 14381.2  60340   61004
Waiting:      334 30156 17474.1  30133   60436
Total:        698 54539 14381.2  60541   61203

Percentage of the requests served within a certain time (ms)
  50%  60541
  66%  60572
  75%  60606
  80%  60621
  90%  60662
  95%  60673
  98%  60679
  99%  60682
 100%  61203 (longest request)
Clone this wiki locally