Laravel chunk和chunkById的坑
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 |
/** |
能看到这个方法有四个参数count,callback,column,alias。
默认的column为null,第一行会进行默认赋值。
1 |
$column = is_null($column) ? $this->getModel()->getKeyName() : $column; |
往下跟:
1 |
/** |
能看到默认的column为id。
进入forPageAfterId方法。
1 |
/** |
能看到如果lastId不为0则自动添加where语句,还会自动添加order by column。
看到这里就明白了。上文的chunkById没有添加column参数,所以底层自动添加了order by id。走了主键索引,没有使用上mark_time的索引。导致查询效率非常低。
chunkById的源码显示了我们可以传递一个column字段来让底层使用这个字段来order by。
代码修改如下:
1 |
$this->dao->where('type', 99)->whereBetween('mark_time', [$date, $date+86399])->select(array('mark_time', 'id'))->chunkById(1000, function ($rows){
|
这样最后执行的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的坑的更多相关文章
- Laravel学习--关于Relation的坑
前段时间比较忙,就没有坚持写博客,但发现这周末再想捡起来,好难,一直到了今天晚上,才决定坐下来写一篇,哈哈哈-- 最近在用 Laravel 5.2,踩了几个关于 Relation 的坑,在这里用博客记 ...
- Laravel登录验证碰到的坑 哈希验证匹配问题
用laravel 写登录验证 本来是用Crypt加密 添加用户到数据库的 后来验证密码 解密时一直报错 The payload is invaild 由于本人是laravel框架小白 自己思考许久未 ...
- 后端PHP框架laravel学习踩的各种坑
安装完laravel的ventor目录后出现“Whoops, looks like something went wrong.”这样的错误信息 打开config/app.php,打开debug为tru ...
- php laravel 环境搭建
最近上一个新项目,时间比较紧,为了满足业务需求,没有办法,只有上我大 php 了,找了一个带些基础的数据结构,用的是 laravel 搭建的,然后寻坑就开始了,先是构建 docker 镜像就坑了,然后 ...
- laravel 使用不同账号发送邮件的问题
业务背景: 公司自己做的oa系统,不同的模块需要用不同的邮箱发送信息给收件人.比如:员工离职的时候用离职的邮箱发送离职邮件通知,员工入职的时候用入职的邮箱发送入职邮件通知.发邮件是一件耗时的任务,如果 ...
- 利用Git搭建自动部署的Laravel环境 - 钟晨宇的博客 - CSDN博客
目标:服务器上搭建Laravel环境,本地使用IDE进行开发,使用Homestead做本地调试环境,代码提交后自动部署到服务器Root目录下. 下面是整个流程的示意图: 1. 准备工作,搭建LNMP ...
- webpack中利用require.ensure()实现按需加载
webpack中的require.ensure()可以实现按需加载资源包括js,css等,它会给里面require的文件单独打包,不和主文件打包在一起,webpack会自动配置名字,如0.js,1.j ...
- webpack中实现按需加载
webpack中的require.ensure()可以实现按需加载资源包括js,css等,它会给里面require的文件单独打包,不和主文件打包在一起,webpack会自动配置名字,如0.js,1.j ...
- require-ensure
require-ensure 说明: require.ensure在需要的时候才下载依赖的模块,当参数指定的模块都下载下来了(下载下来的模块还没执行),便执行参数指定的回调函数.require.ens ...
随机推荐
- TearmQuery()
lucene 中的 TearmQuery() 在.search( tearmQuery, q)查询时 比较矫情 q只能是小写
- 【FZYZOJ】无向图的联通图个数 题解(组合数学)
题目大意:求无向图的连通图个数.由于个数可能很大,只需要求出结果$mod1000000009$的值.$n\leq 1000$ ------------------------- 对于一个含有$n$个结 ...
- day22:面向对象封装对象操作&类操作&面向对象删除操作
面向对象程序开发 1.类的三种定义方式 class MyClass: pass class MyClass(): #(推荐) pass class MyClass(object): # object类 ...
- FAT32文件系统下的文件、目录管理
背景 FAT32作为一个文件系统,有着广泛的应用.在安装操作系统时,许多个人用户默认都选择FAT32文件系统,因此,了解FAT32文件系统下的文件.目录的管理方式就显得非常必要了. FAT32文件系统 ...
- Python 实现 T00ls 自动签到脚本(邮件+钉钉通知)
T00ls 每日签到是可以获取 TuBi 的,由于常常忘记签到,导致损失了很多 TuBi .于是在 T00ls 论坛搜索了一下,发现有不少大佬都写了自己的签到脚本,签到功能实现.定时任务执行以及签到提 ...
- Spring Cloud 系列之 ZooKeeper 注册中心
什么是注册中心 服务注册中心是服务实现服务化管理的核心组件,类似于目录服务的作用,主要用来存储服务信息,譬如提供者 url 串.路由信息等.服务注册中心是微服务架构中最基础的设施之一. 注册中心可以说 ...
- 【Canal】数据同步的终极解决方案,阿里巴巴开源的Canal框架当之无愧!!
写在前面 在当今互联网行业,尤其是现在分布式.微服务开发环境下,为了提高搜索效率,以及搜索的精准度,会大量使用Redis.Memcached等NoSQL数据库,也会使用大量的Solr.Elastics ...
- DotNet Core
安装 dotnet add package Pomelo.EntityFrameworkCore.MySql 使用 MySQL 作为后端 在继承 DbContext 类中重写 OnConfig ...
- Docker 启动 OpenResty
Docker 启动 OpenResty OpenResty 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库.第三方模块以及大多数的依赖项.用于方便地搭建 ...
- java JDBC工具类
package com.oracle.tools; import java.sql.Connection; import java.sql.DriverManager; import java.sql ...