源码分析 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 操作数组(合并,拆分,追加,查找,删除等)
Jul 20 PHP
Fine Uploader文件上传组件应用介绍
Jan 06 PHP
JoshChen_php新手进阶高手不可或缺的规范介绍
Aug 16 PHP
非常实用的php弹出错误警告函数扩展性强
Jan 17 PHP
php获取服务器端mac和客户端mac的地址支持WIN/LINUX
May 15 PHP
php实现图片文件与下载文件防盗链的方法
Nov 03 PHP
php实现Linux服务器木马排查及加固功能
Dec 29 PHP
PHP使用DirectoryIterator显示下拉文件列表的方法
Mar 13 PHP
php+mysql实现的二级联动菜单效果详解
May 10 PHP
php将服务端的文件读出来显示在web页面实例
Oct 31 PHP
php实现的双色球算法示例
Jun 20 PHP
tp5框架使用composer实现日志记录功能示例
Jan 10 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 shell超强免杀、减少体积工具实现代码
2012/10/16 PHP
一组PHP加密解密函数分享
2014/06/05 PHP
学习php设计模式 php实现桥梁模式(bridge)
2015/12/07 PHP
PHP实现超简单的SSL加密解密、验证及签名的方法示例
2017/08/28 PHP
javascript 上下banner替换具体实现
2013/11/14 Javascript
node.js中的fs.exists方法使用说明
2014/12/17 Javascript
Js控制滑轮左右滑动实例
2015/02/13 Javascript
Javascript数据结构与算法之列表详解
2015/03/12 Javascript
属于你的jQuery提示框(Tip)插件
2016/01/20 Javascript
初探nodeJS
2017/01/24 NodeJs
JavaScript两个变量交换值的实现方法
2017/03/01 Javascript
Angular+Bootstrap+Spring Boot实现分页功能实例代码
2017/07/21 Javascript
Vue.js 利用v-for中的index值实现隔行变色
2018/08/01 Javascript
JavaScript作用域链实例详解
2019/01/21 Javascript
vue-router二级导航切换路由及高亮显示的实现方法
2019/07/10 Javascript
JavaScript和TypeScript中的void的具体使用
2019/09/12 Javascript
vue+canvas实现移动端手写签名
2020/05/21 Javascript
Python 文件操作技巧(File operation) 实例代码分析
2008/08/11 Python
python中偏函数partial用法实例分析
2015/07/08 Python
Android应用开发中Action bar编写的入门教程
2016/02/26 Python
Python中使用多进程来实现并行处理的方法小结
2017/08/09 Python
python 中的list和array的不同之处及转换问题
2018/03/13 Python
Python使用itchat模块实现简单的微信控制电脑功能示例
2019/08/26 Python
python excel转换csv代码实例
2019/08/26 Python
Python 实现一个手机号码获取妹子名字的功能
2019/09/25 Python
tensorflow之获取tensor的shape作为max_pool的ksize实例
2020/01/04 Python
Python+OpenCV实现图像的全景拼接
2020/03/05 Python
VScode连接远程服务器上的jupyter notebook的实现
2020/04/23 Python
Pyinstaller加密打包应用的示例代码
2020/06/11 Python
python图片合成的示例
2020/11/09 Python
Html5实现如何在两个div元素之间拖放图像
2013/03/29 HTML / CSS
canvas使用注意点总结
2013/07/19 HTML / CSS
写给女朋友的检讨书
2014/01/28 职场文书
博士生专家推荐信
2014/09/26 职场文书
交警正风肃纪剖析材料
2014/10/29 职场文书
Java实现简易的分词器功能
2021/06/15 Java/Android