源码分析 Laravel 重复执行同一个队列任务的原因


Posted in PHP onDecember 25, 2017

前言

laravel 的队列服务对各种不同的后台队列服务提供了统一的 API。队列允许你延迟执行消耗时间的任务,比如发送一封邮件。这样可以有效的降低请求响应的时间。

发现问题

在 Laravel 中使用 Redis 处理队列任务,框架提供的功能非常强大,但是最近遇到一个问题,就是发现一个任务被多次执行,这是为什么呢?

源码分析 Laravel 重复执行同一个队列任务的原因

先说原因:

因为在 Laravel 中如果一个队列(任务)执行时间大于 60 秒,就会被认为执行失败并重新加入队列中,这样就会导致重复执行同一个任务。

这个任务的逻辑就是给用户推送内容,需要根据队列内容取出用户并遍历,通过请求后端 HTTP 接口发送。比如有 10000 个用户,在用户数量多或接口处理速度没那么快的情况下,执行时间肯定会大于 60 秒,于是这个任务就被重新加入队列。情况更糟糕一点,前面的任务如果都没有在 60 秒执行完,就都会重新加入队列,这样同一个任务就不止重复执行一次了,而是多次。

下面从 Laravel 源代码找一下罪魁祸首。

源代码文件:vendor/laravel/framework/src/Illuminate/Queue/RedisQueue.php

/**
 * The expiration time of a job.
 *
 * @var int|null
 */
protected $expire = 60;

这个 $expire 成员变量是一个固定的值,Laravel 认为一个队列再怎么 60 秒也该执行完了吧。取队列方法:

public function pop($queue = null)
{
 $original = $queue ?: $this->default; 
 $queue = $this->getQueue($queue); 
 $this->migrateExpiredJobs($queue.':delayed', $queue); 
 if (! is_null($this->expire)) {
  $this->migrateExpiredJobs($queue.':reserved', $queue);
 } 
 list($job, $reserved) = $this->getConnection()->eval(
  LuaScripts::pop(), 2, $queue, $queue.':reserved', $this->getTime() + $this->expire
 ); 
 if ($reserved) {
  return new RedisJob($this->container, $this, $job, $reserved, $original);
 }
}

取队列有几步操作,因为队列执行失败,或执行超时等都会放入另外的集合保存起来,以便重试,过程如下:

    1.把因执行失败的队列从 delayed 集合重新 rpush 到当前执行的队列中。

    2.把因执行超时的队列从 reserved 集合重新 rpush 到当前执行的队列中。

    3.然后才是从队列中取任务开始执行,同时把队列放入 reserved 的有序集合。

这里使用了 eval 命令执行这个过程,用到了几个 lua 脚本。

从要执行的队列中取任务:

local job = redis.call('lpop', KEYS[1])
local reserved = false
if(job ~= false) then
 reserved = cjson.decode(job)
 reserved['attempts'] = reserved['attempts'] + 1
 reserved = cjson.encode(reserved)
 redis.call('zadd', KEYS[2], ARGV[1], reserved)
end
return {job, reserved}

可以看到 Laravel 在取 Redis 要执行的队列的时候,同时会放一份到一个有序集合中,并使用过期时间戳作为分值。

只有当这个任务完成后,再把有序集合中这个任务移除。从这个有序集合移除队列的代码就省略,我们看一下 Laravel 如何处理执行时间大于 60 秒的队列。

也就是这段 lua 脚本执行的操作:

local val = redis.call('zrangebyscore', KEYS[1], '-inf', ARGV[1])
if(next(val) ~= nil) then
 redis.call('zremrangebyrank', KEYS[1], 0, #val - 1)
 for i = 1, #val, 100 do
  redis.call('rpush', KEYS[2], unpack(val, i, math.min(i+99, #val)))
 end
end
return true

这里 zrangebyscore 找出分值从无限小到当前时间戳的元素,也就是 60 秒之前加入到集合的任务,然后通过 zremrangebyrank 从集合移除这些元素并 rpush 到队列中。

看到这里应该就恍然大悟了。

如果一个队列 60 秒没执行完,那么进程在取队列的时候从 reserved 集合中把这些任务又重新 rpush 到队列中。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

PHP 相关文章推荐
PHP 执行系统外部命令 system() exec() passthru()
Aug 11 PHP
解析php利用正则表达式解决采集内容排版的问题
Jun 20 PHP
PHP代码优化的53个细节
Mar 03 PHP
destoon后台网站设置变成空白的解决方法
Jun 21 PHP
PHP多态代码实例
Jun 26 PHP
在Laravel框架里实现发送邮件实例(邮箱验证)
May 20 PHP
Yii多表联合查询操作详解
Jun 02 PHP
laravel中的错误与日志用法详解
Jul 26 PHP
PHP 匿名函数与注意事项详细介绍
Nov 26 PHP
php usort 使用用户自定义的比较函数对二维数组中的值进行排序
May 02 PHP
php微信公众号开发之答题连闯三关
Oct 20 PHP
PHP实现二维数组(或多维数组)转换成一维数组的常见方法总结
Dec 04 PHP
浅析PHP中的闭包和匿名函数
Dec 25 #PHP
thinkphp5 加载静态资源路径与常量的方法
Dec 24 #PHP
PHP读取并输出XML文件数据的简单实现方法
Dec 22 #PHP
ajax+php实现无刷新验证手机号的实例
Dec 22 #PHP
Thinkphp5行为使用方法汇总
Dec 21 #PHP
PHP二维数组实现去除重复项的方法【保留各个键值】
Dec 21 #PHP
Laravel 5.5基于内置的Auth模块实现前后台登陆详解
Dec 21 #PHP
You might like
php重定向的三种方法分享
2012/02/22 PHP
基于php 随机数的深入理解
2013/06/05 PHP
ThinkPHP中RBAC类的四种用法分析
2014/11/24 PHP
PHP学习笔记之php文件操作
2016/06/03 PHP
Laravel构建即时应用的一种实现方法详解
2017/08/31 PHP
Laravel框架实现的rbac权限管理操作示例
2019/01/16 PHP
在PHP中输出JS语句以及乱码问题的解决方案
2019/02/13 PHP
WordPress伪静态规则设置代码实例
2020/12/10 PHP
js如何实现设计模式中的模板方法
2013/07/23 Javascript
nodejs获取本机内网和外网ip地址的实现代码
2014/06/01 NodeJs
JavaScript函数柯里化详解
2016/04/29 Javascript
利用Javascript仿Excel的数据透视分析功能
2016/09/07 Javascript
javascript使用递归算法求两个数字组合功能示例
2017/01/03 Javascript
基于javascript实现数字英文验证码
2017/01/25 Javascript
bootstrap日期控件问题(双日期、清空等问题解决)
2017/04/19 Javascript
vue父组件通过props如何向子组件传递方法详解
2017/08/16 Javascript
jQuery.parseJSON()函数详解
2019/02/28 jQuery
如何自定义微信小程序tabbar上边框的颜色
2019/07/09 Javascript
Element InputNumber 计数器的实现示例
2020/08/03 Javascript
python paramiko实现ssh远程访问的方法
2013/12/03 Python
在java中如何定义一个抽象属性示例详解
2017/08/18 Python
基于Django的python验证码(实例讲解)
2017/10/23 Python
通过实例简单了解Python中yield的作用
2019/12/11 Python
详解Python3 中的字符串格式化语法
2020/01/15 Python
解决pyinstaller打包运行程序时出现缺少plotly库问题
2020/06/02 Python
PHP中如何创建和修改数组
2012/05/02 面试题
毕业生就业自荐书
2013/12/15 职场文书
护理个人求职信范文
2014/01/08 职场文书
天猫某品牌专卖店运营计划书
2014/03/21 职场文书
企业厂务公开实施方案
2014/03/26 职场文书
医德医风演讲稿
2014/05/20 职场文书
小学阳光体育活动总结
2014/07/05 职场文书
加强作风建设演讲稿
2014/10/24 职场文书
演讲稿之开卷有益
2019/08/07 职场文书
html5表单的required属性使用
2021/07/07 HTML / CSS
java设计模式--七大原则详解
2021/07/21 Java/Android