资讯专栏INFORMATION COLUMN

为什么 Laravel 会重复执行同一个队列任务?

vboy1010 / 1932人阅读

摘要:把因执行超时的队列从集合重新到当前执行的队列中。从要执行的队列中取任务可以看到在取要执行的队列的时候,同时会放一份到一个有序集合中,并使用过期时间戳作为分值。

(原文链接:https://blog.tanteng.me/2017/...)

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

先说原因:因为在 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 到队列中。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/26260.html

相关文章

  • Laravel5.2队列驱动expire参数设置带来的重复执行问题 数据库驱动

    摘要:已经取消了参数,都用来执行。取数据的过程事物处理已经打开。取得符合条件的队列后程序会更新该条数据,并且更新完后即。 connections => [ .... database => [ driver => database, table => jobs, queue => defaul...

    ysl_unh 评论0 收藏0
  • Laravel 技巧之 定时任务

    摘要:对于定时任务的基本用法,官网文档已经描述得很详细了,这里不再多说。这种情况下如果定时任务能够并行执行,就不会有这样的问题。这个时候我们希望能够像队列那样,将定时任务分散到多台服务器上。 定时任务 Scheduled Tasks 是 Laravel 提供的组件之一,稍微上点规模的项目应该都会用到,比如开发微信应用时通过定时任务去刷新access token,比如每天定时发推送提现用户要记...

    keithyau 评论0 收藏0
  • Laravel 5.7 最佳实践和开发技巧分享

    摘要:当查询数据时,本地范围允许我们创建自己的查询构造器链式方法。这样便会知道这是一个本地范围并且可以在查询构造器中使用。某些查询构造器不可用或者说可用但是方法名不同,关于这些请查阅所有集合的方法。 showImg(https://segmentfault.com/img/remote/1460000017877956?w=800&h=267); Laravel 因可编写出干净,可用可调试的...

    ninefive 评论0 收藏0
  • 高性能千万级定时任务管理服务forsun laravel插件使用详解

    摘要:高性能高精度定时服务,轻松管理千万级定时任务。支持任务到期触发和。支持创建延时任务和定时到期任务,和原生保持相同接口,轻松使用。不支持任务输出任务钩子及维护模式。是不指定任务名时自动生成,每个任务名必须唯一,相同任务名重复定义将会自动覆盖。 Forsun高性能高精度定时服务,轻松管理千万级定时任务。 定时服务项目地址:https://github.com/snower/forsun l...

    Muninn 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<