1、laravel的chunk和chunkById主要處理 比較大的數據,通過分塊來處理。
優缺點:
1)chunk的話,底層原理是通過分頁page參數處理,update的時候回存在漏一半數據情況(並且MySQL的分頁在數據量大時,嚴重影響查詢效率)
2)因為2,出現了chunkById的優化版本,通過主鍵priKey(或者其他column,下面會講)來處理數據,完美的避過update和分頁效率低下問題
3、參考文檔:
https://www.lqwang.net/13.html
https://segmentfault.com/a/1190000015284897
https://learnku.com/articles/15655/the-use-of-offset-limit-or-skip-and-take-in-laravel
4、本文具體講解chunkById使用和底層運行原理,chunk不講。先上干貨代碼:
<?php namespace App\Console\Commands; use Illuminate\Console\Command; use App\Model\OrderReturnTrace; use App\Dao\UpPayOrderDao; use App\Model\OrderPayTrace; use App\Model\OrderTrace; class OmsOrderDeliveryQuery extends Command { /** * The name and signature of the console command. * * @var string */ protected $signature = 'oms:odquery {order_id?}'; protected $order_id; /** * The console command description. * * @var string */ protected $description = '待發貨單物流查詢及更新'; /** * Create a new command instance. * * @return void */ public function __construct() { parent::__construct(); } /** * Execute the console command. * * @return mixed */ public function handle() { // $this->order_id = $this->argument('order_id') ?? ''; try{ if(!empty($this->order_id)) { $orderM = OrderTrace::where(['order_id'=>$this->order_id, 'pay_status'=>2, 'order_status'=>1, 'delivery_status'=>1])->first(); if(empty($orderM)) return ; if(! $res = UpPayOrderDao::omsDeliveryQuery($orderM->order_id) ){ return false; } # 更新物流信息 UpPayOrderDao::orderDeliveryRelease($this->order_id, $res); return true; } else { # 執行oms物流查詢 $echoCsv = function($item){ if(! $res = UpPayOrderDao::omsDeliveryQuery($item->order_id) ){ log_write("定時任務退款單refundId{$item->refund_id}狀態查詢有誤"); return false; } # 更新物流信息 UpPayOrderDao::orderDeliveryRelease($item->order_id, $res); }; # 批量查詢字段涉及update,使用chunkById($count, $callback, $coulumn, $alias)處理,避免chunk漏數據 $refundAll = OrderTrace::where('pay_status', 2) ->where('order_type', 0) ->where('order_status', 1) ->where('pay_channel', 2) ->where('delivery_status', 1) ->select('id','order_id',''); $refundAll->chunkById(200, function($refundAll) { foreach ($refundAll as $item) { $echoCsv($item); } }, 'id'); } }catch(\Exception $e) { log_write("定時任務refundOrderUnionPayQuery,異常:msg={$e->getMessage()}"); UpPayOrderDao::sendEmail('定時任務refundOrderUnionPayQuery,異常', $e->getMessage()); return false; } return ; } }
5、chunkById的底層原理&運行:
(或者其他column,下面會講)
參考這篇文章,講得很清楚的:https://www.lqwang.net/13.html
Laravel chunk和chunkById的坑
公司中的項目在逐漸的向Laravel框架進行遷移。在編寫定時任務腳本的時候,用到了chunk和chunkById的API,記錄一下踩到的坑。
一、前言
數據庫引擎為innodb。
表結構簡述,只列出了本文用到的字段。
字段 類型 注釋
id int(11) ID
type int(11) 類型
mark_time int(10) 標注時間(時間戳)
索引,也只列出需要的部分。
索引名 字段
PRIMARY id
idx_sid_blogdel_marktime type
blog_del
mark_time
Idx_marktime mark_time
二、需求
每天凌晨一點取出昨天標注type為99的所有數據,進行邏輯判斷,然后進行其他操作。本文的重點只在於取數據的階段。
數據按月分表,每個月表中的數據為1000w上下。
三、chunk處理數據
代碼如下
$this->dao->where('type', 99)->whereBetween('mark_time', [$date, $date+86399])->select(array('mark_time', 'id'))->chunk(1000, function ($rows){
// 業務處理
});
從一個月中的數據,篩選出type為99,並且標注時間在某天的00:00:00-23:59:59的數據。可以使用到mark_time和type的索引。
type為99,一天的數據大概在15-25w上下的樣子。使用->get()->toArray()內存會直接炸掉。所以使用chunk方法,每次取出1000條數據。
使用chucnk,不會出現內存不夠的情況。但是性能較差。粗略估計,從一月數據中取出最后一天的數據,跑完20w數據大概需要一兩分鍾。
查看源碼,底層的chunk方法,是為sql語句添加了限制和偏移量。
1
select * from `users` asc limit 500 offset 500;
在數據較多的時候,越往后的話效率會越慢,因為Mysql的limit方法底層是這樣的。
limit 10000,10
是掃描滿足條件的10010行,然后扔掉前面的10000行,返回最后最后20行。在數據較多的時候,性能會非常差。
查了下API,對於這種情況Laraverl提供了另一個API chunkById。
四、chunkById 原理
使用limit和偏移量在處理大量的數據會有性能的明顯下降。於是chunkById使用了id進行分頁處理。很好理解,代碼如下:
1
select * from `users` where `id` > :last_id order by `id` asc limit 500;
API會自動保存最后一個ID,然后通過id > :last_id 再加上limit就可以通過主鍵索引進行分頁。只取出來需要的行數。性能會有明顯的提升。
五、chunkById的坑
API顯示chunk和chunkById的用法完全相同。於是把腳本的代碼換成了chunkById。
$this->dao->where('type', 99)->whereBetween('mark_time', [$date, $date+86399])->select(array('mark_time', 'id'))->chunkById(1000, function ($rows){
// 業務處理
});
在執行腳本的時候,1月2號和1月1號的數據沒有任何問題。執行速度快了很多。但是在執行12月31號的數據的時候,發現腳本一直執行不完。
在定位后發現是腳本沒有進入業務處理的部分,也就是sql一直沒有執行完。當時很疑惑,因為剛才執行的沒問題,為什么執行12月31號的就出問題了呢。
於是查看sql服務器中的執行情況。
1
show full processlist;
發現了問題。上節說了chunkById的底層是通過id進行order by,然后limie取出一部分一部分的數據,也就是我們預想的sql是這樣的。
1
select * from `tabel` where `type` = 99 and mark_time between :begin_date and :end_date limit 500;
explain出來的情況如下:
select_type type key rows Extra
SIMPLE Range idx_marktime 2370258 Using index condition; Using where
實際上的sql是這樣的:
1
select * from `tabel` where `type` = 99 and mark_time between :begin_date and :end_date order by id limit 500;
實際explain出來的情況是這樣的:
select_type type key rows Extra
SIMPLE Index PRIMARY 4379 Using where
chunkById會自動添加order by id。innodb一定會使用主鍵索引。那么就不會再使用mark_time的索引了。導致sql執行效率及其緩慢。
六、解決方法
再次查看chunkById的源碼。
/**
* Chunk the results of a query by comparing numeric IDs.
*
* @param int $count
* @param callable $callback
* @param string|null $column
* @param string|null $alias
* @return bool
*/
public function chunkById($count, callable $callback, $column = null, $alias = null)
{
$column = is_null($column) ? $this->getModel()->getKeyName() : $column;
$alias = is_null($alias) ? $column : $alias;
$lastId = null;
do {
$clone = clone $this;
// We'll execute the query for the given page and get the results. If there are
// no results we can just break and return from here. When there are results
// we will call the callback with the current chunk of these results here.
$results = $clone->forPageAfterId($count, $lastId, $column)->get();
$countResults = $results->count();
if ($countResults == 0) {
break;
}
// On each chunk result set, we will pass them to the callback and then let the
// developer take care of everything within the callback, which allows us to
// keep the memory low for spinning through large result sets for working.
if ($callback($results) === false) {
return false;
}
$lastId = $results->last()->{$alias};
unset($results);
} while ($countResults == $count);
return true;
}
能看到這個方法有四個參數count,callback,column,alias。
默認的column為null,第一行會進行默認賦值。
$column = is_null($column) ? $this->getModel()->getKeyName() : $column;
往下跟:
/**
* Get the primary key for the model.
*
* @return string
*/
public function getKeyName()
{
return $this->primaryKey;
}
/**
* The primary key for the model.
*
* @var string
*/
protected $primaryKey = 'id';
能看到默認的column為id。
進入forPageAfterId方法。
/**
* Constrain the query to the next "page" of results after a given ID.
*
* @param int $perPage
* @param int|null $lastId
* @param string $column
* @return \Illuminate\Database\Query\Builder|static
*/
public function forPageAfterId($perPage = 15, $lastId = 0, $column = 'id')
{
$this->orders = $this->removeExistingOrdersFor($column);
if (! is_null($lastId)) {
$this->where($column, '>', $lastId);
}
return $this->orderBy($column, 'asc')
->take($perPage);//take取多少條
}
能看到如果lastId不為0則自動添加where語句,還會自動添加order by column。
看到這里就明白了。上文的chunkById沒有添加column參數,所以底層自動添加了order by id。走了主鍵索引,沒有使用上mark_time的索引。導致查詢效率非常低。
chunkById的源碼顯示了我們可以傳遞一個column字段來讓底層使用這個字段來order by。
代碼修改如下:
$this->dao->where('type', 99)->whereBetween('mark_time', [$date, $date+86399])->select(array('mark_time', 'id'))->chunkById(1000, function ($rows){
// 業務處理
}, 'mark_time');
這樣最后執行的sql如下:
1
select * from `tabel` where `type` = 99 and mark_time between :begin_date and :end_date order by mark_time limit 500;
再次執行腳本,大概執行一次也就十秒作用了,性能提升顯著。
七、總結
chunk和chunkById的區別就是chunk是單純的通過偏移量來獲取數據,chunkById進行了優化,不實用偏移量,使用id過濾,性能提升巨大。在數據量大的時候,性能可以差到幾十倍的樣子。
而且使用chunk在更新的時候,也會遇到數據會被跳過的問題。詳見解決Laravel中chunk方法分塊處理數據的坑
同時chunkById在你沒有傳遞column參數時,會默認添加order by id。可能會遇到索引失效的問題。解決辦法就是傳遞column參數即可。
本人感覺chunkById不光是根據Id分塊,而是可以根據某一字段進行分塊,這個字段是可以指定的。叫chunkById有一些誤導性,chunkByColumn可能更容易理解。算是自己提的小小的建議。
本文標題:Laravel chunk和chunkById的坑
本文作者:旺陽
本文鏈接:https://www.lqwang.net/13.html
發布時間:2020-01-06
版權聲明:本博客所有文章除特別聲明外,均采用 CC BY-NC-SA 4.0 許可協議。轉載請注明出處!