源码分析 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 相关文章推荐
改进的IP计数器
Oct 09 PHP
php不用正则采集速度探究总结
Mar 24 PHP
php 移除数组重复元素的一点说明
Nov 27 PHP
PHP开发中常用的字符串操作函数
Feb 08 PHP
PHP 查找字符串常用函数介绍
Jun 07 PHP
php file_get_contents抓取Gzip网页乱码的三种解决方法
Nov 12 PHP
php获取指定范围内最接近数的方法
Jun 02 PHP
Symfony核心类概述
Mar 17 PHP
PHP程序中的文件锁、互斥锁、读写锁使用技巧解析
Mar 21 PHP
php简单处理XML数据的方法示例
May 19 PHP
php实现与python进行socket通信的方法示例
Aug 30 PHP
PHP 获取 ping 时间的实现方法
Sep 29 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
全新的PDO数据库操作类php版(仅适用Mysql)
2012/07/22 PHP
php在数据库抽象层简单使用PDO的方法
2015/11/03 PHP
PHP实现的迪科斯彻(Dijkstra)最短路径算法实例
2017/09/16 PHP
关于php支持的协议与封装协议总结(推荐)
2017/11/17 PHP
支持汉转拼和拼音分词的PHP中文工具类ChineseUtil
2018/02/23 PHP
学习jquery必备 api中英文对照的chm手册 下载
2007/05/03 Javascript
动态创建样式表在各浏览器中的差异测试代码
2011/09/13 Javascript
brook javascript框架介绍
2011/10/10 Javascript
jQuery获取样式中的背景颜色属性值/颜色值
2012/12/17 Javascript
JQueryEasyUI datagrid框架的基本使用
2013/04/08 Javascript
jquery对ajax的支持介绍
2013/12/10 Javascript
网页从弹窗页面单选框传值至父页面代码分享
2015/09/29 Javascript
浅谈JavaScript 覆盖原型以及更改原型
2016/08/31 Javascript
jQuery EasyUI 选项卡面板tabs的使用实例讲解
2017/12/25 jQuery
关于vue中 $emit的用法详解
2018/04/12 Javascript
微信小程序实现上传图片裁剪图片过程解析
2019/08/22 Javascript
node实现mock-plugin中间件的方法
2019/12/25 Javascript
js中火星坐标、百度坐标、WGS84坐标转换实现方法示例
2020/03/02 Javascript
[00:29]2019完美世界全国高校联赛(秋季赛)总决赛海口落幕
2019/12/10 DOTA
python回调函数用法实例分析
2015/05/09 Python
分享Pycharm中一些不为人知的技巧
2018/04/03 Python
Python实现分段线性插值
2018/12/17 Python
python GUI库图形界面开发之PyQt5拖放控件实例详解
2020/02/25 Python
python中threading开启关闭线程操作
2020/05/02 Python
PyCharm 2020.1版安装破解注册码永久激活(激活到2089年)
2020/09/24 Python
英国最大的奢侈珠宝和手表网站:C W Sellors
2017/02/10 全球购物
俄罗斯EPL钻石珠宝店:ЭПЛ
2019/10/22 全球购物
J2EE中常用的名词进行解释
2015/11/09 面试题
结婚典礼证婚词
2014/01/08 职场文书
经济信息系毕业生自荐信
2014/06/02 职场文书
2014统计局民主生活会对照检查材料思想汇报
2014/10/02 职场文书
2015商场元旦促销活动策划方案
2014/12/09 职场文书
2014年节能降耗工作总结
2014/12/11 职场文书
医疗纠纷调解协议书
2015/08/06 职场文书
幽默导游词应该怎么写?
2019/08/26 职场文书
我收到了德劲DE1107
2022/04/05 无线电