源碼分析 Laravel 重複執行同一個隊列任務的原因講解

來源:互聯網
上載者:User
laravel 的佇列服務對各種不同的後台佇列服務提供了統一的 API,下面這篇文章通過源碼分析給大家介紹了關於 Laravel 重複執行同一個隊列任務的原因,文中通過範例程式碼介紹的非常詳細,需要的朋友可以參考借鑒,下面來一起看看吧。

前言

laravel 的佇列服務對各種不同的後台佇列服務提供了統一的 API。隊列允許你順延強制消耗時間的任務,比如發送一封郵件。這樣可以有效降低請求響應的時間。

發現問題

在 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 = falseif(job ~= false) then reserved = cjson.decode(job) reserved['attempts'] = reserved['attempts'] + 1 reserved = cjson.encode(reserved) redis.call('zadd', KEYS[2], ARGV[1], reserved)endreturn {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))) endendreturn true

這裡 zrangebyscore 找出分值從無限小到目前時間戳的元素,也就是 60 秒之前加入到集合的任務,然後通過 zremrangebyrank 從集合移除這些元素並 rpush 到隊列中。

看到這裡應該就恍然大悟了。

如果一個隊列 60 秒沒執行完,那麼進程在取隊列的時候從 reserved 集合中把這些任務又重新 rpush 到隊列中。

總結

您可能感興趣的文章:

關於 Laravel Redis 多個進程同時取隊列的問題詳解

php-msf源碼的詳解

thinkphp5 URL和路由的功能詳解與執行個體講解

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.