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的源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* 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,第一行会进行默认赋值。

1
$column = is_null($column) ? $this->getModel()->getKeyName() : $column;

往下跟:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 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方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 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);
}

能看到如果lastId不为0则自动添加where语句,还会自动添加order by column。

看到这里就明白了。上文的chunkById没有添加column参数,所以底层自动添加了order by id。走了主键索引,没有使用上mark_time的索引。导致查询效率非常低。

chunkById的源码显示了我们可以传递一个column字段来让底层使用这个字段来order by。

代码修改如下:

1
2
3
$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可能更容易理解。算是自己提的小小的建议。

本文非原创,转载于https://www.lqwang.net/13.html

Laravel chunk和chunkById的坑的更多相关文章

  1. Laravel学习--关于Relation的坑

    前段时间比较忙,就没有坚持写博客,但发现这周末再想捡起来,好难,一直到了今天晚上,才决定坐下来写一篇,哈哈哈-- 最近在用 Laravel 5.2,踩了几个关于 Relation 的坑,在这里用博客记 ...

  2. Laravel登录验证碰到的坑 哈希验证匹配问题

    用laravel 写登录验证 本来是用Crypt加密 添加用户到数据库的 后来验证密码  解密时一直报错 The payload is invaild 由于本人是laravel框架小白 自己思考许久未 ...

  3. 后端PHP框架laravel学习踩的各种坑

    安装完laravel的ventor目录后出现“Whoops, looks like something went wrong.”这样的错误信息 打开config/app.php,打开debug为tru ...

  4. php laravel 环境搭建

    最近上一个新项目,时间比较紧,为了满足业务需求,没有办法,只有上我大 php 了,找了一个带些基础的数据结构,用的是 laravel 搭建的,然后寻坑就开始了,先是构建 docker 镜像就坑了,然后 ...

  5. laravel 使用不同账号发送邮件的问题

    业务背景: 公司自己做的oa系统,不同的模块需要用不同的邮箱发送信息给收件人.比如:员工离职的时候用离职的邮箱发送离职邮件通知,员工入职的时候用入职的邮箱发送入职邮件通知.发邮件是一件耗时的任务,如果 ...

  6. 利用Git搭建自动部署的Laravel环境 - 钟晨宇的博客 - CSDN博客

    目标:服务器上搭建Laravel环境,本地使用IDE进行开发,使用Homestead做本地调试环境,代码提交后自动部署到服务器Root目录下. 下面是整个流程的示意图:  1. 准备工作,搭建LNMP ...

  7. webpack中利用require.ensure()实现按需加载

    webpack中的require.ensure()可以实现按需加载资源包括js,css等,它会给里面require的文件单独打包,不和主文件打包在一起,webpack会自动配置名字,如0.js,1.j ...

  8. webpack中实现按需加载

    webpack中的require.ensure()可以实现按需加载资源包括js,css等,它会给里面require的文件单独打包,不和主文件打包在一起,webpack会自动配置名字,如0.js,1.j ...

  9. require-ensure

    require-ensure 说明: require.ensure在需要的时候才下载依赖的模块,当参数指定的模块都下载下来了(下载下来的模块还没执行),便执行参数指定的回调函数.require.ens ...

随机推荐

  1. CF习题集一

    CF习题集一 一.CF915E Physical Education Lessons 题目描述 \(Alex\)高中毕业了,他现在是大学新生.虽然他学习编程,但他还是要上体育课,这对他来说完全是一个意 ...

  2. log4net用法实例

    内容转载自:https://www.cnblogs.com/youring2/archive/2011/04/27/2030424.html 1.引用log4net.dll 2.在AssemblyIn ...

  3. GitHub 热点速览 Vol.31:在?跑个 GitHub 评分如何?

    摘要:个性化的 GitHub README 自从 7 月上线之后一直风靡在各大技术平台,当中最有意思的莫过于代表你技术的 GitHub Readme Stats 了,除了能显示你提交的 pr.comm ...

  4. iOS开发多线程在实际项目中的运用

    实际项目开发中为了能够给用户更好的体验,有些延时操作我们都会放在子线程中进行. 今天我们就来聊聊多线程在实际项目中的运用. 我们先来看看多线程的基础知识: 1.多线程的原理: 同一时间,CPU只能处理 ...

  5. 记不住git命令?试试这个命令浏览网站

    Find the right git commands without digging through the web. 找 git 命令,无需谷歌百度,无需 git -help,这个网站以图形界面的 ...

  6. JAVA的基本程序设计结构<上>

    一个简单的Java应用程序 public class FirstSample { public static void main(String[] args) { System.out.println ...

  7. OO第一单元(前四周)作业总结

    OO第一单元(前四周)作业总结 OO第一单元(前四周)作业总结要求(第四次作业) 0.前言 本次博客针对的是本人学习Java的第一阶段的三次作业的作业总结 第一次作业的内容是:7-1 计算税率 (20 ...

  8. 秦九韶算法 & 三分法

    前言 今天考试出了一个题 郭郭模拟退火骗了75分 于是再次把咕咕了好久的模退提上日程 如果进展顺利 明后天应该会开爬山算法和模退的博客笔记 今天先把今天考试的正解学习一下--三分法 引入 老规矩上板子 ...

  9. Linux学习笔记之linux软件包安装以及源的替换

    先是软件源的替换,在刚安装的Ubuntu中会配有原先的软件源,所以如果要替换时,可在网上找与自己ubuntu相对应的软件源,比如我的ubuntu版本为12.04,所以我得找到相对应能够适用Ubuntu ...

  10. C#LeetCode刷题之#387-字符串中的第一个唯一字符(First Unique Character in a String)

    问题 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/3939 访问. 给定一个字符串,找到它的第一个不重复的字符,并返回 ...