源碼分析 Laravel 重復(fù)執(zhí)行同一個(gè)隊(duì)列任務(wù)的原因
前言
laravel 的隊(duì)列服務(wù)對(duì)各種不同的后臺(tái)隊(duì)列服務(wù)提供了統(tǒng)一的 API。隊(duì)列允許你延遲執(zhí)行消耗時(shí)間的任務(wù),比如發(fā)送一封郵件。這樣可以有效的降低請(qǐng)求響應(yīng)的時(shí)間。
發(fā)現(xiàn)問題
在 Laravel 中使用 Redis 處理隊(duì)列任務(wù),框架提供的功能非常強(qiáng)大,但是最近遇到一個(gè)問題,就是發(fā)現(xiàn)一個(gè)任務(wù)被多次執(zhí)行,這是為什么呢?
先說(shuō)原因:
因?yàn)樵?Laravel 中如果一個(gè)隊(duì)列(任務(wù))執(zhí)行時(shí)間大于 60 秒,就會(huì)被認(rèn)為執(zhí)行失敗并重新加入隊(duì)列中,這樣就會(huì)導(dǎo)致重復(fù)執(zhí)行同一個(gè)任務(wù)。
這個(gè)任務(wù)的邏輯就是給用戶推送內(nèi)容,需要根據(jù)隊(duì)列內(nèi)容取出用戶并遍歷,通過(guò)請(qǐng)求后端 HTTP 接口發(fā)送。比如有 10000 個(gè)用戶,在用戶數(shù)量多或接口處理速度沒那么快的情況下,執(zhí)行時(shí)間肯定會(huì)大于 60 秒,于是這個(gè)任務(wù)就被重新加入隊(duì)列。情況更糟糕一點(diǎn),前面的任務(wù)如果都沒有在 60 秒執(zhí)行完,就都會(huì)重新加入隊(duì)列,這樣同一個(gè)任務(wù)就不止重復(fù)執(zhí)行一次了,而是多次。
下面從 Laravel 源代碼找一下罪魁禍?zhǔn)住?/strong>
源代碼文件:vendor/laravel/framework/src/Illuminate/Queue/RedisQueue.php
/** * The expiration time of a job. * * @var int|null */ protected $expire = 60;
這個(gè) $expire 成員變量是一個(gè)固定的值,Laravel 認(rèn)為一個(gè)隊(duì)列再怎么 60 秒也該執(zhí)行完了吧。取隊(duì)列方法:
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); } }
取隊(duì)列有幾步操作,因?yàn)殛?duì)列執(zhí)行失敗,或執(zhí)行超時(shí)等都會(huì)放入另外的集合保存起來(lái),以便重試,過(guò)程如下:
1.把因執(zhí)行失敗的隊(duì)列從 delayed 集合重新 rpush 到當(dāng)前執(zhí)行的隊(duì)列中。
2.把因執(zhí)行超時(shí)的隊(duì)列從 reserved 集合重新 rpush 到當(dāng)前執(zhí)行的隊(duì)列中。
3.然后才是從隊(duì)列中取任務(wù)開始執(zhí)行,同時(shí)把隊(duì)列放入 reserved 的有序集合。
這里使用了 eval 命令執(zhí)行這個(gè)過(guò)程,用到了幾個(gè) lua 腳本。
從要執(zhí)行的隊(duì)列中取任務(wù):
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 要執(zhí)行的隊(duì)列的時(shí)候,同時(shí)會(huì)放一份到一個(gè)有序集合中,并使用過(guò)期時(shí)間戳作為分值。
只有當(dāng)這個(gè)任務(wù)完成后,再把有序集合中這個(gè)任務(wù)移除。從這個(gè)有序集合移除隊(duì)列的代碼就省略,我們看一下 Laravel 如何處理執(zhí)行時(shí)間大于 60 秒的隊(duì)列。
也就是這段 lua 腳本執(zhí)行的操作:
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 找出分值從無(wú)限小到當(dāng)前時(shí)間戳的元素,也就是 60 秒之前加入到集合的任務(wù),然后通過(guò) zremrangebyrank 從集合移除這些元素并 rpush 到隊(duì)列中。
看到這里應(yīng)該就恍然大悟了。
如果一個(gè)隊(duì)列 60 秒沒執(zhí)行完,那么進(jìn)程在取隊(duì)列的時(shí)候從 reserved 集合中把這些任務(wù)又重新 rpush 到隊(duì)列中。
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
- Laravel學(xué)習(xí)教程之從入口到輸出過(guò)程詳解
- Laravel框架源碼解析之反射的使用詳解
- Laravel源碼解析之路由的使用和示例詳解
- 通過(guò)源碼解析Laravel的依賴注入
- Laravel框架數(shù)據(jù)庫(kù)CURD操作、連貫操作總結(jié)
- PHP開發(fā)框架Laravel數(shù)據(jù)庫(kù)操作方法總結(jié)
- Laravel框架表單驗(yàn)證詳解
- Laravel框架中擴(kuò)展函數(shù)、擴(kuò)展自定義類的方法
- Laravel框架路由配置總結(jié)、設(shè)置技巧大全
- PHP的Laravel框架中使用消息隊(duì)列queue及異步隊(duì)列的方法
- PHP框架Laravel插件Pagination實(shí)現(xiàn)自定義分頁(yè)
- Laravel 5框架學(xué)習(xí)之向視圖傳送數(shù)據(jù)
- Laravel框架源碼解析之入口文件原理分析
相關(guān)文章
基于PHP的登錄和注冊(cè)的功能的實(shí)現(xiàn)
這篇文章主要介紹了基于PHP的登錄和注冊(cè)的功能的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08關(guān)于 Laravel Redis 多個(gè)進(jìn)程同時(shí)取隊(duì)列問題詳解
這篇文章主要給大家介紹了關(guān)于 Laravel Redis 多個(gè)進(jìn)程同時(shí)取隊(duì)列問題的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2017-12-12laravel 解決ajax異步提交數(shù)據(jù),并還回填充表格的問題
今天小編就為大家分享一篇laravel 解決ajax異步提交數(shù)據(jù),并還回填充表格的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-10-10php 模擬 asp.net webFrom 按鈕提交事件實(shí)例
需要php方面的項(xiàng)目開發(fā),php剛剛?cè)腴T,在寫按鈕提交過(guò)程中,asp.net里的按鈕事件更好些,下面有個(gè)實(shí)例,大家可以看看2014-10-10PHP 中提示undefined index如何解決(多種方法)
這篇文章主要介紹了PHP 中提示undefined index如何解決(多種方法)的相關(guān)資料,需要的朋友可以參考下2016-03-03PHP基于自增數(shù)據(jù)如何生成不重復(fù)的隨機(jī)數(shù)示例
這篇文章主要給大家介紹了利用PHP基于自增數(shù)據(jù)如何能生成不重復(fù)的隨機(jī)數(shù),文中給出了詳細(xì)的示例代碼供大家參考學(xué)習(xí),對(duì)大家具有一定的參考價(jià)值,需要的朋友們下面來(lái)一起看看吧。2017-05-05PHP將敏感文字內(nèi)容替換為星號(hào)的操作方法
在PHP中,將敏感文字用星號(hào)替換通常涉及到字符串的搜索和替換操作,下面是一個(gè)基本的示例,展示如何將特定的敏感詞替換為星號(hào),感興趣的朋友跟隨小編一起看看吧2024-04-04