記一次生產者消費者讀取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 執行, 多視窗
簡單點,我們直接開啟多視窗資訊跑命令