源码分析 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 相关文章推荐
BBS(php & mysql)完整版(七)
Oct 09 PHP
PHP 程序员也要学会使用“异常”
Jun 16 PHP
php 生成静态页面的办法与实现代码详细版
Feb 15 PHP
一些PHP Coding Tips(php小技巧)[2011/04/02最后更新]
May 02 PHP
php实例分享之二维数组排序
May 15 PHP
PHP判断文章里是否有图片的简单方法
Jul 26 PHP
php在线解压ZIP文件的方法
Dec 30 PHP
PHP实现获取文件后缀名的几种常用方法
Aug 08 PHP
基于PHP-FPM进程池探秘
Oct 17 PHP
php基于环形链表解决约瑟夫环问题示例
Nov 07 PHP
实例介绍PHP删除数组中的重复元素
Mar 03 PHP
PHPStorm2020.1永久激活及下载更新至2020(推荐)
Sep 25 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 5.0对象模型深度探索之对象复制
2008/03/27 PHP
PHP中基本HTTP认证技巧分析
2015/03/16 PHP
thinkphp 5框架实现登陆,登出及session登陆状态检测功能示例
2019/10/10 PHP
破除网页鼠标右键被禁用的绝招大全
2006/12/27 Javascript
JS window.opener返回父页面的应用
2009/10/24 Javascript
JQuery自适应IFrame高度(支持嵌套 兼容IE,ff,safafi,chrome)
2011/03/28 Javascript
读jQuery之二(两种扩展)
2011/06/11 Javascript
js函数setTimeout延迟执行的简单介绍
2013/07/17 Javascript
Jquery chosen动态设置值实例介绍
2013/08/08 Javascript
JavaScript中一个奇葩的IE浏览器判断方法
2014/04/16 Javascript
js交换排序 冒泡排序算法(Javascript版)
2014/10/04 Javascript
javascript中callee与caller的区别分析
2015/04/20 Javascript
基于OL2实现百度地图ABCD marker的效果
2015/10/01 Javascript
jQuery实现带有动画效果的回到顶部和底部代码
2015/11/04 Javascript
JavaScript实现阿拉伯数字和中文数字互相转换
2016/06/12 Javascript
js实现简单的二级联动效果
2017/03/09 Javascript
vue-axios使用详解
2017/05/10 Javascript
解决vue中使用swiper插件问题及swiper在vue中的用法
2018/04/04 Javascript
vue 集成jTopo 处理方法
2019/08/07 Javascript
Vue实现腾讯云点播视频上传功能的实现代码
2020/08/17 Javascript
手把手教你实现 Promise的使用方法
2020/09/02 Javascript
python标准算法实现数组全排列的方法
2015/03/17 Python
通过数据库向Django模型添加字段的示例
2015/07/21 Python
Python使用lxml模块和Requests模块抓取HTML页面的教程
2016/05/16 Python
Python3利用Dlib19.7实现摄像头人脸识别的方法
2018/05/11 Python
python使用phoenixdb操作hbase的方法示例
2019/02/28 Python
HTML5 本地存储实现购物车功能
2017/09/07 HTML / CSS
HTML5 Canvas鼠标与键盘事件demo示例
2013/07/04 HTML / CSS
英国品牌男装折扣网站:Brown Bag
2018/03/08 全球购物
Linux中如何设置Java环境变量(Ubuntu)
2016/07/24 面试题
《奇妙的国际互联网》 教学反思
2014/02/25 职场文书
公司转让协议书
2016/03/19 职场文书
CSS实现漂亮的时钟动画效果的实例代码
2021/03/30 HTML / CSS
php 防护xss,PHP的防御XSS注入的终极解决方案
2021/04/01 PHP
详解Django的MVT设计模式
2021/04/29 Python
Win11远程连接不上怎么办?Win11远程桌面用不了的解决方法
2022/08/05 数码科技