優雅 VS 效能

gordon發表於2022-01-19

背景

公司是做跨境電商,由於外部原因,公司對IT費用做了大的調整,把伺服器配置對半砍,以前沒有出效能問題的程式碼,由於伺服器配置對半砍了以後,效能問題就出現了,也就有了這篇文章。伺服器具體表現是,一臺專門跑定時任務的伺服器在某幾個時間段CPU跑滿。cpu負載圖如:
7D5Mq0.png
定時任務的框架是用PHP+Swoole+Laravel部分元件。由於是定時任務,xhprof在此就派不上用場了,swoole tracer又收費。只好另想別的辦法,根據zabbix的cpu監控負載圖,cpu沾滿的時段去找相應的定時任務,找到可疑的定時任務後,在到程式碼裡面分塊統計程式碼執行時長,然後根據程式碼執行時長,找出效能瓶頸點。

業務背景

根據統計結果找到了相應的程式碼,這塊程式碼實現的功能是查詢指定商品的sku別名。比如商品sku:apple-13pro-red-64,在我們其它業務系統中可能叫apple-13p-red-64或apple-13pro-red,在匹配庫存和價格的時候,就需要用apple-13pro-red-64,apple-13p-red-64,apple-13pro-red三個sku去匹配。一個sku對應多個別名,一個別名對應一個sku。

程式碼

資料表sku_alias的結構如下:

欄位名稱 資料型別 備註
sku varchar sku
alias varchar sku對應的別名
private function getSKuAlias(string $sku): Collection
{
    if (is_null($this->allSkuAlias)) {
        $this->allSkuAlias = DB::table('sku_alias')->get();
    }

    $alias = $this->allSkuAlias->where('sku', $sku)->pluck('alias');
    $alias = $alias->merge($this->allSkuAlias->where('alias', $sku)->pluck('sku'));
    return $alias->push($sku)->toArray();
}

解釋一下上面的程式碼,查詢sku_alias資料表,將資料表的查詢結果給allSkuAlias屬性,然後去對allSkuAlias查詢sku或別名等於引數sku的資料。然後再將查詢結果返回。sku_alias的數表一共714條資料。伺服器配置沒有被砍的時候,可以掩蓋此段程式碼的效能問題,當伺服器配置被砍,效能問題就暴露了,因為getSKuAlias的時間複雜度是O(N),一共需要進行1428次查詢,更為糟糕的是getSKuAlias方法還會被迴圈呼叫。cpu 100%也就是不足為怪了。為什麼getSKuAlias的時間複雜度是O(N),熟悉Laravel的朋友都知道

DB::table('sku_alias')->get(); //這裡返回的是Collection物件

而Collection的where方法最終是用php array_filter函式實現,而array_filter的時間複雜度是O(N)。既然知道了原因,最佳化方向就有了,把時間複雜度O(N)最佳化成O(1),第一時間想到的是用Hash陣列(索引陣列,其它語言裡面叫map或dict)。由於一個sku會有多個別名,所以需要兩個Hash陣列,在進行查詢sku對應別名時需要一個一對多的Hash陣列。在進行別名對應sku的查詢時,需要定義一對一的Hash陣列,以剛才apple-13pro-red-64的舉例,兩個Hash陣列定義如下:

[
    'apple-13p-red-64' => [
        'apple-13p-red-64',
        'apple-13pro-red',
    ],
] //sku對應的別名

[
    'apple-13p-red-64' => 'apple-13p-red-64',
    'apple-13pro-red' => 'apple-13p-red-64',
] //別名對應sku

最佳化後的最終程式碼:

private function getSKuAlias(string $sku): array
{
    if ($this->allSkuAlias === null) {
        $source =  DB::table('sku_alias')
            ->get(['sku', 'alias']);
        $this->allSkuAlias = $source->groupBy('sku')
            ->map(fn($alias) => $alias->pluck('alias')->toArray())
            ->toArray();
        $this->allAliasSku = $source->pluck('sku', 'alias')
            ->toArray();
    }

    $alias = [$sku];
    if (isset($this->allSkuAlias[$sku])) {
        array_push($alias, ...$this->allSkuAlias[$sku]);
    }

    if (isset($this->allAliasSku[$sku])) {
        $alias[] = $this->allAliasSku[$sku];
    }

    return array_unique($alias);
}

感悟

最佳化前的程式碼用Collection的where和pluck方法,程式碼行數少,6行程式碼就實現功能。最佳化後的程式碼行數增加,最佳化前時間複雜度是O(N),最佳化後近乎O(1)。效能方面的差距只有自己去細品。如果不是伺服器配置被砍半,這個問題也就不會出現。特別是當下伺服器的cpu的效能來說,要不是窮,誰會去做這種最佳化。日常開發的時候,也不會注意到效能方面的差異。所以選擇效能還是優雅?

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章