【laravel5】詳解laravel的chunk和chunkById函數


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 許可協議。轉載請注明出處!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM