記一次生產者消費者讀取 200w 資料, 寫入 2000w 資料的過程

surest發表於2019-05-29

記一次生產者消費者讀取200w資料, 寫入2000w資料的過程

前情提要: 由於開發階段,資料量的不充足,同時線上的產品資料需要使用到經緯度這一塊資料. 就有資料部門的同事爬去了100w條資料,給的也是csv檔案, 由此產生一個問題, 如何來大批量的讀取這些資料呢

這裡我們想到了兩種方式, csv檔案讀取採用檔案行讀取, 讀取excel 採用 phpexcel

為了相容以後可能匯入的xls檔案,我這裡就採用了 讀取excel 的方法

我們需要考慮到兩個問題

1) phpexcel 是讀取到記憶體中的, 100w資料雖然不多,但如果不去設定響應的引數,極有可能導致記憶體溢位

2) 讀取的這是商圈資訊, 可是還需要進行 寫入假資料 : 店鋪 \ 折扣 等, 這些都是應該在讀取的時候同時進行寫入mysql的, 怎麼解決讀寫產生的發生異常問題

通過搜尋資料, 找到了可能比較適合現階段的做法的解決辦法

通過生產者 - 消費者模式 進行

何為 生產者 - 消費者模式

見文章: 聊聊併發——生產者消費者模式

線上程世界裡,生產者就是生產資料的執行緒,消費者就是消費資料的執行緒。在多執行緒開發當中,如果生產者處理速度很快,而消費者處理速度很慢,那麼生產者就必須等待消費者處理完,才能繼續生產資料。同樣的道理,如果消費者的處理能力大於生產者,那麼消費者就必須等待生產者。為了解決這個問題於是引入了生產者和消費者模式。

依照現有的問題自我解釋一遍: 讀資料的方法 便是生產者, 寫資料的操作便是消費者, 通過該種方式,將讀 \ 寫隔離開, 互不影響

以上也會產生幾個問題

1) 生產者速度大於消費者速度
2) 消費者速度大於生產者速度

最簡單的解決辦法,便是增加生產者 或 增加消費者 提高速度. 這也是其靈魂所在吧

兩者之間的通訊方式當然多種多樣, 這裡我們採用 redis 佇列作為通訊介質進行

安裝相關的包工具

excel 套餐包 : phpoffice/phpspreadsheet

假資料包 : fzaninotto/faker

實現

use PhpOffice\PhpSpreadsheet\Reader\IReadFilter;

# 參考來自於
# https://phpspreadsheet.readthedocs.io/en/latest/topics/reading-files/

// 這是一個指定讀取某些行的過濾器, 來自於官方文件寫法
class ChunkReadFilter implements IReadFilter
{
    private $startRow = 0;

    private $endRow = 0;

    /**
    * Set the list of rows that we want to read.
    *
    * @param mixed $startRow
    * @param mixed $chunkSize
    */
    public function setRows($startRow, $chunkSize)
    {
        $this->startRow = $startRow;
        $this->endRow = $startRow + $chunkSize;
    }

    public function readCell($column, $row, $worksheetName = '')
    {
        //  Only read the heading row, and the rows that are configured in $this->_startRow and $this->_endRow
        if (($row == 1) || ($row >= $this->startRow && $row < $this->endRow)) {
            return true;
        }

        return false;
    }
}

-----------

/**
* 資料轉換
* Class Transform
* @package app\literacy
*/
class Transform
{
    protected $inputFileType = 'Csv';
    protected $inputFileName = __DIR__ . '/excels/d88_info.map_position.csv';
    protected $list_key = "excel_discount";
    protected $end = 1;

    /**
    * 使用cli 命令執行
    * php think psysh
    * (new \app\literacy\Transform())->write_excel()
    * 執行了這個命令之後,再次執行
    * (new \app\literacy\Transform())->consumer();
    *
    * write_excel 是生產者, 獲取資料,寫入到redis佇列中
    * consumer 是消費者, 從redis中獲取到資料, 寫入到資料庫中
    */
    public function write_excel()
    {
        $reader = IOFactory::createReader($this->inputFileType);

        // 設定每個迴圈讀取的行數
        $chunkSize = 100;

        $chunkFilter = new ChunkReadFilter();

        # 設定過濾器
        $reader->setReadFilter($chunkFilter);

        // Loop to read our worksheet in "chunk size" blocks
        for ($startRow = 2; $startRow <= 1000000; $startRow += $chunkSize) {
            $chunkFilter->setRows($startRow, $chunkSize);
            $spreadsheet = $reader->load($this->inputFileName);
            $sheetData = $spreadsheet->getActiveSheet()->toArray(null, true, true, true);
            $this->producer($sheetData);

            unset($sheetData);
            unset($spreadsheet);
        }
    }

    .....

$sheetData 就是我們讀取的資料,他是迴圈方式讀取的, 每次讀取的設定的是100行, 我測試了一下,讀取100行的速度大約在1.5s

生產者

建立一個生產者,將資料寫入到 redis佇列中

    ....

    /**
    * 生產者
    */
    public function producer($data)
    {
        if($data) {
            $redis = redis_connect(2);
            $data = serialize($data);
            $key = md5(microtime(true));
            $redis->set($key, $data, 3600);
            $redis->lPush($this->list_key, $key);
        }else{
            echo "未讀取到資料";
        }
    }

這裡我們採用key-value的形式寫入資料, 將key寫入到佇列中, 同理, 獲取資料的時候,去list - key資料後再獲取value資料, 並寫入資料庫

消費者

    /**
    * 消費者
    * 這個需要進行非同步執行, 例如使用命令列
    * php think psysh
    * (new \app\literacy\Transform())->consumer();
    */
    public function consumer()
    {
        $redis = redis_connect(2);
        while ($this->end < 10) {
            $keys = $redis->blPop([$this->list_key], 50);
            if(!empty($keys)) {
                $key = $keys[1];
                $data = $redis->get($key);
                if($data) {
                    $redis->del($key);
                    $this->write_mysql(unserialize($data));
                }
            }else{
                $this->end = $this->end + 1;
            }
        }
    }

    /**
    * 把資料寫入資料庫
    * @param $data
    */
    public function write_mysql($origin_data)
    {
        $data = [];
        $superStoreIds = Superstore::field('id')->all();
        foreach ($origin_data as $k => $item) {
            if(!is_null($item['A'])) {
                $data['superstore_name'] = random_int(0,10000) . "店";
                $data['longitude'] = $item['C'];
                $data['latitude'] = $item['D'];
                $data['type'] = random_int(0,2);
                $data['create_time'] = time();
                $data['update_time'] = time();
                $data['creator_id'] = 1;
                $data['store_num'] = random_int(2,20);
                $sid = Db::connect('localhost_connect')->table('d88_superstore')->insertGetId($data);

                echo "商圈 +1";
                $this->store($sid, $data['store_num']);
            }

        }
    }

如上, 我們能看到 blPop: 移出並獲取列表的第一個元素, 如果列表沒有元素會阻塞列表直到等待超時或發現可彈出元素為止. 也就是說,獲取不到資料的時候, 會進行阻塞, 同時使用while也會為程式阻塞一段時間

不過由於我們需要在建立商圈的同時建立店鋪\折扣, 導致, 讀取200w條資料, 實際加上假資料要寫入1000 多萬資料.

所以我們需要開啟多個執行緒來跑這個消費者的命令

採用的方法

1) workman 來讀取

2) cli 執行, 多視窗

簡單點,我們直接開啟多視窗資訊跑命令

截圖

每天3小時...加油

相關文章