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. 焦点损失函数 Focal Loss 与 GHM

    文章来自公众号[机器学习炼丹术] 1 focal loss的概述 焦点损失函数 Focal Loss(2017年何凯明大佬的论文)被提出用于密集物体检测任务. 当然,在目标检测中,可能待检测物体有10 ...

  2. c++ explict

    explicit 用于一个参数的构造函数:防止隐式转换. 什么意思呢? myClass(int x); 这是个构造函数 我们可以使用 myClass a(4);  或 myClass a=4;来调用它 ...

  3. win10 64位 汇编环境

    masm6或者masm5 下载. dosbox 下载安装 为何要用这个呢,因为 机子是64位的,dosbox 模拟32位的用来执行生成的exe文件 masm 安装好后,有个bin文件:个人建议将其设置 ...

  4. NCoreCoder.Aop详解

    于今天,功能终于完善度到比较满意的程度了 准备好好写一篇文章,而不是之前的流水账,分享一下最近这些天的踩坑 一开始AOP选的微软提供的DispatchProxy 关于这个,有大佬的文章,可以看看,了解 ...

  5. DataGrip 2020.1 安装与激活

    1 软件下载 百度网盘: 链接:https://pan.baidu.com/s/1kHSq1XS0i4YDF0HuzsxCLg 提取码:djyc 2 安装 解压文件后点击 datagrip-2020. ...

  6. 初步理解@Transactional注解

    在SSM项目中,经常在业务层的类或者方法上看到@Transactional注解,只是知道这个注解的作用是进行事务管理,但是具体有哪些属性,在什么情况下进行回滚,确是不那么清楚.所以在网上看了一些视频和 ...

  7. 19、Java 序列化

    1.序列化的概念,意义以及使用场景 序列化: 将对象写入到IO流中,也就是把Java对象转换为字节序列的过程 反序列化: 从IO流中恢复对象*,也就是把字节序列恢复为Java对象的过程 意义: 序列化 ...

  8. 制作 macOS Sierra U盘USB启动安装盘方法教程 (亲测)

    备注:相关镜像到apple官网下载 https://discussionschinese.apple.com/thread/250596904 进去点击"请使用这个 App Store 链接 ...

  9. Vue Vuex中的严格模式/实例解析/dispatch/commit /state/getters

    严格模式 import getters from './getters' import mutations from './mutations' import actions from './acti ...

  10. 搭建 WordPress 博客教程

    搭建 WordPress 博客教程(超详细) 在 2018年7月29日 上张贴 由 suncent一条评论 本文转自:静候那一米阳光 链接:https://www.jianshu.com/p/5675 ...